Recipe5.8.Processing Nodes by Position


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




XSLT Cookbook
XSLT Cookbook: Solutions and Examples for XML and XSLT Developers, 2nd Edition
ISBN: 0596009747
EAN: 2147483647
Year: 2003
Pages: 208
Authors: Sal Mangano

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net