You want to process nodes in a sequence that is a function of their position in a document or node set.
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( ) < 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> <!-- HereIDENTITY-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>
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>
</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>
</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>
</xsl:text> </xsl:for-each> <xsl:text>
</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>
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.