ASPN ActiveState Programmer Network  
ActiveState, a division of Sophos
/ Home / Perl / PHP / Python / Tcl / XSLT /
/ Safari / My ASPN /
Cookbooks | Documentation | Mailing Lists | Modules | News Feeds | Products | User Groups
Submit Recipe
My Recipes

All Recipes
All Cookbooks


View by Category

Title: Restricting processing to consider only the first instance of an element
Submitter: Brian Quinlan (other recipes)
Last Updated: 2001/08/03
Version no: 1.0
Category: XPath Tricks

 

4 stars 1 vote(s)


Editors pick

Description:

Sometimes it is desirable to operate on an element type only once, even if it appears many times in the document, but still have the opertunity to collect information about every occurance.

Source: Text Source

<!-- XSLT --> 
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output indent="yes"/>
    <xsl:template match="/">
        <!-- Do this to suppress the display of all the text in the document--> 
        <xsl:apply-templates select="//SPEECH/SPEAKER"/>
    </xsl:template>
    <xsl:template match="//SPEECH/SPEAKER">
        <xsl:variable name="temp" select="string(.)"/>
        <!-- Only execute if we haven't seen the current speaker before--> 
        <xsl:if test="self::*[not(preceding::SPEAKER[.=$temp])]">
            <SPEAKER><xsl:value-of select="$temp"/></SPEAKER>
            <LINES>
                <!-- Collect all of the lines spoken by the current speaker --> 
                <xsl:for-each select="//SPEECH[SPEAKER=$temp]/LINE">
                    <LINE><xsl:value-of select="."/></LINE>
                </xsl:for-each>
            </LINES>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

<!-- XML -->
<SCENE>
    <SPEECH>
        <SPEAKER>BERNARDO</SPEAKER>
        <LINE>Who's there?</LINE>
    </SPEECH>

    <SPEECH>
        <SPEAKER>FRANCISCO</SPEAKER>
        <LINE>Nay, answer me: stand, and unfold yourself.</LINE>
    </SPEECH>

    <SPEECH>
        <SPEAKER>BERNARDO</SPEAKER>
        <LINE>Long live the king!</LINE>
    </SPEECH>

    <SPEECH>
        <SPEAKER>FRANCISCO</SPEAKER>
        <LINE>Bernardo?</LINE>
    </SPEECH>
</SCENE>

<!-- Output -->
<?xml version="1.0" encoding="UTF-8"?>
<SCENE>
    <LINES>
        <SPEAKER>BERNARDO</SPEAKER>
        <LINE>Who's there?</LINE>
        <LINE>Long live the king!</LINE>
    </LINES>
    <LINES>
        <SPEAKER>FRANCISCO</SPEAKER>
        <LINE>Nay, answer me: stand, and unfold yourself.</LINE>
        <LINE>Bernardo?</LINE>
    </LINES>
</SCENE>

The license for this recipe is available here.

Discussion:

This example takes a scene from a play and groups the lines spoken by actor, instead of by their cronologic appearance in the play.

The key point is the use of the "preceding" XPath axis to ensure that the speaker is only considered if no speaker with the same name has appeared previously in the document.



Add comment

Number of comments: 3

Methinks it is a special case of the problem described that is solved here, Peter Jentsch, 2001/10/20
Usually, if you want to take an action only for the first occurance of some element *type*, you'd use the position predicate or its shortcut, for example to select the first and then do something with it.

Here you don't only look at the element *type*, but also at it's content. Which is something slightly different.

Add comment

Code Does not produce correct results, Joseph Feser, 2001/12/17
This is the actual output produced by the code above.

In order to get the SPEAKER node inside the LINES node, you would need to move the line

<SPEAKER><xsl:value-of select="$temp"/></SPEAKER>

Down inside the LINES node.

<?xml version="1.0"?>

<SPEAKER>BERNARDO</SPEAKER>
<LINES>
<LINE>Who's there?</LINE>
<LINE>Long live the king!</LINE>
</LINES>
<SPEAKER>FRANCISCO</SPEAKER>
<LINES>
<LINE>Nay, answer me: stand, and unfold yourself.</LINE>
<LINE>Bernardo?</LINE>
</LINES>


---------------------------------------------------------------------

In order to get the exact output as specified in the "OUTPUT", you would need to replace the / template with this:.

<xsl:template match="/">
<!-- Do this to suppress the display of all the text in the document-->
<SCENE>
<xsl:apply-templates select="//SPEECH/SPEAKER"/>
</SCENE>
</xsl:template>


-------------------------------------------------------------------
Another Example of the entire stylesheet that would produce the correct results in less time (if more than two nodes existed) would be:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" method="xml" indent="yes" encoding="iso8859-1"/>
<xsl:key name="speaker" match="SPEECH" use="SPEAKER" />
<xsl:template match="/">
<!-- Do this to suppress the display of all the text in the document-->
<SCENE>
<xsl:apply-templates select="SCENE/SPEECH[count(. | key('speaker', SPEAKER)[1]) = 1]/SPEAKER"/>
</SCENE>
</xsl:template>
<xsl:template match="SPEAKER">
<xsl:variable name="temp" select="."/>
<LINES>
<!-- Collect all of the lines spoken by the current speaker --> 
<SPEAKER><xsl:value-of select="$temp"/></SPEAKER>
<xsl:for-each select="//SPEECH[SPEAKER=$temp]/LINE">
<LINE><xsl:value-of select="."/></LINE>
</xsl:for-each>
</LINES>
</xsl:template>
</xsl:stylesheet>

Joe
Add comment

Performance Issues, Wolfgang Werner, 2004/09/29
Hi, my estimate would be that 'self::*[not(preceding::SPEAKER[.=$temp])]' is quite slow on larger documents, but i didn't really check it. I had some bad experience when using '//*[@id = $idref]' on bigger documents. Does anyone know if you can compare these expressions in terms of performance? Regards, Wolfgang
Add comment



Highest rated recipes:

1. Search and Replace

2. Generating a newline

3. Internationalization ...

4. Restricting processing ...

5. Result Pagination with ...

6. Fetching information ...

7. Getting text children of ...

8. Creating empty elements




Privacy Policy | Email Opt-out | Feedback | Syndication
© 2006 ActiveState Software Inc. All rights reserved.