Chapter 5. Selecting and Traversing

I choose a block of marble and chop off whatever I don’t need.

Francois-Auguste Rodin

I have [traversed] the length and breadth of this country and talked with the best people, and I can assure you that data processing is a fad that won’t last out the year.

The editor in charge of business books for a major publishing company, 1957

Introduction

Doing anything remotely interesting in XSLT involves two related operations: determining which XML nodes to visit (selecting) and determining in what order you want to visit them (traversing). Selecting is largely in the domain of XPath, a separate specification but one intimately related to XSLT. Traversing is a function of built-in control structures of XSLT and how you organize your templates to harness them.

XSLT veterans are unlikely to find much revelation in this particular chapter. Nevertheless, it is important for two reasons. First, the ideas presented in these recipes distinguish XSLT from other programming languages and therefore tend to be things that trip up novices on their first attempts to master XSLT. Second, the examples covered in this section are the primitive building blocks of many more complex recipes covered in later chapters. Virtually everything one does in XSLT involves application of selection and traversal. By analogy to cooking, knowing how to make a good brown stock is a prerequisite to making a sauce espagnole![1] This chapter is comprised of examples of the “brown stock” variety and should be mastered before proceeding to more advanced applications of XSLT.

Although this chapter presents some primitive examples, it is not an XPath or XSLT tutorial. The reader should know the basics of XPath covered in Chapter 1. I also assume you know what the default XSLT processing rules are and how XSLT determines what templates should be processed next. If you feel you need some help in these areas, I would recommend either Michael Kay’s XSLT Programmer’s Reference (Wrox Press, 2004) or Evan Lenz’ XSLT 1.0 Pocket Reference (O’Reilly, 2005). You will not see as much distinction between XSLT 1.0 and 2.0 here as in previous chapters. This is due to the fact that XSLT 1.0 was fairly complete in its ability to express most of these operations. The exception is in cases where the selection is based on constructing groups of nodes that are related by criteria other than XML’s hierarchical organization. Here, the new xsl:for-each-group instruction is a highly welcome addition.

Several examples in this section use computer-science terminology commonly associated with algorithms involving tree-data structures. The uninitiated may wonder what trees have to do with XML. The answer is that XML can be viewed as a language for specifying trees and XSLT as a language that processes these trees. In fact, it is possible to make an XSLT processor process non-XML input by making the processor think it is a tree using a SAX driver. Michael Kay demonstrates this technique in XSLT Programmer’s Reference (Wrox, 2001), as does Eric M. Burke in Java and XSLT (O’Reilly, 2001). In any case, the important concept that motivates the examples in this chapter is that thinking in terms of trees allows one to discover several useful XML processing techniques.

The examples provided in the chapter draw heavily on the following two test documents. To avoid repeating them in each recipe, I list them in Example 5-1 and Example 5-2.

Example 5-1. SalesBySalesPerson.xml
<?xml version="1.0" encoding="UTF-8"?>
<salesBySalesperson>
  <salesperson name="John Adams" seniority="1">
     <product sku="10000" totalSales="10000.00"/>
     <product sku="20000" totalSales="50000.00"/>
     <product sku="25000" totalSales="920000.00"/>
  </salesperson>
  <salesperson name="Wendy Long" seniority="5">
     <product sku="10000" totalSales="990000.00"/>
     <product sku="20000" totalSales="150000.00"/>
     <product sku="30000" totalSales="5500.00"/>
  </salesperson>
  <salesperson name="Willie B. Aggressive" seniority="10">
     <product sku="10000" totalSales="1110000.00"/>
     <product sku="20000" totalSales="150000.00"/>
     <product sku="25000" totalSales="2920000.00"/>
     <product sku="30000" totalSales="115500.00"/>
     <product sku="70000" totalSales="10000.00"/>
  </salesperson>
  <salesperson name="Arty Outtolunch" seniority="10"/>
</salesBySalesperson>
Example 5-2. orgchart.xml
<?xml version="1.0" encoding="UTF-8"?>
<employee name="Jil Michel" sex="female">
     <employee name="Nancy Pratt" sex="female">
          <employee name="Phill McKraken" sex="male"/>
          <employee name="Ima Little" sex="female">
               <employee name="Betsy Ross" sex="female"/>
          </employee>
     </employee>
     <employee name="Jane Doe" sex="female">
          <employee name="Walter H. Potter" sex="male"/>
          <employee name="Wendy B.K. McDonald" sex="female">
               <employee name="Craig F. Frye" sex="male"/>
               <employee name="Hardy Hamburg" sex="male"/>
               <employee name="Rich Shaker" sex="male"/>
          </employee>
     </employee>
     <employee name="Mike Rosenbaum" sex="male">
          <employee name="Cindy Post-Kellog" sex="female">
               <employee name="Allen Bran" sex="male"/>
               <employee name="Frank N. Berry" sex="male"/>
               <employee name="Jack Apple" sex="male"/>
          </employee>
          <employee name="Oscar A. Winner" sex="male">
               <employee name="Jack Nicklaus" sex="male">
                    <employee name="R.P. McMurphy" sex="male"/>
               </employee>
               <employee name="Tom Hanks" sex="male">
                    <employee name="Forrest Gump" sex="male"/>
                    <employee name="Andrew Beckett" sex="male"/>
               </employee>
               <employee name="Susan Sarandon" sex="female">
                    <employee name="Helen Prejean" sex="female"/>
               </employee>
          </employee>
     </employee>
</employee>

5.1. Ignoring Duplicate Elements

Problem

You want to select all nodes that are unique in a given context based on uniqueness criteria.

Solution

XSLT 1.0

Selecting unique nodes is a common application of the preceding and preceding-sibling axes. If the elements you select are not all siblings, then use preceding. The following code produces a unique list of products from SalesBySalesperson.xml:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
   
<xsl:template match="/">

<products>
     <xsl:for-each select="//product[not(@sku=preceding::product/@sku)]">
          <xsl:copy-of select="."/>
     </xsl:for-each>
</products>
</xsl:template>     
   
</xsl:stylesheet>

If the elements are all siblings, then use preceding-sibling:

<products>
     <product sku="10000" totalSales="10000.00"/>
     <product sku="10000" totalSales="990000.00"/>
     <product sku="10000" totalSales="1110000.00"/>
     <product sku="20000" totalSales="50000.00"/>
     <product sku="20000" totalSales="150000.00"/>
     <product sku="20000" totalSales="150000.00"/>
     <product sku="25000" totalSales="920000.00"/>
     <product sku="25000" totalSales="2920000.00"/>
     <product sku="30000" totalSales="5500.00"/>
     <product sku="30000" totalSales="115500.00"/>
     <product sku="70000" totalSales="10000.00"/>
</products>
   
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
   
<xsl:template match="/products ">
<products>
     <xsl:for-each select="product[not(@sku=preceding-sibling::product/@sku)]">
          <xsl:copy-of select="."/>
     </xsl:for-each>
</products>
</xsl:template>     
   
</xsl:stylesheet>

To avoid preceding, which can be inefficient, travel up to the ancestors that are siblings, and then use preceding-sibling and travel down to the nodes you want to test:

<xsl:for-each select="//product[not(@sku=../preceding-sibling::*/product/@sku)]">
     <xsl:copy-of select="."/>
</xsl:for-each>

If you are certain that the elements are sorted so that duplicate nodes are adjacent (as in the earlier products), then you only have to consider the immediately preceding sibling:

<xsl:for-each 
     select="/salesperson/product[not(@name=preceding-sibling::product[1]/@name]">
     <!-- do something with each uniquely named product -->
</xsl:for-each>

XSLT 2.0

You can solve this problem in XSLT 2.0 as a grouping problem. Simply use for-each-group with the uniqueness criteria as the group-by value. Use the first node in the current group as the unique one:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  
  <xsl:template match="/">
    <products>
      <xsl:for-each-group select="//product" group-by="@sku">
        <xsl:copy-of select="current-group()[1]"/>
      </xsl:for-each-group>
    </products>
  </xsl:template>
  
</xsl:stylesheet>

Discussion

XSLT 1.0

Using the node-set() extension function, you can also do the following:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exslt=" http://exslt.org">

     <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
   
<xsl:template match="/">
     
<xsl:variable name="products">
     <xsl:for-each select="//product">
          <xsl:sort select="@sku"/>
          <xsl:copy-of select="."/>
     </xsl:for-each>
</xsl:variable>
     
<products>
     <xsl:for-each select="exslt:node-set($products)/product">
          <xsl:variable name="pos" select="position()"/>
          <xsl:if test="$pos = 1 or 
          not(@sku = $products/preceding-sibling::product[1]/@sku">
               <xsl:copy-of select="."/>
          </xsl:if>
     </xsl:for-each>
</products>
     
</xsl:template>

However, I have never found this technique to be faster than using the preceding axis. This technique does have an advantage in situations where the duplicate testing is not trivial. For example, consider a case where duplicates are determined by the concatenation of two attributes:

<xsl:stylesheet version="1.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:exslt=" http://exslt.org">

     <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
   
<xsl:template match="/">
     
<xsl:variable name="people">
     <xsl:for-each select="//person">
          <xsl:sort select="concat(@lastname,@firstname)"/>
          <xsl:copy-of select="."/>
     </xsl:for-each>
</xsl:variable>
     
<products>
     <xsl:for-each select="exslt:node-set($people/)person">
          <xsl:variable name="pos" select="position()"/>
          <xsl:if test="$pos = 1 or 
               concat(@lastname,@firstname) != 
                          concat(people/person[$pos - 1]/@lastname,
                                 people/person[$pos - 1]/@firstname)">
               <xsl:copy-of select="."/>
          </xsl:if>
     </xsl:for-each>
</products>
     
</xsl:template>

When you attempt to remove duplicates, the following examples do not work:

<xsl:template match="/">
<products>
     <xsl:for-each select="//product[not(@sku=preceding::product[1]/@sku)]">
          
<xsl:sort select="@sku"/>

          <xsl:copy-of select="."/>
     </xsl:for-each>
</products>
</xsl:template>

Do not sort to avoid considering all but the immediately preceding element. The axis is relative to the node’s original order in the document. The same situation applies when using preceding-sibling. The following code is also sure to fail:

<xsl:template match="/">
     
<xsl:variable name="products">
     <xsl:for-each select="//product">
     
<!-- sort removed from here -->

          <xsl:copy-of select="."/>
     </xsl:for-each>
</xsl:variable>
     
<products>
     <xsl:for-each select="exsl:node-set($products)/product">
          
<xsl:sort select="@sku"/>

          <xsl:variable name="pos" select="position()"/>
          <xsl:if test="$pos = 1 or 
               @sku != $products/product[$pos - 1]/@sku">
               <xsl:copy-of select="."/>
          </xsl:if>
     </xsl:for-each>
</products>
</xsl:template>

This code fails because position() returns the position after sorting, but the contents of $products has not been sorted; instead, an inaccessible copy of it was.

XSLT 2.0

Sometimes you only want to remove duplicate elements when they are adjacent. Consider, for example, a data set derived from a series of measurements taken at set time intervals. If the system being measured is fairly stable, there may be many adjacent measurements that are equal. One may want to remove these adjacent duplicates without removing other equal measurements that appear later in the sequence.

For this problem, you would still use xsl:for-each-group but with group-adjacent rather than group-by:

<measurements>
  <data time="12:00:00" value="1.0"/>
  <data time="12:00:01" value="1.0"/>
  <data time="12:00:02" value="1.1"/>
  <data time="12:00:03" value="1.1"/>
  <data time="12:00:04" value="1.0"/>
  <data time="12:00:05" value="1.1"/>
  <data time="12:00:06" value="1.2"/>
  <data time="12:00:07" value="1.3"/>
  <data time="12:00:08" value="1.4"/>
  <data time="12:00:09" value="1.6"/>
  <data time="12:00:10" value="1.9"/>
  <data time="12:00:11" value="2.1"/>
  <data time="12:00:12" value="1.7"/>
  <data time="12:00:13" value="1.5"/>
</measurements>

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  
  <xsl:template match="/measurements">
    <xsl:copy>
      <xsl:for-each-group select="data" group-adjacent="@value">
        <xsl:copy-of select="current-group()[1]"/>
      </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>
  
</xsl:stylesheet>

<!--Output -->
<measurements>
   <data time="12:00:00" value="1.0"/>
   <data time="12:00:02" value="1.1"/>
   <data time="12:00:04" value="1.0"/>
   <data time="12:00:05" value="1.1"/>
   <data time="12:00:06" value="1.2"/>
   <data time="12:00:07" value="1.3"/>
   <data time="12:00:08" value="1.4"/>
   <data time="12:00:09" value="1.6"/>
   <data time="12:00:10" value="1.9"/>
   <data time="12:00:11" value="2.1"/>
   <data time="12:00:12" value="1.7"/>
   <data time="12:00:13" value="1.5"/>
</measurements>

See Also

The XSLT FAQ (http://www.dpawson.co.uk/xsl/sect2/N2696.html) describes a solution that uses keys and describes solutions to related problems.

5.2. Selecting All but a Specific Element

Problem

You want to select all elements in a specific context, except the ones you choose to exclude.

Solution

The best way to select all but a specific element is to say:

<xsl:apply-templates select="*[not(self::element-to-ignore)]"/>

or, if iterating, say:

<xsl:for-each select="*[not(self::element-to-ignore)]">
...
</xsl:for-each>

Discussion

When XSLT newbies first need to select all but a specific element, they will probably think of the following construct first:

<xsl:apply-templates select="*[name() != 'element-to-ignore']"/>

This code works in many cases, but it could cause trouble when the document uses namespaces. Recall that name() returns the node’s QName: the namespace prefix concatenated to the local part of the name. However, in any given XML document, nothing forces the author to use a specific prefix:

<!--This will fail if the author decided to use SALES:product instead of 
sales:product -->
<xsl:apply-templates select="*[name() != 'sales:product']"/>

Alternatively, you could use local-name(). However, this prefix would ignore elements from all namespaces that have that particular local name, which might not be what you want.

This recommendation applies only in the case of elements, not attributes. If you need to select all but a specific attribute, use local-name(). The self axis, when applied to a name, refers only to elements. In other words, use <xsl:copy-of select=@*[local-name() != 'ignored-attribute']/> and not <xsl:copy-of select=@*[not(self::ignored-attribute)]/>.

Finally, just in case of confusion, selecting all but a single element is different from selecting all but a single instance of element. The latter is used in an example discussed earlier in this chapter:

<xsl:apply-templates select="*[generate-id() != generate-id($node-to-ignore)]"/>

See Also

Jeni Tennison’s XSLT and XPath on the Edge (M&T Books, 2001) details when and when not to use name() and local-name().

5.3. Selecting Nodes by Context

Problem

You want to select nodes that are bracketed by preceding and following nodes.

Solution

XSLT 1.0

There are several ways to solve this problem in XSLT 1.0, but the easiest to understand computes the position of the nodes that should be selected at each step. If you had the following unstructured document and you desired to select the paragraphs that are bracketed by headings, you can use this technique:

<doc>
  <heading>Structure, I don't need any stinkin structure</heading>
  <para>1.1</para>
  <para>1.2</para>
  <para>1.3</para>
  <para>1.4</para>
  <heading>Confessions of a flat earther</heading>
  <para>2.1</para>
  <para>2.2</para>
  <para>2.3</para>
  <heading>Flat hierarchies save trees!</heading>
  <para>3.1</para>
  <para>3.2</para>
</doc>

 <xsl:template match="/doc">
    <xsl:copy>
      <!-- First select the bracketing elements -->
      <xsl:apply-templates select="heading"/>
    </xsl:copy>
 </xsl:template>
 
<!-- Match on the bracketing elements --> 
 <xsl:template match="heading">
  <!-- Compute how many of the desired elements (para) follow this heading -->
  <xsl:variable name="numFollowingPara" select="count(following-sibling::para)"/>
  
  <!-- Compute how many of the desired elements (para) follow the next heading 
       and subtract from the preceding count to get the position of the last
       para in this group-->
  <xsl:variable name="lastParaInHeading" 
      select="$numFollowingPara -
             count(following-sibling::heading[1]/following-sibling::para)"/>

    <!-- You now can select the desired elements by their position relative to 
         the current heading -->
  
    <xsl:apply-templates 
                  select="following-sibling::para[position() &lt;= $lastParaInHeading]"/>

  </xsl:template>

XSLT 2.0

This problem is tailor-made for the for-each-group instruction. Specifically, you would use the group-starting-with attribute:

<xsl:template match="/doc">
  <xsl:copy>
      <xsl:for-each-group select="*" group-starting-with="heading">
                  <!--Select the para elements in the group bracketed by heading -->
                  <xsl:apply-templates select="current-group()[self::para]"/> 
                  </xsl:for-each-group>             
  </xsl:copy>
</xsl:template>

Discussion

Selecting nodes based on their position relative to other nodes is a common requirement in document-oriented XML transformations where structure is implied rather than literally encoded into the hierarchy of the document. Clearly, if each group consisting of a heading and paragraphs was contained in a separate parent element (for example, a section element), then the problem would be trivial. This is a classic trade-off between ease of use for the document creators vs. ease of use for the document transformers. With the introduction of for-each-group in XSLT 2.0, the trade-off equals out since you can much more easily deal with unstructured documents.

See Also

Recipe 8.8 shows applications of this technique for transforming implicitly structured documents into explicitly structured ones. It also shows other ways the problem can be approached in XSLT 1.0 and 2.0.

5.4. Performing a Preorder Traversal

Problem

You want to recursively process an element first and then process its child elements.

Solution

Solutions to this recipe have the following general form:

<xsl:template match="node()">
     <!-- Do something with current node -->
   
     <!--Process children -->
     <xsl:apply-templates/>
</xsl:template>

Discussion

The term preorder is computer-science jargon for traversing a tree, so you visit the root and recursively visit the children of the root in preorder. This process is arguably the most common means of processing XML. Of this idiom’s many applications, this chapter will consider two. As you review recipes in later sections, you will see this mode of traversal arise frequently.

Consider an organization chart (Example 5-2) encoded in a simplistic fashion so that an employee element B is a child of another employee element A if B reports to A. Example 5-1 employs a preorder traversal to explain who manages whom. Example 5-2 shows the output.

Example 5-3. 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" priority="10">
     <xsl:value-of select="@name"/><xsl:text> is the head of the company. </xsl:text>
     <xsl:call-template name="HeShe"/><xsl:text> manages </xsl:text>
     <xsl:call-template name="manages"/>
     <xsl:apply-templates/>
</xsl:template>
     
<xsl:template match="employee[employee]">
     <xsl:value-of select="@name"/><xsl:text> is a manager. </xsl:text>
     <xsl:call-template name="HeShe"/> <xsl:text> manages </xsl:text>
     <xsl:call-template name="manages"/>
     <xsl:apply-templates/>
</xsl:template>
   
<xsl:template match="employee">
     <xsl:value-of select="@name"/><xsl:text> has no worries. </xsl:text>
     <xsl:text>&#xa;&#xa;</xsl:text>
</xsl:template>
   
<xsl:template name="HeShe">
     <xsl:choose>
          <xsl:when test="@sex = 'male' ">
               <xsl:text>He</xsl:text>
          </xsl:when>
          <xsl:otherwise>
               <xsl:text>She</xsl:text>
          </xsl:otherwise>
     </xsl:choose>
</xsl:template>
     
<xsl:template name="manages">
     <xsl:for-each select="*">
          <xsl:choose>
            <xsl:when test="position() != last() and last() > 2">
              <xsl:value-of select="@name"/><xsl:text>, </xsl:text>
            </xsl:when>
            <xsl:when test="position() = last() and last() > 1">
              <xsl:text> and </xsl:text><xsl:value-of 
                         select="@name"/><xsl:text>. </xsl:text>
            </xsl:when>
            <xsl:when test="last() = 1">
              <xsl:value-of select="@name"/><xsl:text>. </xsl:text>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="@name"/>
            </xsl:otherwise>
          </xsl:choose> 
     </xsl:for-each>
     <xsl:text>&#xa;&#xa;</xsl:text>
</xsl:template>
</xsl:stylesheet>
Example 5-4. Output
Jil Michel is the head of the company. She manages Nancy Pratt, Jane Doe, and Mike 
Rosenbaum. 
   
Nancy Pratt is a manager. She manages Phill McKraken and Ima Little. 
   
Phill McKraken has no worries. 
   
Ima Little is a manager. She manages Betsy Ross. 
   
Betsy Ross has no worries. 
   
Jane Doe is a manager. She manages Walter H. Potter and Wendy B.K. McDonald. 
   
Walter H. Potter has no worries. 
   
Wendy B.K. McDonald is a manager. She manages Craig F. Frye, Hardy Hamburg, and Rich 
Shaker. 
   
Craig F. Frye has no worries. 
   
Hardy Hamburg has no worries. 
   
Rich Shaker has no worries. 
   
Mike Rosenbaum is a manager. He manages Cindy Post-Kellog and Oscar A. Winner. 
   
Cindy Post-Kellog is a manager. She manages Allen Bran, Frank N. Berry, and Jack Apple. 
   
Allen Bran has no worries. 
   
Frank N. Berry has no worries. 
   
Jack Apple has no worries. 
   
Oscar A. Winner is a manager. He manages Jack Nickolas, Tom Hanks, and Susan Sarandon. 
   
Jack Nicklaus is a manager. He manages R.P. McMurphy. 
   
R.P. McMurphy has no worries. 
   
Tom Hanks is a manager. He manages Forrest Gump and Andrew Beckett. 
   
Forrest Gump has no worries. 
   
Andrew Beckett has no worries. 
   
Susan Sarandon is a manager. She manages Helen Prejean. 
   
Helen Prejean has no worries.

A more serious application of preorder traversal is the conversion of an expression tree to prefix notation. Given the MathML content fragment in Example 5-3 and Example 5-4, you can create a transformation to a Lisp-like syntax. Example 5-5 shows the output.

Example 5-5. MathML fragment representing x2+4x+4 = 0
<apply>
     <eq/>
     <apply>
          <plus/>
          <apply>
               <power/>
               <ci>x</ci>
               <cn>2</cn>
          </apply>
          <apply>
               <times/>
               <cn>4</cn>
               <ci>x</ci>
          </apply>
          <cn>4</cn>
     </apply>
     <cn>0</cn>
</apply>
Example 5-6. Stylesheet to convert MathML fragment to prefix notation
<?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="apply">
          <xsl:value-of select="local-name(*[1])"/>
          <xsl:text>(</xsl:text>
          <xsl:apply-templates/>
          <xsl:text>)</xsl:text>
          <xsl:if test="following-sibling::*">,</xsl:if>
     </xsl:template>
     
     <xsl:template match="ci|cn">
          <xsl:value-of select="."/>
          <xsl:if test="following-sibling::*">,</xsl:if>
     </xsl:template>
</xsl:stylesheet>
Example 5-7. Output
eq(plus(power(x,2),times(4,x),4),0)

The MathML converter is not the purest example of a preorder traversal, largely because MathML encodes mathematical expressions unconventionally. This is largely an instance of the preorder idiom because the code processes the apply element first (and outputs the name of its first child element) and then processes its child elements recursively (via <xsl:apply-templates/>). The code following apply-templates balances the parentheses, inserts commas where necessary, and involves no further traversal.

5.5. Performing a Postorder Traversal

Problem

You want to recursively process the children of an element first and then the element itself.

Solution

Solutions to this recipe have the following general form:

<xsl:template match="node()">
     <!--Process children -->
     <xsl:apply-templates/>
   
     <!-- Do something with current node -->
   
</xsl:template>

Discussion

The term postorder is computer-science jargon for traversing a tree so that you recursively visit the children of the root in postorder and then visit the root. This algorithm produces a stylesheet that processes the outermost leaf nodes and works its way up to the document root.

You can apply a postorder traversal to the organizational chart (orgchart.xml) to produce an explanation of who reports to whom, starting from the bottom, as shown in Example Example 5-6. Example Example 5-7 shows the output.

Example 5-8. Stylesheet
<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" priority="10">
     <xsl:apply-templates/>
     <xsl:value-of select="@name"/><xsl:text> is the head of the company. </xsl:text>
     <xsl:call-template name="reportsTo"/>
     <xsl:call-template name="HimHer"/> <xsl:text>. </xsl:text>
     <xsl:text>&#xa;&#xa;</xsl:text>
</xsl:template>
     
<xsl:template match="employee[employee]">
     <xsl:apply-templates/>
     <xsl:value-of select="@name"/><xsl:text> is a manager. </xsl:text>
     <xsl:call-template name="reportsTo"/>
     <xsl:call-template name="HimHer"/> <xsl:text>. </xsl:text>
     <xsl:text>&#xa;&#xa;</xsl:text>
</xsl:template>
   
<xsl:template match="employee">
     <xsl:text>Nobody reports to </xsl:text>
     <xsl:value-of select="@name"/><xsl:text>. &#xa;</xsl:text>
</xsl:template>
   
<xsl:template name="HimHer">
     <xsl:choose>
       <xsl:when test="@sex = 'male' ">
         <xsl:text>him</xsl:text>
       </xsl:when>
       <xsl:otherwise>
         <xsl:text>her</xsl:text>
       </xsl:otherwise>
     </xsl:choose>
</xsl:template>
     
<xsl:template name="reportsTo">
     <xsl:for-each select="*">
       <xsl:choose>
         <xsl:when test="position() &lt; last() - 1 and last() > 2">
          <xsl:value-of select="@name"/><xsl:text>, </xsl:text>
         </xsl:when>
         <xsl:when test="position() = last() - 1  and last() > 1">
          <xsl:value-of select="@name"/><xsl:text> and </xsl:text>
         </xsl:when>
         <xsl:when test="position() = last() and last() = 1">
          <xsl:value-of select="@name"/><xsl:text> reports to </xsl:text>
         </xsl:when>
         <xsl:when test="position() = last()">
          <xsl:value-of select="@name"/><xsl:text> report to </xsl:text>
         </xsl:when>
         <xsl:otherwise>
          <xsl:value-of select="@name"/>
         </xsl:otherwise>
       </xsl:choose> 
     </xsl:for-each>
</xsl:template>
          
</xsl:stylesheet>
Example 5-9. Output
Nobody reports to Phill McKraken. 
Nobody reports to Betsy Ross. 
Ima Little is a manager. Betsy Ross reports to her. 
   
Nancy Pratt is a manager. Phill McKraken and Ima Little report to her. 
   
Nobody reports to Walter H. Potter. 
Nobody reports to Craig F. Frye. 
Nobody reports to Hardy Hamburg. 
Nobody reports to Rich Shaker. 
Wendy B.K. McDonald is a manager. Craig F. Frye, Hardy Hamburg, and Rich Shaker report to her.
   
Jane Doe is a manager. Walter H. Potter and Wendy B.K. McDonald report to her. 
   
Nobody reports to Allen Bran. 
Nobody reports to Frank N. Berry. 
Nobody reports to Jack Apple. 
Cindy Post-Kellog is a manager. Allen Bran, Frank N. Berry, and Jack Apple report to her. 
   
Nobody reports to R.P. McMurphy. 
Jack Nicklaus is a manager. R.P. McMurphy reports to him. 
   
Nobody reports to Forrest Gump. 
Nobody reports to Andrew Beckett. 
Tom Hanks is a manager. Forrest Gump and Andrew Beckett report to him. 
   
Nobody reports to Helen Prejean. 
Susan Sarandon is a manager. Helen Prejean reports to her. 
   
Oscar A. Winner is a manager. Jack Nickolas, Tom Hanks, and Susan Sarandon report to him. 
   
Mike Rosenbaum is a manager. Cindy Post-Kellog and Oscar A. Winner report to him. 
   
Jil Michel is the head of the company. Nancy Pratt, Jane Doe,
 and Mike Rosenbaum report to her.

5.6. Performing an In-Order Traversal

Problem

You have an XML document or fragment that represents an expression to be processed in-order.

Solution

An in-order traversal is most applicable to a binary tree. The general form of the algorithm in this case follows:

<xsl:template match="node()">
     <!--Process left subtree -->
     <xsl:apply-templates select="*[1]"/>
     
     <!-- Do something with current node -->
     
     <!--Process right subtree --> 
     <xsl:apply-templates select="*[2]"/>
</xsl:template>

However, in-order traversal can extend to n-ary trees with the following algorithm:

<xsl:template match="node()">
     <xsl:variable name="current-node" select="."/>
     <!--Process left subtree -->
     <xsl:apply-templates select="*[1]"/>
     
     <!-- Do something with $current-node -->
   
     <!-- Apply recursively to middle children     
     <xsl:for-each select="*[position() > 1 and position() &lt; last()">
          
          <!-- Process "left" subtree --> 
          <xsl:apply-templates select="."/>
   
          <!--Do something with $current-node -->
   
     </xsl:for-each>
   
     <!--Process right subtree -->
     <xsl:apply-templates select="*[last()]"/>
   
</xsl:template>

The rationale behind this algorithm can be better understood by considering Figure 5-1, which shows the binary equivalent of an n-ary tree. The generalized n-ary in-order traversal produces the same result as the binary in-order traversal on the binary equivalent tree.

N-ary to binary tree equivalent
Figure 5-1. N-ary to binary tree equivalent

Discussion

This form of traversal has a much narrower range of applicability then other traversal examples in this chapter. One notable application, shown in Example 5-8 and Example 5-9, is as a component of a stylesheet that converts MathML markup to C or Java-style infix expressions. Example 5-10 shows the output.

Example 5-10. Input MathML fragment
<apply>
     <eq/>
     <apply>
          <plus/>
          <apply>
               <minus/>
               <ci>y</ci>
               <cn>2</cn>
          </apply>
          <apply>
               <times/>
               <cn>4</cn>
               <apply>
                    <plus/>
                    <ci>x</ci>
                    <cn>1</cn>
               </apply>
          </apply>
          <cn>8</cn>
     </apply>
     <cn>0</cn>
</apply>
Example 5-11. In-order traversal of MathML fragment to produce a C expression
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:C="http://www.ora.com/XSLTCookbook/nampespaces/C">

     <xsl:output method="text"/>
     <xsl:strip-space elements="*"/>
   
     <!-- Table to convert from MathML operation names to C operators -->
     <C:operator mathML="plus" c="+" precedence="2"/>
     <C:operator mathML="minus" c="-" precedence="2"/>
     <C:operator mathML="times" c="*" precedence="3"/>
     <C:operator mathML="div" c="/" precedence="3"/>
     <C:operator mathML="mod" c="%" precedence="3"/>
     <C:operator mathML="eq" c="=  =" precedence="1"/>
     
     <!-- load operation conversion table into a variable -->               
     <xsl:variable name="ops" select="document('')/*/C:operator"/>
          
     <xsl:template match="apply"> 
          <xsl:param name="parent-precedence" select="0"/>
          
          <!-- Map mathML operation to operator name and precedence -->
          <xsl:variable name="mathML-opName" select="local-name(*[1])"/>
          <xsl:variable name="c-opName" 
               select="$ops[@mathML=$mathML-opName]/@c"/>
          <xsl:variable name="c-opPrecedence"
               select="$ops[@mathML=$mathML-opName]/@precedence"/>
          
          <!-- Parenthesis required if the precedence of the containing
           expression is greater than current sub-expression -->
          <xsl:if test="$parent-precedence > $c-opPrecedence">
          <xsl:text>(</xsl:text>
          </xsl:if>
   
          <!-- Recursively process the left sub-tree which is at 
               position 2 in MathML apply element-->          
          <xsl:apply-templates select="*[2]">
               <xsl:with-param name="parent-precedence" 
                    select="$c-opPrecedence"/>
          </xsl:apply-templates>
          
          <!-- Process the current node (i.e. the operator at 
               position 1 in MathML apply element -->
          <xsl:value-of select="concat(' ',$c-opName,' ')"/>
          
          <!-- Recursively process middle children -->
          <xsl:for-each select="*[position()>2 and 
                              position() &lt; last()]">
               <xsl:apply-templates select=".">
                    <xsl:with-param name="parent-precedence"
                         select="$c-opPrecedence"/>
               </xsl:apply-templates>
               <xsl:value-of select="concat(' ',$c-opName,' ')"/>
          </xsl:for-each>
          
          <!-- Recursively process right subtree-->
          <xsl:apply-templates select="*[last()]">
               <xsl:with-param name="parent-precedence" 
                         select="$c-opPrecedence"/>
          </xsl:apply-templates>
   
          <!-- Parenthesis required if the precedence of the containing
               expression is greater than current sub-expression -->
          <xsl:if test="$parent-precedence > $c-opPrecedence">
               <xsl:text>)</xsl:text>
          </xsl:if>
          
     </xsl:template>     
   
     <xsl:template match="ci|cn">
          <xsl:value-of select="."/>
     </xsl:template>
   
</xsl:stylesheet>
Example 5-12. Output
y - 2 + 4 * (x + 1) + 8 =  = 0

Obviously, this stylesheet is not a full-fledged MathML-to-C translator. However, Chapter 9 discusses this problem more thoroughly.

5.7. Performing a Level-Order Traversal

Problem

You want to order elements by increasing level (tree depth). In other words, you want to traverse the tree breadth first.

Solution

This form of traversal is tailor-made for using xsl:for-each along with xsl:sort:

<xsl:for-each select="//*">
     <xsl:sort select="count(ancestor::*)" data-type="number"/>
     <!--  process the current element -->
</xsl:for-each>

This recursive solution is longer and less obvious:

<xsl:template match="/*">
     <xsl:call-template name="level-order"/>
</xsl:template>
   
<xsl:template name="level-order">
<xsl:param name="max-level" select="10"/>
<xsl:param name="current-level" select="1"/>
   
<xsl:choose>
     <xsl:when test="$current-depth &lt;= $max-level">
          <!-- process the current level -->
          <xsl:call-template name="level-order-aux">
               <xsl:with-param name="level" 
                         select="$current-level"/>
               <xsl:with-param name="actual-level" 
                         select="$current-level"/>
          </xsl:call-template>
          <!-- process the next level -->
          <xsl:call-template name="level-order">
               <xsl:with-param name="current-level" 
                         select="$current-level + 1"/>
          </xsl:call-template>
     </xsl:when>
</xsl:choose>
   
</xsl:template>
   
<xsl:template name="level-order-aux">
     <xsl:param name="level" select="1"/>
     <xsl:param name="actual-level" select="1"/>
     <xsl:choose>
          <xsl:when test="$level = 1">
               <!-- Process the current element here -->
               <!-- $actual-level is the number of the current level -->
          </xsl:when>
          <xsl:otherwise>
               <!-- Recursively descend to the next level on 
                    all children -->
               <xsl:for-each select="*">
                    <xsl:call-template name="level-order-aux">
                         <xsl:with-param name="level" 
                              select="$level - 1"/>
                         <xsl:with-param name="actual-level"
                              select="$actual-level"/>
                    </xsl:call-template>
               </xsl:for-each>
          </xsl:otherwise>
     </xsl:choose>
</xsl:template>

This solution requires that you either set an arbitrary bound for the maximum depth of the tree or compute the maximum depth. One way of doing so is by finding the node that has no children and more ancestors than any other node without children:

<xsl:param name="max-level">
  <xsl:for-each select="//*[not(*)]">
    <xsl:sort select="count(ancestor::*)" data-type="number" order="descending" />
    <xsl:if test="position() = 1">
      <xsl:value-of select="count(ancestor::*) + 1" />
    </xsl:if>
  </xsl:for-each>
</xsl:param>

Discussion

Arguably, the need to traverse an XML document by level does not arise often. Grouping nodes by depth or distance from the root does not fit into XSLT’s natural control flow, which is organized to express traversals that descend the document tree. This is apparent by the fact that that you had to use xsl:sort to coerce the stylesheet to process nodes by their depth or, equivalently, the number of ancestors. The complexity of the recursive solution provides further evidence of this unnatural process.

Still, this form of traversal is sometimes handy. For example, you can use it to process an organizational chart that lists employees by their distance from the top spot, as shown in Example 5-11. The output is shown in Example 5-12.

Example 5-13. Level-order traversal of orgrchart.xml using sort
<?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:template match="/">
  <xsl:for-each select="//employee">
    <xsl:sort select="count(ancestor::*)" order="ascending"/>
    <xsl:variable name="level" select="count(ancestor::*)"/>
    <xsl:choose>
      <xsl:when test="$level = 0">
        <xsl:value-of select="@name"/>
        <xsl:text> is the head honcho.&#xA;</xsl:text>
      </xsl:when>
       <xsl:otherwise>
         <xsl:value-of select="@name"/>
       <xsl:text> has </xsl:text>
       <xsl:value-of select="$level"/>
       <xsl:text> boss(es) to knock off.&#xA;</xsl:text>
     </xsl:otherwise>
    </xsl:choose>
  </xsl:for-each>
</xsl:template>
   
</xsl:stylesheet>
Example 5-14. Output
Jil Michel is the head honcho.
Nancy Pratt has 1 boss(es) to knock off.
Jane Doe has 1 boss(es) to knock off.
Mike Rosenbaum has 1 boss(es) to knock off.
Phill McKraken has 2 boss(es) to knock off.
Ima Little has 2 boss(es) to knock off.
Walter H. Potter has 2 boss(es) to knock off.
Wendy B.K. McDonald has 2 boss(es) to knock off.
Cindy Post-Kellog has 2 boss(es) to knock off.
Oscar A. Winner has 2 boss(es) to knock off.
Betsy Ross has 3 boss(es) to knock off.
Craig F. Frye has 3 boss(es) to knock off.
Hardy Hamburg has 3 boss(es) to knock off.
Rich Shaker has 3 boss(es) to knock off.
Allen Bran has 3 boss(es) to knock off.
Frank N. Berry has 3 boss(es) to knock off.
Jack Apple has 3 boss(es) to knock off.
Jack Nicklaus has 3 boss(es) to knock off.
Tom Hanks has 3 boss(es) to knock off.
Susan Sarandon has 3 boss(es) to knock off.
R.P. McMurphy has 4 boss(es) to knock off.
Forrest Gump has 4 boss(es) to knock off.
Andrew Beckett has 4 boss(es) to knock off.
Helen Prejean has 4 boss(es) to knock off.

One advantage of the recursive variation is that it is easy to tell when you transition from one level to the next. You could use this information to better format your output, as shown in Example 5-13 and Example 5-14.

Example 5-15. Level-order traversal of orgrchart.xml using recursion
<?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" version="1.0" encoding="UTF-8"/>
   
<xsl:strip-space elements="*"/>
     
<xsl:template match="/employee">
     <xsl:call-template name="level-order"/>
</xsl:template>
   
<xsl:template name="level-order">
<xsl:param name="max-depth" select="10"/>
<xsl:param name="current-depth" select="0"/>
   
<xsl:choose>
  <xsl:when test="$current-depth &lt;= $max-depth">
    <xsl:variable name="text">
      <xsl:call-template name="level-order-aux">
         <xsl:with-param name="level" select="$current-depth"/>
        <xsl:with-param name="actual-level" select="$current-depth"/>
      </xsl:call-template>
    </xsl:variable>
    <xsl:if test="normalize-space($text)">
      <xsl:value-of select="$text"/>
      <xsl:text>&#xa;</xsl:text>
      <xsl:call-template name="level-order">
        <xsl:with-param name="current-depth" select="$current-depth + 1"/>
        </xsl:call-template>
    </xsl:if>
  </xsl:when>
</xsl:choose>
   
</xsl:template>
   
<xsl:template name="level-order-aux">
  <xsl:param name="level" select="0"/>
  <xsl:param name="actual-level" select="0"/>
  <xsl:choose>
    <xsl:when test="$level = 0">
      <xsl:choose>
        <xsl:when test="$actual-level = 0">
      <xsl:value-of select="@name"/>
      <xsl:text> is the head honcho.&#xA;</xsl:text>
     </xsl:when>
     <xsl:otherwise>
       <xsl:value-of select="@name"/>
       <xsl:text> has </xsl:text>
       <xsl:value-of select="$actual-level"/>
       <xsl:text> boss(es) to knock off.&#xA;</xsl:text>
     </xsl:otherwise>
    </xsl:choose>
   </xsl:when>
   <xsl:otherwise>
      <xsl:for-each select="employee">
         <xsl:call-template name="level-order-aux">
           <xsl:with-param name="level" select="$level - 1"/>
           <xsl:with-param name="actual-level" select="$actual-level"/>
         </xsl:call-template>
      </xsl:for-each>
   </xsl:otherwise>
  </xsl:choose>
</xsl:template>
   
</xsl:stylesheet>
Example 5-16. Output
Jil Michel is the head honcho.
   
Nancy Pratt has 1 boss(es) to knock off.
Jane Doe has 1 boss(es) to knock off.
Mike Rosenbaum has 1 boss(es) to knock off.
   
Phill McKraken has 2 boss(es) to knock off.
Ima Little has 2 boss(es) to knock off.
Walter H. Potter has 2 boss(es) to knock off.
Wendy B.K. McDonald has 2 boss(es) to knock off.
Cindy Post-Kellog has 2 boss(es) to knock off.
Oscar A. Winner has 2 boss(es) to knock off.
   
Betsy Ross has 3 boss(es) to knock off.
Craig F. Frye has 3 boss(es) to knock off.
Hardy Hamburg has 3 boss(es) to knock off.
Rich Shaker has 3 boss(es) to knock off.
Allen Bran has 3 boss(es) to knock off.
Frank N. Berry has 3 boss(es) to knock off.
Jack Apple has 3 boss(es) to knock off.
Jack Nickolas has 3 boss(es) to knock off.
Tom Hanks has 3 boss(es) to knock off.
Susan Sarandon has 3 boss(es) to knock off.
   
R.P. McMurphy has 4 boss(es) to knock off.
Forrest Gump has 4 boss(es) to knock off.
Andrew Beckett has 4 boss(es) to knock off.
Helen Prejean has 4 boss(es) to knock off.

If, for some reason, you wished to have random access to nodes at any specific level, you could define a key as follows:

<xsl:key name="level" match="employee" use="count(ancestor::*)"/>
   
<xsl:template match="/">
     <xsl:for-each select="key('level',3)">
          <!-- do something with the nodes on level 3 --> 
     </xsl:for-each>
</xsl:template>

You can also make the match more specific or use a predicate with the key function:

<xsl:key name="level" match="//employee" use="count(ancestor::*)"/>
   
<xsl:template match="/">
     <xsl:for-each select="key('level',3)[@sex='female']">
          <!-- do something with the female employees on level 3 --> 
     </xsl:for-each>
</xsl:template>

The use of XSLT’s key facility is not mandatory because you can always write select="//*[count(ancestors::*)=3]" when you need access to level-3 elements; however, performance will suffer if your stylesheet repeatedly evaluates such an expression. In fact, if your stylesheet has a well-defined structure, you’d be much better off explicitly navigating to the desired level; e.g., select= "/employee/employee/employee“, although this navigation can become quite unwieldy.

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).



[1] Sauce espagnole is highly concentrated brown stock with tomatoes bound with roux and reduced. The brown stock is made from veal and beef shanks. Sauce espagnole is necessary for making demi-glace, the “mother sauce” for many other brown sauces. When it comes to “reuse,” the chefs have the software developers beat!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset