Previous Page Next Page

Recipe 5.8. 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 aggregation 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 aggregate-func. For example, 0 is the identity
            for addition, 1 is the identity for subtraction, "" is the
            identity for concatenation, 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 organization's chart into a two-column report using a variation of this idea, shown in Examples Example 5-15 and Example 5-16.

Example 5-17. 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 5-18. 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 Nicklaus                 Tom Hanks
Susan Sarandon                
   
Jack Nicklaus
------------------------------------------------------------
R.P. McMurphy                 
   
Tom Hanks
------------------------------------------------------------
Forrest 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 5-17 and Example 5-18.

Example 5-19. 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 commission = </xsl:text>
     <xsl:call-template name="total-commission">
          <xsl:with-param name="salespeople" select="*"/>
     </xsl:call-template>
</xsl:template>
   
<!-- By default salespeople get 2% commission and no base salary -->
<xsl:template match="salesperson" mode="commission">
     <xsl:value-of select="0.02 * sum(product/@totalSales)"/>
</xsl:template>
   
<!-- salespeople with seniority > 4 get $10000.00 base + 0.5% commission -->
<xsl:template match="salesperson[@seniority > 4]" mode="commission" 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% commission -->
<xsl:template match="salesperson[@seniority > 8]" mode="commission" priority="2">
     <xsl:value-of select="@seniority * 2000.00 + 0.08 * 
          sum(product/@totalSales)"/>
</xsl:template>
     
<xsl:template name="total-commission">
     <xsl:param name="salespeople"/>
     <xsl:choose>
       <xsl:when test="$salespeople">
         <xsl:variable name="first">
          <xsl:apply-templates select="$salespeople[1]" mode="commission"/>
         </xsl:variable>
         <xsl:variable name="rest">
          <xsl:call-template name="total-commission">
            <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 5-20. Output
Total commission = 471315

XSLT 2.0

When using 2.0, one can combine functions and templates to avoid recursion but still exploit the pattern-matching capabilities of templates for computing the commissions:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                xmlns:ckbk="http://www.oreilly.com/catalog/xsltckbk">

<xsl:output method="text"/>
   
<xsl:template match="salesBySalesperson">
  <xsl:text>Total commission = </xsl:text>
  <xsl:value-of select="ckbk:total-commission(*)"/>
</xsl:template>
   
<!-- By default salespeople get 2% commission and no base salary -->
<xsl:template match="salesperson" mode="commission" as="xs:double">
  <xsl:sequence select="0.02 * sum(product/@totalSales)"/>
</xsl:template>
   
<!-- salespeople with seniority > 4 get $10000.00 base + 0.5% commission -->
<xsl:template match="salesperson[@seniority > 4]" mode="commission" 
              priority="1" as="xs:double">
  <xsl:sequence select="10000.00 + 0.05 * sum(product/@totalSales)"/>
</xsl:template>
   
<!-- salespeople with seniority > 8 get (seniority * $2000.00) base + 0.8% commission -->
<xsl:template match="salesperson[@seniority > 8]" mode="commission" 
              priority="2" as="xs:double">
  <xsl:sequence select="@seniority * 2000.00 + 0.08 * 
                        sum(product/@totalSales)"/>
</xsl:template>
     
<xsl:function name="ckbk:total-commission" as="xs:double">
  <xsl:param name="salespeople" as="node( )*"/>
  <xsl:sequence select="sum(for $s in $salespeople return ckbk:commission($s))"/>
</xsl:function>
   
<xsl:function name="ckbk:commission" as="xs:double">
  <xsl:param name="salesperson" as="node( )"/>
  <xsl:apply-templates select="$salesperson" mode="commission"/>
</xsl:function>

See Also

Michael Kay has a nice example of recursive-aggregation on page 535 of XSLT Programmer's 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).


Previous Page Next Page