Recipe 5.8. Processing Nodes by PositionProblemYou want to process nodes in a sequence that is a function of their position in a document or node set. SolutionUse 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 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> DiscussionXSLT'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>
</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 5-18. OutputJil 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. OutputTotal commission = 471315 XSLT 2.0When 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 AlsoMichael 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). |