Processing Nodes by Position

Problem

You want to process nodes in a sequence that is a function of their position in a document or node set.

Solution

Use xsl:sort with the select set to the position( ) or last( ) functions. The most trivial application of this example processes nodes in reverse document order:

<xsl:apply-templates>
     <xsl:sort select="position(  )" order="descending" data-type="number"/>
</xsl:apply-templates>

or:

<xsl:for-each select="*">
     <xsl:sort select="position(  )" order="descending" data-type="number"/>
     <!-- ... -->
</xsl:for-each>

Another common version of this example traverses a node set as if it were a matrix of a specified number of columns. Here, you process all nodes in the first column, then the second, and then the third:

<xsl:for-each select="*">
     <xsl:sort select="(position(  ) - 1)  mod 3" />
     <!-- ... -->
</xsl:for-each>

Or, perhaps more cleanly with:

<xsl:for-each select="*[position(  ) mod 3 = 1]">
    <xsl:apply-templates 
         select=". | following-sibling::*[position(  ) &lt; 3]" />
</xsl:for-each>

Sometimes you need to use position( ) to separate the first node in a node set from the remaining nodes. Doing so lets you perform complex aggregation operations on a document using recursion. I call this example recursive-aggregation. The abstract form of this example follows:

<xsl:template name="aggregation">
     <xsl:param name="node-set"/>
     <xsl:choose>
       <xsl:when test="$node-set">
         <!--We compute some function of the first element that produces 
         a value that we want to aggregate. The function may depend on
         the type of the element (i.e. it can be polymorphic)-->
         <xsl:variable name="first">
          <xsl:apply-templates select="$node-set[1]" mode="calc"/>
         </xsl:variable>
         <!--We recursivly process the remaining nodes using position(  ) -->
         <xsl:variable name="rest">
          <xsl:call-template name="aggregation">
            <xsl:with-param name="node-set" 
              select="$node-set[position(  )!=1]"/>
            </xsl:call-template>
         </xsl:variable>
         <!-- We perform some aggragation operation. This might not require
            a call to a template. For example, this might be 
            $first + $rest           or 
            $first * $rest           or
            concat($first,$rest)      etc. -->
         <xsl:call-template name="aggregate-func">
          <xsl:with-param name="a" select="$first"/>     
          <xsl:with-param name="b" select="$rest"/>     
         </xsl:call-template>
       </xsl:when>
       <!-- Here IDENTITY-VALUE should be replaced with the identity
            under the aggragate-func. For example, 0 is the identity
            for addition, 1 is the identity for subtarction, "" is the
            identity for concatentation, etc. -->
       <xsl:otherwise>IDENTITY-VALUE</xsl:otherwise>
</xsl:template>

Discussion

XSLT’s natural tendency is to process nodes in document order. This is equivalent to saying that nodes are processed in order of their position. Thus, the following two XSLT fragments are equivalent (the sort is redundant):

<xsl:for-each select="*">
     <xsl:sort select="position(  )"/>
     <!-- ... -->
</xsl:for-each>
   
<xsl:for-each select="*">
     <!-- ... -->
</xsl:for-each>

You can format our organizations chart into a two-column report using a variation of this idea, shown in Example 4-30 and Example 4-31.

Example 4-30. 2-columns-orgchat.xslt stylesheet

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:output method="text" />
<xsl:strip-space elements="*"/>
   
<xsl:template match="employee[employee]">
<xsl:value-of select="@name"/>
<xsl:text>&#xA;</xsl:text>
<xsl:call-template name="dup">
     <xsl:with-param name="input" select=" '-' "/>
     <xsl:with-param name="count" select="80"/>
</xsl:call-template>
<xsl:text>&#xA;</xsl:text>
<xsl:for-each select="employee[(position(  ) - 1) mod 2 = 0]">
     <xsl:value-of select="@name"/>
     <xsl:call-template name="dup">
          <xsl:with-param name="input" select=" ' ' "/>
          <xsl:with-param name="count" select="40 - string-length(@name)"/>
     </xsl:call-template>
     <xsl:value-of select="following-sibling::*[1]/@name"/>
     <xsl:text>&#xA;</xsl:text>
</xsl:for-each>
<xsl:text>&#xA;</xsl:text>
<xsl:apply-templates/>
</xsl:template>
   
<xsl:template name="dup">
<xsl:param name="input"/>
<xsl:param name="count" select="1"/>
<xsl:choose>
     <xsl:when test="not($count) or not($input)"/>
     <xsl:when test="$count = 1">
          <xsl:value-of select="$input"/>
     </xsl:when>
     <xsl:otherwise>
          <xsl:if test="$count mod 2">
               <xsl:value-of select="$input"/>
          </xsl:if>
          <xsl:call-template name="dup">
               <xsl:with-param name="input" 
                    select="concat($input,$input)"/>
               <xsl:with-param name="count" 
                    select="floor($count div 2)"/>
          </xsl:call-template>     
     </xsl:otherwise>
</xsl:choose>
</xsl:template>
   
</xsl:stylesheet>

Example 4-31. Output

Jil Michel
------------------------------------------------------------
Nancy Pratt                   Jane Doe
Mike Rosenbaum                
   
Nancy Pratt
------------------------------------------------------------
Phill McKraken                Ima Little
   
Ima Little
------------------------------------------------------------
Betsy Ross                    
   
Jane Doe
------------------------------------------------------------
Walter H. Potter              Wendy B.K. McDonald
   
Wendy B.K. McDonald
------------------------------------------------------------
Craig F. Frye                 Hardy Hamburg
Rich Shaker                   
   
Mike Rosenbaum
------------------------------------------------------------
Cindy Post-Kellog             Oscar A. Winner
   
Cindy Post-Kellog
------------------------------------------------------------
Allen Bran                    Frank N. Berry
Jack Apple                    
   
Oscar A. Winner
------------------------------------------------------------
Jack Nickolas                 Tom Hanks
Susan Sarandon                
   
Jack Nickolas
------------------------------------------------------------
R.P. McMurphy                 
   
Tom Hanks
------------------------------------------------------------
Forest Gump                   Andrew Beckett
   
Susan Sarandon
------------------------------------------------------------
Helen Prejean

One example of recursive-aggregation is a stylesheet that computes the total commission paid to salespeople whose commission is a function of their total sales over all products, shown in Example 4-32 and Example 4-33.

Example 4-32. Total-commission.xslt stylesheet

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output method="text"/>
   
<xsl:template match="salesBySalesperson">
     <xsl:text>Total commision = </xsl:text>
     <xsl:call-template name="total-commision">
          <xsl:with-param name="salespeople" select="*"/>
     </xsl:call-template>
</xsl:template>
   
<!-- By default salespeople get 2% commsison and no base salary -->
<xsl:template match="salesperson" mode="commision">
     <xsl:value-of select="0.02 * sum(product/@totalSales)"/>
</xsl:template>
   
<!-- salespeople with seniority > 4 get $10000.00 base + 0.5% commsison -->
<xsl:template match="salesperson[@seniority > 4]" mode="commision" priority="1">
     <xsl:value-of select="10000.00 + 0.05 * sum(product/@totalSales)"/>
</xsl:template>
   
<!-- salespeople with seniority > 8 get (seniority * $2000.00) base + 0.8% commsison -->
<xsl:template match="salesperson[@seniority > 8]" mode="commision" priority="2">
     <xsl:value-of select="@seniority * 2000.00 + 0.08 * 
          sum(product/@totalSales)"/>
</xsl:template>
     
<xsl:template name="total-commision">
     <xsl:param name="salespeople"/>
     <xsl:choose>
       <xsl:when test="$salespeople">
         <xsl:variable name="first">
          <xsl:apply-templates select="$salespeople[1]" mode="commision"/>
         </xsl:variable>
         <xsl:variable name="rest">
          <xsl:call-template name="total-commision">
            <xsl:with-param name="salespeople" 
              select="$salespeople[position(  )!=1]"/>
          </xsl:call-template>
         </xsl:variable>
         <xsl:value-of select="$first + $rest"/>
       </xsl:when>
       <xsl:otherwise>0</xsl:otherwise>
     </xsl:choose>
</xsl:template>
   
</xsl:stylesheet>

Example 4-33. Output

Total commision = 471315

See Also

Michael Kay has a nice example of recursive-aggregation on page 535 of XSLT Programmers Reference (Wrox Press, 2001). He uses this example to compute the total area of various shapes in which the formula for area varies by the type of shape.

Jeni Tennison also provides examples of recursive-aggregation and alternative ways to perform similar types of processing in XSLT and XPath on the Edge ( M&T Books, 2001).

Get XSLT Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.