The brilliant moves we occasionally make would not have been possible without the prior dumb ones.
Stanley Goldstein
This chapter renders all previous chapters moot. Okay, maybe that is a slight exaggeration. The fact is that the examples in previous chapters solve the particular problems they address. They are also useful for didactic purposes. If an example does not solve a problem you face, it might point the way to a solution. The desired solution could be a small modification of the code or a reapplication of the same techniques.
This chapter sets its goals a little higher. It presents examples that solve a very broad range of problems without requiring customization of the example’s core code. Those of you who are familiar with C++, and specifically the Standard Template Library (STL), already know the power you can obtain by creating generic code (generic algorithms) and reusing them in various contexts. Others who use functional programming languages (e.g., Lisp, ML, or Haskell) also know of the great power obtained through the creation of higher-order functions: general-purpose functions that are specialized by accepting special purpose functions as arguments. This chapter shows that XSLT, although not specifically designed as a generic or functional language, has inherent capabilities to enable similar usage.
The techniques used in this chapter stretch the abilities of XSLT quite a bit. Not everyone will want to use the examples, some of which are complex and slow. Nevertheless, I am reminded of the days before C++ had native support for templates. You could fake generic programming by using macros, but the results were awkward. However, enough people saw the potential, and templates soon became a first-class feature in C++, and possibly one of C++’s most important characteristics, despite the proliferation of other OO languages. Pushing the language envelope in this way puts pressure on the language and possibly makes it evolve faster. This faster development is good because languages that cease to evolve often die out.
Before diving into the examples, let’s first discuss some of the general techniques used in this chapter. This will allow the examples to concentrate on the application of the techniques rather than their mechanics.
This chapter extensively uses
XSLT’s ability to
import (xsl:import
) and override templates,
variables, and other top-level elements in the importing spreadsheet.
I like to use the object-oriented term override
when discussing xsl:import
; however, a more
technically correct explanation notes that some top-level elements in
the importing stylesheet have higher import
precedence
than matching elements in the imported
stylesheet. You can find a complete explanation of how each XSLT
top-level element works with respect to xsl:import
in Michael Kay’s XSLT
Programmer’s Reference (Wrox, 2001).
This chapter takes advantage of the ability to combine a global variable’s contents defined in an imported stylesheet with one defined in an importing stylesheet.
The following stylesheet defines two variables. The first,
$data1-public-data
,
is unique to this stylesheet. The second, $data
,
is defined in terms of the first, but can be overridden:
<!-- data1.xslt --> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:d="http://www.ora.com/XSLTCookbook/NS/data"> <xsl:output method="xml" indent="yes"/> <d:data value="1"/> <d:data value="2"/> <d:data value="3"/> <xsl:variable name="data1-public-data" select="document('')/*/d:*"/> <xsl:variable name="data" select="$data1-public-data"/> <xsl:template match="/"> <demo> <xsl:copy-of select="$data"/> </demo> </xsl:template> </xsl:stylesheet>
Now define another stylesheet that extends the value
of
$data
. It too defines a unique variable that is
the union of the first stylesheet’s public data and
locally defined data. It then redefines $data
in
terms of this union:
<!-- data2.xslt --> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:d="http://www.ora.com/XSLTCookbook/NS/data"> <xsl:import href="data1.xslt"/> <xsl:output method="xml" indent="yes"/> <d:data value="5"/> <d:data value="7"/> <d:data value="11"/> <xsl:variable name="data2-public-data" select="document('')/*/d:* | $data1-public-data"/> <xsl:variable name="data" select="$data2-public-data"/> </xsl:stylesheet>
The output of data1.xslt
is:
<demo xmlns:d="data"> <d:data value="1"/> <d:data value="2"/> <d:data value="3"/> </demo>
The output of data2.xslt
is:
<demo xmlns:d="data"> <d:data value="1"/> <d:data value="2"/> <d:data value="3"/> <d:data value="5"/> <d:data value="7"/> <d:data value="11"/> </demo>
Defining the second stylesheet’s
$data
in terms of the first’s,
without the need for the extra variables, would be convenient;
however, XSLT treats this definition circularly. The technique
defines a named set that is operated on by templates in a core
stylesheet but allows importing stylesheets to expand the set. The
motivation for this will become more clear as
you proceed.
XSLT provides no direc t way to pass the name of a template to another template so that the second template can invoke the first indirectly. In other words, the following code is illegal in XSLT 1.0 and 2.0:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <!-- THIS IS NOT LEGAL XSLT --> <xsl:template match="/"> <!-- We can call templates by name ...--> <xsl:call-template name="sayIt"> <xsl:with-param name="aTempl" select=" 'sayHello' "/> </xsl:call-template> <xsl:call-template name="sayIt"> <xsl:with-param name="aTempl" select=" 'sayGoodbye' "/> </xsl:call-template> </xsl:template> <xsl:template name="sayIt"> <xsl:param name="aTempl"/> <!--But not when the name is indirectly specified with a variable --> <xsl:call-template name="{$aTemple}"/> </xsl:template> <xsl:template name="sayHello"> <xsl:value-of select=" 'Hello!' "/> </xsl:template> <xsl:template name="sayGoodbye"> <xsl:value-of select=" 'Goodbye!' "/> </xsl:template> </xsl:stylesheet>
As it turns out, you can create some powerful and reusable code if you can figure out how to achieve this level of indirection within the confines of XSLT. Fortunately, you can achieve this goal by using matching instead of naming. The trick is to define a template that can match only one particular piece of data. That piece of data is called a template tag, and by convention, you define the tag directly above the template it matches:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:f="http://www.ora.com/XSLTCookbook/namespaces/func"> <xsl:output method="text"/> <xsl:template match="/"> <xsl:call-template name="sayIt"> <xsl:with-param name="aTempl" select=" 'sayHello' "/> </xsl:call-template> <xsl:call-template name="sayIt"> <xsl:with-param name="aTempl" select=" 'sayGoodbye' "/> </xsl:call-template> </xsl:template> <xsl:template name="sayIt"> <xsl:param name="aTempl"/> <!--Apply templates by selecting a tag element that is unique to the template we want to invoke --> <xsl:apply-templates select="document('')/*/f:func[@name=$aTempl]"/> </xsl:template> <!-- A tagged template consists of a tag element and a template that matches that tagged element --> <f:func name="sayHello"/> <xsl:template match="f:func[@name='sayHello']"> <xsl:text>Hello!
</xsl:text> </xsl:template> <!-- Another tagged template --> <f:func name="sayGoodbye"/> <xsl:template match="f:func[@name='sayGoodbye']"> <xsl:text>Goodbye!
</xsl:text> </xsl:template> </xsl:stylesheet>
In this particular case, these contortions are pure overkill because you could simply create a template that takes the output string as data. The true power of this technique is only realized when the tagged functions compute something the caller can use.
When using this technique, use a sanity-checking template that will match when no tagged template matches:
<xsl:template match="f:func"> <xsl:message terminate="yes"> BAD FUNC! Template may not match generic:func declaration. </xsl:message> </xsl:template>
Mike Kay points out that another generic programming technique has the templates match themselves, as in:
<xsl:template name="f:sayHello" match="xsl:template[@name='f:sayHello']"> <xsl:text>Hello!
</xsl:text> </xsl:template> <xsl:template name="f:sayGoodbye" match="xsl:template[@name='f:sayGoodbye']"> <xsl:text>Goodbye!
</xsl:text> </xsl:template>
By using this technique, you can still call the template by name without any problems, and it still looks like a normal template. This chapter does not use this technique because we sometimes like to associate other data with the template tags and thus prefer them to be separate elements.
Dimitre Novatchev was the first person, to my knowledge, to discover techniques for generic and functional programming in XSLT. He wrote several articles on the topic. See http://fxsl.sourceforge.net. The generic programming recipes in this chapter were developed before I discovered Dimitre’s work and vary in some ways from Dimitre’s approach. I would recommend viewing Dimitre’s work only after you are comfortable with these examples. Dimitre pushes the edge of what can be done further than I do, and his techniques are thus somewhat more challenging. Dimitre has an XSLT library called FXSL - an XSLT functional programming library that can be downloaded from http://sourceforge.net/project/showfiles.php?group_id=53841.
Dimitre Novatchev has a version of FXSL that takes advantage of the new capabilities of XSLT 2.0 (http://sourceforge.net/project/showfiles.php?group_id=53841). Most notably, his library takes great advantage of 2.0’s functions and function overloading. I stongly recommend his library to XSLT 2.0 developers interested in “higher-order” programming in XSLT.
Here are some excerpts from Dimitre’s post regarding fxsl and higher order XSLT programming. Here, HOF stands for Higher Order Function.
The benefit should be obvious:
1. Composability:
f:map
can be in any place where we could have a functional composition chain.2. It is much simpler and more understandable to write:
f:map(fsomeFun(), $seq)
than
<xsl:for-each select="$seq">
<xsl:sequence select="f:apply(fsomeFun(), $seq)"/>
</xsl:for-each>
Having a HOF wrapper with the same name and signature as any one of all the F(unctions) & O(perators) provides essentially a functional programming system, where all standard XPath 2.0 F & Os (and all standard XSLT 2.0 functions) are higher order.
The programmer can immediately start using all standard XPath 2.0 F & Os (which he knows well) without having to do anything in addition. The necessary steep learning curve is almost avoided. This is wealth of functions that are already implemented and they have guaranteed support by any compliant XSLT 2.0 processor.
Another important result is a set of strict and simple rules how to write user-defined
xsl:function
’s that are implemented as higher order.Adhering to these rules makes XSLT 2.0 + FXSL really a closed (in relation of HOF support) functional programming system.
Yet another benefit is that all HOF functions could potentially be used in a non-XSLT environment (for example, if they are available in pre-compiled form), such as using embedded XPath 2.0 in a programming language, or why not in XQuery. Of course, in this approach any new HOF will have to be implemented in XSLT and precompiled before they can be referenced.
Trying to summarize all the benefits:
The FXSL + XSLT 2.0 functional programming system has already reached a state where the solutions to many very difficult problems can be simply expressed as one-liner XPath expressions.
This may represent a radical change in the way of reasoning and in how problems are solved in XSLT.
Here Dimitre demonstrates the ease of performing complex mathematical manipulations using FXSL.
The other, factual reason is that using XSLT for math computations is simple, compact, elegant, and fun.
For example, to get the sequence:
N^10, N = 1 to 10
one simply writes this one-line expression:
f:map(f:flip(f:pow(),10), 1 to 10)
To get the sum of the tenth powers of all numbers from 1 to 10, one writes this one-liner:
sum(f:map(f:flip(f:pow(),10), 1 to 10))
To get the tenth root of the above, this one-liner is used:
There are two kinds of polymorphic behavior in XSLT. The first form is reminiscent of overloading, and the second is similar to overriding.
Some modern languages, notably C++, let you create overloaded functions: functions that have the same name but take different types as arguments. The compiler figures out which version of the function to call based on the type of data passed to it at the point of call. XSLT does not have this exact capability; however, consider the following stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" /> <xsl:template match="/"> <html> <head> <title>Area of Shapes</title> </head> <body> <h1>Area of Shapes</h1> <table cellpadding="2" border="1"> <tbody> <tr> <th>Shape</th> <th>Shape Id</th> <th>Area</th> </tr> <xsl:apply-templates/> </tbody> </table> </body> </html> </xsl:template> <xsl:template match="shape"> <tr> <td><xsl:value-of select="@kind"/></td> <td><xsl:value-of select="@id"/></td> <xsl:variable name="area"> <xsl:apply-templates select="." mode="area"/> </xsl:variable> <td align="right"><xsl:value-of select="format-number($area,'#.000')"/></td> </tr> </xsl:template> <xsl:template match="shape[@kind='triangle']" mode="area"> <xsl:value-of select="@base * @height"/> </xsl:template> <xsl:template match="shape[@kind='square']" mode="area"> <xsl:value-of select="@side * @side"/> </xsl:template> <xsl:template match="shape[@kind='rectangle']" mode="area"> <xsl:value-of select="@width * @height"/> </xsl:template> <xsl:template match="shape[@kind='circle']" mode="area"> <xsl:value-of select="3.1415 * @radius * @radius"/> </xsl:template> </xsl:stylesheet>
Notice that several templates have an area
mode.
These templates differ in what types of shapes they accept and
compute a different function based on the particular shape. If you
equate mode
to function name
and the match pattern
to the data
type
, you will immediately see this technique as an
instance of polymorphic overloading.
Overriding is the second form of polymorphism. You can see numerous
examples of overriding in this book’s examples. In
XSLT, you use xsl:import
to achieve this form of
polymorphic behavior. The following example is a rewrite of the
DocBook stylesheet from Recipe 10.1. It was
engineered for extensibility in the following ways:
It uses variables to define primitive components of otherwise monolithic attribute content. The variables can be redefined when importing stylesheets.
It uses attribute sets, which can be augmented with additional attributes or have existing attributes overridden in importing stylesheets.
It uses simple templates for each section of the document that can be overridden in importing stylesheets.
It provides a hook via
a call to the named template
extra-head-meta-data
whose default implementation
does nothing.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html"/> <!-- Variables defining various style components --> <xsl:variable name="standard-font-family" select=" 'font-family: Times serif; font-weight' "/> <xsl:variable name="chapter-label-font-size" select=" 'font-size : 48pt' "/> <xsl:variable name="chapter-title-font-size" select=" 'font-size : 24pt' "/> <xsl:variable name="epigraph-font-size" select=" 'font-size : 10pt' "/> <xsl:variable name="sect1-font-size" select=" 'font-size : 18pt' "/> <xsl:variable name="sect2-font-size" select=" 'font-size : 14pt' "/> <xsl:variable name="normal-font-size" select=" 'font-size : 12pt' "/> <xsl:variable name="normal-text-color" select=" 'color: black' "/> <xsl:variable name="chapter-title-color" select=" 'color: red' "/> <xsl:variable name="epigraph-padding" select=" 'padding-top:4; padding-bottom:4' "/> <xsl:variable name="epigraph-common-style" select="concat($standard-font- family,'; ', $epigraph-font-size, '; ', $epigraph-padding, '; ',$normal-text- color)"/> <xsl:variable name="sect-common-style" select="concat($standard-font-family,'; font-weight: bold', '; ',$normal-text-color)"/> <!-- Attribute sets --> <xsl:attribute-set name="chapter-align"> <xsl:attribute name="align">right</xsl:attribute> </xsl:attribute-set> <xsl:attribute-set name="normal-align"> </xsl:attribute-set> <xsl:attribute-set name="chapter-label" use-attribute-sets="chapter-align"> <xsl:attribute name="style"> <xsl:value-of select="$standard-font-family"/>; <xsl:value-of select="$chapter-label-font-size"/>; <xsl:value-of select="$chapter-title-color"/> <xsl:text>; padding-bottom:10; font-weight: bold</xsl:text> </xsl:attribute> </xsl:attribute-set> <xsl:attribute-set name="chapter-title" use-attribute-sets="chapter-align"> <xsl:attribute name="style"> <xsl:value-of select="$standard-font-family"/>; <xsl:value-of select="$chapter-title-font-size"/>; <xsl:value-of select="$chapter-title-color"/> <xsl:text>; padding-bottom:150; font-weight: bold</xsl:text> </xsl:attribute> </xsl:attribute-set> <xsl:attribute-set name="epigraph-para" use-attribute-sets="chapter-align"> <xsl:attribute name="style"> <xsl:value-of select="$epigraph-common-style"/><xsl:text>; font-style: italic</xsl:text> </xsl:attribute> </xsl:attribute-set> <xsl:attribute-set name="epigraph-attribution" use-attribute-sets="chapter-align"> <xsl:attribute name="style"> <xsl:value-of select="$epigraph-common-style"/> </xsl:attribute> </xsl:attribute-set> <xsl:attribute-set name="sect1"> <xsl:attribute name="align">left</xsl:attribute> <xsl:attribute name="style"> <xsl:value-of select="$sect-common-style"/>; <xsl:value-of select="$sect1-font-size"/> </xsl:attribute> </xsl:attribute-set> <xsl:attribute-set name="sect2"> <xsl:attribute name="align">left</xsl:attribute> <xsl:attribute name="style"> <xsl:value-of select="$sect-common-style"/>; <xsl:value-of select="$sect2-font-size"/> </xsl:attribute> </xsl:attribute-set> <xsl:attribute-set name="normal"> <xsl:attribute name="align">left</xsl:attribute> <xsl:attribute name="style"> <xsl:value-of select="$standard-font-family"/>; <xsl:value-of select="$normal-font-size"/>; <xsl:value-of select="$normal-text-color"/> </xsl:attribute> </xsl:attribute-set> <!-- Templates --> <xsl:template match="/"> <html> <head> <xsl:apply-templates mode="head"/> <xsl:call-template name="extra-head-meta-data"/> </head> <body style= "margin-left:100;margin-right:100;margin-top:50;margin-bottom:50"> <xsl:apply-templates/> <xsl:apply-templates select="chapter/chapterinfo/*" mode="copyright"/> </body> </html> </xsl:template> <!-- Head --> <xsl:template match="chapter/title" mode="head"> <title><xsl:value-of select="."/></title> </xsl:template> <xsl:template match="author" mode="head"> <meta name="author" content="{concat(firstname,' ', surname)}"/> </xsl:template> <xsl:template match="copyright" mode="head"> <meta name="copyright" content="{concat(holder,' ',year)}"/> </xsl:template> <xsl:template match="text()" mode="head"/> <!--Override in importing stylesheet if necessary --> <xsl:template name="extra-head-meta-data"/> <!-- Body --> <xsl:template match="chapter"> <div xsl:use-attribute-sets="chapter-label"> <xsl:value-of select="@label"/> </div> <xsl:apply-templates/> </xsl:template> <xsl:template match="chapter/title"> <div xsl:use-attribute-sets="chapter-title"> <xsl:value-of select="."/> </div> </xsl:template> <xsl:template match="epigraph/para"> <div xsl:use-attribute-sets="epigraph-para"> <xsl:value-of select="."/> </div> </xsl:template> <xsl:template match="epigraph/attribution"> <div xsl:use-attribute-sets="epigraph-attribution"> <xsl:value-of select="."/> </div> </xsl:template> <xsl:template match="sect1"> <h1 xsl:use-attribute-sets="sect1"> <xsl:value-of select="title"/> </h1> <xsl:apply-templates/> </xsl:template> <xsl:template match="sect2"> <h2 xsl:use-attribute-sets="sect2"> <xsl:value-of select="title"/> </h2> <xsl:apply-templates/> </xsl:template> <xsl:template match="para"> <p xsl:use-attribute-sets="normal"> <xsl:value-of select="."/> </p> </xsl:template> <xsl:template match="text()"/> <xsl:template match="copyright" mode="copyright"> <div style="font-size : 10pt; font-family: Times serif; padding-top : 100"> <xsl:text>Copyright </xsl:text> <xsl:value-of select="holder"/> <xsl:text> </xsl:text> <xsl:value-of select="year"/> <xsl:text>. All rights reserved.</xsl:text> </div> </xsl:template> <xsl:template match="*" mode="copyright"/> </xsl:stylesheet>
Calling XSLT an object-oriented language would stretch the truth. The
behavior of xsl:import
is only loosely similar to
inheritance, and it operates at the level of the entire stylesheet.
Furthermore, it has no notion of encapsulation or data abstraction.
However, XSLT developers who already have an object-oriented mindset
can often leverage that experience to the creation of more modular
and reusable stylesheets.
Consider the example of overloading in the “Solution” section. Whenever you need to perform similar operations on disparate data, you will often want to structure your stylesheet in this manner. Of course, you could achieve the same result using conditional logic:
<xsl:template match="shape"> <tr> <td><xsl:value-of select="@kind"/></td> <td><xsl:value-of select="@id"/></td> <xsl:variable name="area"> <xsl:call-template name="area"/> </xsl:variable> <td align="right"><xsl:value-of select="format-number($area,'#.000')"/></td> </tr> </xsl:template> <xsl:template name="area"> <xsl:choose> <xsl:when test="@kind='triangle' "> <xsl:value-of select="@base * @height"/> </xsl:when> <xsl:when test="@kind='square' " > <xsl:value-of select="@side * @side"/> </xsl:when> <xsl:when test="@kind='rectangle' "> <xsl:value-of select="@width * @height"/> </xsl:when> <xsl:when test="@kind='circle' "> <xsl:value-of select="3.1415 * @radius * @radius"/> </xsl:when> </xsl:choose> </xsl:template>
In a simple case such as this, it is difficult to argue that the overloading method is superior to the conditional one. However, consider cases when someone needs to reuse the stylesheet, except that he must deal with triangles encoded in terms of sides and angles. If the stylesheet is written using separate templates for each shape, then importing the stylesheet and extending its behavior is simple. However, if the logic for each shape is written in a monolithic conditional manner, then the user must either copy the entire stylesheet and edit the parts that must change or extend the original and risk breaking it unintentionally.
Overriding is the key to creating modular and reusable XSLT. Nevertheless, reuse does not come for free; it requires planning. Specifically, you must think about three things:
What clients of your stylesheet might want to alter
What you may not want them to alter
What they are unlikely to alter
Failing to think about Item 1 results in an inflexible stylesheet that can be reused only by the cut, paste, and edit mechanism. Failing to think about Item 2 could create pitfalls for the client of your stylesheet. Failing to think about Item 3 results in overly complicated stylesheets with potentially poor performance and low maintainability.
In the DocBook stylesheet, we made it convenient to override font and other text attributes individually. We made it less convenient to override alignment attributes in a way that would create inconsistent or poor alignment choices between text elements where you desire consistency or a particular alignment. For example, it is more difficult to change the alignment of section and normal text elements by specifying their alignment separately. Finally, we did not try to make it easy for particular HTML element names to be overridden because doing so would have complicated the stylesheet in a way that adds little value.
Chris Rathman has a more extensive example of polymorphic XSLT at http://www.angelfire.com/tx4/cus/shapes/xsl.html.
You want to create reusable templates that perform a wide variety of node-set aggregation operations.
A fully generic extensible solution exploits the template-tagging method discussed in this chapter’s introduction:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate" extension-element-prefixes="generic"> <xsl:variable name="generic:public-generics" select="document('')/*/generic:*"/> <xsl:variable name="generic:generics" select="$generic:public-generics"/> <!-- Primitive generic functions on x --> <generic:func name="identity"/> <xsl:template match="generic:func[@name='identity']"> <xsl:param name="x"/> <xsl:value-of select="$x"/> </xsl:template> <generic:func name="square"/> <xsl:template match="generic:func[@name='square']"> <xsl:param name="x"/> <xsl:value-of select="$x * $x"/> </xsl:template> <generic:func name="cube"/> <xsl:template match="generic:func[@name='cube']"> <xsl:param name="x"/> <xsl:value-of select="$x * $x * $x"/> </xsl:template> <generic:func name="incr" param1="1"/> <xsl:template match="generic:func[@name='incr']"> <xsl:param name="x"/> <xsl:param name="param1" select="@param1"/> <xsl:value-of select="$x + $param1"/> </xsl:template> <!-- Primitive generic aggregators --> <generic:aggr-func name="sum" identity="0"/> <xsl:template match="generic:aggr-func[@name='sum']"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:value-of select="$x + $accum"/> </xsl:template> <generic:aggr-func name="product" identity="1"/> <xsl:template match="generic:aggr-func[@name='product']"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:value-of select="$x * $accum"/> </xsl:template> <!-- Generic aggregation template --> <xsl:template name="generic:aggregation"> <xsl:param name="nodes"/> <xsl:param name="aggr-func" select=" 'sum' "/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1" select="$generic:generics[self::generic:func and @name = $func]/@param1"/> <xsl:param name="i" select="1"/> <xsl:param name="accum" select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]/@identity"/> <xsl:choose> <xsl:when test="$nodes"> <!--Compute func($x) --> <xsl:variable name="f-of-x"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$nodes[1]"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable> <!-- Aggregate current $f-of-x with $accum --> <xsl:variable name="temp"> <xsl:apply-templates select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]"> <xsl:with-param name="x" select="$f-of-x"/> <xsl:with-param name="accum" select="$accum"/> <xsl:with-param name="i" select="$i"/> </xsl:apply-templates> </xsl:variable> <!--We tail recursively process the remaining nodes using position() --> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="$nodes[position()!=1]"/> <xsl:with-param name="aggr-func" select="$aggr-func"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="accum" select="$temp"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>
The generic code has three basic parts.
The first part consists of tagged generic functions on a single
variable x
. These functions allow performance of
aggregation operations on functions of an input set. The simplest
such function is identity
, which is used when you
want to aggregate the input set itself. Square, cube, and incr
functions are also predefined. Users of the stylesheet can define
other functions.
The second part consists of tagged generic aggregator functions. You will see two common implemented aggregators: sum and product. Again, importing stylesheets can add other forms of aggregation.
The third part consists of the generic aggregation algorithm. It
accepts as parameters a set of nodes to aggregate, the name of an
aggregator function (default is sum
), and the name
of a single element function (the default is
identity
). The $i
parameter
keeps track of the position of the currently processed node and is
made available to both the element and aggregation functions, should
they desire it. The $accum
keeps a working value
of the aggregation. Notice how the default value is initialized from
the @identity
attribute kept with the aggregate
function’s tag. This initialization demonstrates a
powerful feature of the generic approach with which metadata can be
associated with the function tags. This feature is reminiscent of the
way C++-based generic programming uses traits classes.
The first step to understanding this code is to show a simple application that both uses and extends the aggregation facilities, as shown in Example 16-1.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate" extension-element-prefixes="generic aggr"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/ generic:*"/> <!--Add a generic element function for computing reciprocal --> <generic:func name="reciprocal"/> <xsl:template match="generic:func[@name='reciprocal']"> <xsl:param name="x"/> <xsl:value-of select="1 div $x"/> </xsl:template> <!--Add generic aggregators for computing the min and the max values in a node set--> <generic:aggr-func name="min" identity=""/> <xsl:template match="generic:aggr-func[@name='min']"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:choose> <xsl:when test="$accum = @identity or $accum >= $x"> <xsl:value-of select="$x"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template> <generic:aggr-func name="max" identity=""/> <xsl:template match="generic:aggr-func[@name='max']"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:choose> <xsl:when test="$accum = @identity or $accum < $x"> <xsl:value-of select="$x"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template> <!--Test aggregation functionality --> <xsl:template match="numbers"> <results> <!-- Sum the numbers --> <sum> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> </xsl:call-template> </sum> <!-- Sum the squares --> <sumSq> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'square' "/> </xsl:call-template> </sumSq> <!-- Product of the reciprocals --> <prodRecip> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" 'product' "/> <xsl:with-param name="func" select=" 'reciprocal' "/> </xsl:call-template> </prodRecip> <!-- Maximum --> <max> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" 'max' "/> </xsl:call-template> </max> <!-- Minimum --> <min> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" 'min' "/> </xsl:call-template> </min> </results> </xsl:template> </xsl:stylesheet>
Example 16-1 shows how new element and aggregation functions can be added to those prepackaged with aggregation.xslt. You might not initially expect that computing minimums and maximums can be accomplished with this generic code, but it is quite easy to do.
You can test this code against the following input:
<numbers> <number>1</number> <number>2</number> <number>3</number> </numbers>
The result is:
<?xml version="1.0" encoding="utf-8"?> <results> <sum>6</sum> <sumSq>14</sumSq> <prodRecip>0.16666666666666666</prodRecip> <max>3</max> <min>1</min> </results>
The “Solution” section shows only the tip of the iceberg in relation to what can be done with this generic aggregation framework. For example, nothing says you must aggregate numbers. The following code shows how this generic code can be applied to strings as well:
<strings> <string>camel</string> <string>through</string> <string>the</string> <string>eye</string> <string>of</string> <string>needle</string> </strings> <!DOCTYPE stylesheet [ <!ENTITY % standard SYSTEM "../strings/standard.ent"> %standard; ]> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" extension-element-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!--Add a generic element function for converting first character of $x to uppercase --> <generic:func name="upperFirst"/> <xsl:template match="generic:func[@name='upperFirst']"> <xsl:param name="x"/> <!-- See Recipe 2.8 for an explanation of LOWER_TO_UPPER --> <xsl:variable name="upper" select="translate(substring($x,1,1),&LOWER_TO_UPPER;)"/> <xsl:value-of select="concat($upper, substring($x,2))"/> </xsl:template> <!--Add generic aggregator that concatenates --> <generic:aggr-func name="concat" identity=""/> <xsl:template match="generic:aggr-func[@name='concat']"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:value-of select="concat($accum,$x)"/> </xsl:template> <!--Test aggregation functionality --> <xsl:template match="strings"> <results> <camelCase> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="string"/> <xsl:with-param name="aggr-func" select=" 'concat' "/> <xsl:with-param name="func" select=" 'upperFirst' "/> </xsl:call-template> </camelCase> </results> </xsl:template> </xsl:stylesheet> <results> <camelCase>CamelThroughTheEyeOfNeedle</camelCase> </results>
Aggregation can also compute the statistical
functions’ average and variance. Here you exploit
the $i
index parameter. You need to be a little
crafty to compute variance; you need to maintain three values in the
$accum
parameter—the sum, the sum of the
squares, and the running variance. You can do this by using an
element with attributes. The only downside is that you are forced to
use a node-set function in XSLT 1.0:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="generic exslt"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!--Add generic aggregators for computing the min and the max values in a node set--> <generic:aggr-func name="avg" identity="0"/> <xsl:template match="generic:aggr-func[@name='avg']"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:param name="i"/> <xsl:value-of select="(($i - 1) * $accum + $x) div $i"/> </xsl:template> <generic:aggr-func name="variance" identity=""/> <xsl:template match="generic:aggr-func[@name='variance']"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:param name="i"/> <xsl:choose> <xsl:when test="$accum = @identity"> <!-- Initialize the sum, sum of squares, and variance. The variance of a single number is zero --> <variance sum="{$x}" sumSq="{$x * $x}">0</variance> </xsl:when> <xsl:otherwise> <!-- Use node-set to convert $accum to a nodes set containing the variance element --> <xsl:variable name="accumElem" select="exslt:node-set($accum)/*"/> <!-- Aggregate the sum of $x component --> <xsl:variable name="sum" select="$accumElem/@sum + $x"/> <!-- Aggregate the sum of $x squared component --> <xsl:variable name="sumSq" select="$accumElem/@sumSq + $x * $x"/> <!-- Return the element with attributes and the current variance as its value --> <variance sum="{$sum}" sumSq="{$sumSq}"> <xsl:value-of select="($sumSq - ($sum * $sum) div $i) div ($i - 1)"/> </variance> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="numbers"> <results> <!-- Average --> <avg> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" 'avg' "/> </xsl:call-template> </avg> <!-- Average of the squares --> <avgSq> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'square' "/> <xsl:with-param name="aggr-func" select=" 'avg' "/> </xsl:call-template> </avgSq> <!-- Variance --> <variance> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" 'variance' "/> </xsl:call-template> </variance> </results> </xsl:template> </xsl:stylesheet>
This example shows how you can use your aggregation facilities to compute sums of polymorphic functions:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns: generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:aggr="http://www. ora.com/XSLTCookbook/namespaces/aggregate" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="generic aggr"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!-- Extend the primitives to compute commission--> <generic:func name="commission"/> <xsl:template match="generic:func[@name='commission']"> <xsl:param name="x"/> <!-- defer actual computation to a polymorphic template using mode commission --> <xsl:apply-templates select="$x" mode="commission"/> </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 match="salesBySalesperson"> <results> <result> <xsl:text>Total commission = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*"/> <xsl:with-param name="aggr-func" select=" 'sum' "/> <xsl:with-param name="func" select=" 'commission' "/> </xsl:call-template> </result> <result> <xsl:text>Min commission = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*"/> <xsl:with-param name="aggr-func" select=" 'min' "/> <xsl:with-param name="func" select=" 'commission' "/> </xsl:call-template> </result> <result> <xsl:text>Max commission = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*"/> <xsl:with-param name="aggr-func" select=" 'max' "/> <xsl:with-param name="func" select=" 'commission' "/> </xsl:call-template> </result> <result> <xsl:text>Avg commission = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*"/> <xsl:with-param name="aggr-func" select=" 'avg' "/> <xsl:with-param name="func" select=" 'commission' "/> </xsl:call-template> </result> <result> <xsl:text>Avg sales = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*/product/@totalSales"/> <xsl:with-param name="aggr-func" select=" 'avg' "/> </xsl:call-template> </result> <result> <xsl:text>Min sales = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*/product/@totalSales"/> <xsl:with-param name="aggr-func" select=" 'min' "/> </xsl:call-template> </result> <result> <xsl:text>Max sales = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*/product/@totalSales"/> <xsl:with-param name="aggr-func" select=" 'max' "/> </xsl:call-template> </result> </results> </xsl:template> </xsl:stylesheet>
The result when run against salesBySalesperson.xml (see Chapter 4) is:
<results xmlns:exslt="http://exslt.org/common"> <result>Total commission = 471315</result> <result>Min commission = 19600</result> <result>Max commission = 364440</result> <result>Avg commission = 117828.75</result> <result>Avg sales = 584636.3636363636</result> <result>Min sales = 5500.00</result> <result>Max sales = 2920000.00</result> </results>
This section has demonstrated that many of the recipes implemented separately in Chapter 3 can be implemented easily in terms of this single generic example. In fact, this generic example can compute an infinite range of aggregation-like functions over a set of nodes. Unfortunately, this flexibility and generality is not free. A generic implementation will typically be 40% slower than a custom hand-coded solution. If speed is the most important consideration, then you may want to consider an optimized hand-coded solution. However, if you need to implement a complex piece of XSLT rapidly that performs a wider variety of aggregation operations, a generic solution will speed up development substantially.[1] One of the tricks of getting the most mileage out of this approach is to have many common generic element and aggregation functions that are ready to be used. In the actual implementation of aggregation.xslt, I have all the functions from this example (and several others). You can access the complete code at the book’s web site (http://www.oreilly.com/catalog/xsltckbk).
In cases when the aggregate function is not symmetric, you might need to aggregate over a node list in reverse order. This aggregation requires only a minor change to the generic aggregation function:
<xsl:template name="generic:reverse-aggregation"> <xsl:param name="nodes"/> <xsl:param name="aggr-func" select=" 'sum' "/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1" select="$generic:generics[self::generic:func and @name = $func]/@param1"/> <xsl:param name="i" select="1"/> <xsl:param name="accum" select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]/@identity"/> <xsl:choose> <xsl:when test="$nodes"> <!--Compute func($x) --> <xsl:variable name="f-of-x"> <xsl:apply-templates select= "$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$nodes[last()]"/> <xsl:with-param name="i" select="$i"/> </xsl:apply-templates> </xsl:variable> <!-- Aggregate current $f-of-x with $accum --> <xsl:variable name="temp"> <xsl:apply-templates select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]"> <xsl:with-param name="x" select="$f-of-x"/> <xsl:with-param name="accum" select="$accum"/> <xsl:with-param name="i" select="$i"/> </xsl:apply-templates> </xsl:variable> <xsl:call-template name="generic:reverse-aggregation"> <xsl:with-param name="nodes" select="$nodes[position()!=last()]"/> <xsl:with-param name="aggr-func" select="$aggr-func"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="accum" select="$temp"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template>
You want to create reusable templates for performing a wide variety of bounded aggregation operations.
<xsl:template name="generic:bounded-aggregation"> <xsl:param name="x" select="0"/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1"/> <xsl:param name="test-func" select=" 'less-than' "/> <xsl:param name="test-param1" select="$x + 1"/> <xsl:param name="incr-func" select=" 'incr' "/> <xsl:param name="incr-param1" select="1"/> <xsl:param name="i" select="1"/> <xsl:param name="aggr-func" select=" 'sum' "/> <xsl:param name="aggr-param1"/> <xsl:param name="accum" select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]/@identity"/> <!-- Check if aggregation should continue --> <xsl:variable name="continue"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $test-func]"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="param1" select="$test-param1"/> </xsl:apply-templates> </xsl:variable> <xsl:choose> <xsl:when test="string($continue)"> <!--Compute func($x) --> <xsl:variable name="f-of-x"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable> <!-- Aggregate current $f-of-x with $accum --> <xsl:variable name="temp"> <xsl:apply-templates select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]"> <xsl:with-param name="x" select="$f-of-x"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$aggr-param1"/> <xsl:with-param name="accum" select="$accum"/> </xsl:apply-templates> </xsl:variable> <!-- Compute the next value of $x--> <xsl:variable name="next-x"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $incr-func]"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="param1" select="$incr-param1"/> </xsl:apply-templates> </xsl:variable> <!--We tail recursively process the remaining nodes using position() --> <xsl:call-template name="generic:bounded-aggregation"> <xsl:with-param name="x" select="$next-x"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="test-func" select="$test-func"/> <xsl:with-param name="test-param1" select="$test-param1"/> <xsl:with-param name="incr-func" select="$incr-func"/> <xsl:with-param name="incr-param1" select="$incr-param1"/> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="aggr-func" select="$aggr-func"/> <xsl:with-param name="aggr-param1" select="$aggr-param1"/> <xsl:with-param name="accum" select="$temp"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template>
This template does not aggregate a node set, but rather aggregates
over a set of values defined by an initial value, an increment
function, and a predicate that determines when the aggregation should
terminate. The incrementing function and test function each can take
their own parameters. The other
generic:bounded-aggregation
parameters are the
same as generic:aggregation
in Recipe 16.2.
This recipe addresses the problem of aggregating XML content generically. However, sometimes you need to perform aggregation-like operations on mathematically synthesized data.
The simplest thing to do with this generic bounded aggregation function is to implement the factorial and prod-range templates from Recipe 3.5:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="generic aggr exslt"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <xsl:template name="factorial"> <xsl:param name="n" select="0"/> <xsl:call-template name="generic:bounded-aggregation"> <xsl:with-param name="x" select="$n"/> <xsl:with-param name="test-func" select=" 'greater-than' "/> <xsl:with-param name="test-param1" select="0"/> <xsl:with-param name="incr-func" select=" 'decr' "/> <xsl:with-param name="aggr-func" select=" 'product' "/> </xsl:call-template> </xsl:template> <xsl:template name="prod-range"> <xsl:param name="start" select="1"/> <xsl:param name="end" select="1"/> <xsl:call-template name="generic:bounded-aggregation"> <xsl:with-param name="x" select="$start"/> <xsl:with-param name="test-func" select=" 'less-than-eq' "/> <xsl:with-param name="test-param1" select="$end"/> <xsl:with-param name="incr-func" select=" 'incr' "/> <xsl:with-param name="aggr-func" select=" 'product' "/> </xsl:call-template> </xsl:template> <xsl:template match="/"> <results> <factorial n="0"> <xsl:call-template name="factorial"/> </factorial> <factorial n="1"> <xsl:call-template name="factorial"> <xsl:with-param name="n" select="1"/> </xsl:call-template> </factorial> <factorial n="5"> <xsl:call-template name="factorial"> <xsl:with-param name="n" select="5"/> </xsl:call-template> </factorial> <factorial n="20"> <xsl:call-template name="factorial"> <xsl:with-param name="n" select="20"/> </xsl:call-template> </factorial> <product start="1" end="20"> <xsl:call-template name="prod-range"> <xsl:with-param name="start" select="1"/> <xsl:with-param name="end" select="20"/> </xsl:call-template> </product> <product start="10" end="20"> <xsl:call-template name="prod-range"> <xsl:with-param name="start" select="10"/> <xsl:with-param name="end" select="20"/> </xsl:call-template> </product> </results> </xsl:template> </xsl:stylesheet>
The resulting output is:
<results> <factorial n="0">1</factorial> <factorial n="1">1</factorial> <factorial n="5">120</factorial> <factorial n="20">2432902008176640000</factorial> <product start="1" end="20">2432902008176640000</product> <product start="10" end="20">6704425728000</product> </results>
However, this is only the tip of the iceberg! You can also use
generic:bounded-aggregation
for numeric
integration:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="generic aggr exslt"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <xsl:template name="integrate"> <xsl:param name="from" select="0"/> <xsl:param name="to" select="1"/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="delta" select="($to - $from) div 100"/> <xsl:call-template name="generic:bounded-aggregation"> <xsl:with-param name="x" select="$from"/> <xsl:with-param name="func" select=" 'f-of-x-dx' "/> <xsl:with-param name="func-param1"> <params f-of-x="{$func}" dx="{$delta}"/> </xsl:with-param> <xsl:with-param name="test-func" select=" 'less-than' "/> <xsl:with-param name="test-param1" select="$to"/> <xsl:with-param name="incr-func" select=" 'incr' "/> <xsl:with-param name="incr-param1" select="$delta"/> <xsl:with-param name="aggr-func" select=" 'sum' "/> </xsl:call-template> </xsl:template> <xsl:template name="integrate2"> <xsl:param name="from" select="0"/> <xsl:param name="to" select="1"/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="delta" select="($to - $from) div 100"/> <xsl:call-template name="generic:bounded-aggregation"> <xsl:with-param name="x" select="$from"/> <xsl:with-param name="func" select=" 'f-of-x-dx-2' "/> <xsl:with-param name="func-param1"> <params f-of-x="{$func}" dx="{$delta}"/> </xsl:with-param> <xsl:with-param name="test-func" select=" 'less-than' "/> <xsl:with-param name="test-param1" select="$to"/> <xsl:with-param name="incr-func" select=" 'incr' "/> <xsl:with-param name="incr-param1" select="$delta"/> <xsl:with-param name="aggr-func" select=" 'sum' "/> </xsl:call-template> </xsl:template> <generic:func name="f-of-x-dx"/> <xsl:template match="generic:func[@name='f-of-x-dx']"> <xsl:param name="x"/> <xsl:param name="param1"/> <xsl:variable name="f-of-x"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = exslt:node-set($param1)/*/@f-of-x]"> <xsl:with-param name="x" select="$x"/> </xsl:apply-templates> </xsl:variable> <xsl:value-of select="$f-of-x * exslt:node-set($param1)/*/@dx"/> </xsl:template> <generic:func name="f-of-x-dx-2"/> <xsl:template match="generic:func[@name='f-of-x-dx-2']"> <xsl:param name="x"/> <xsl:param name="param1"/> <xsl:variable name="func" select="exslt:node-set($param1)/*/@f-of-x"/> <xsl:variable name="dx" select="exslt:node-set($param1)/*/@dx"/> <xsl:variable name="f-of-x"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$x"/> </xsl:apply-templates> </xsl:variable> <xsl:variable name="f-of-x-plus-dx"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$x + $dx"/> </xsl:apply-templates> </xsl:variable> <!-- This is just the absolute value of $f-of-x-plus-dx - $f-of-x --> <xsl:variable name="abs-diff" select="(1 - 2 *(($f-of-x-plus-dx - $f-of-x) < 0)) * ($f-of-x-plus-dx - $f-of-x)"/> <xsl:value-of select="$f-of-x * $dx + ($abs-diff * $dx) div 2"/> </xsl:template> <xsl:template match="/"> <results> <integrate desc="intgr x from 0 to 1"> <xsl:call-template name="integrate"/> </integrate> <integrate desc="intgr x from 0 to 1 with more precision"> <xsl:call-template name="integrate"> <xsl:with-param name="delta" select="0.001"/> </xsl:call-template> </integrate> <integrate desc="intgr x from 0 to 1 with better algorithm"> <xsl:call-template name="integrate2"/> </integrate> <integrate desc="intgr x**2 from 0 to 1"> <xsl:call-template name="integrate"> <xsl:with-param name="func" select=" 'square' "/> </xsl:call-template> </integrate> <integrate desc="intgr x**2 from 0 to 1 with better algorithm"> <xsl:call-template name="integrate2"> <xsl:with-param name="func" select=" 'square' "/> </xsl:call-template> </integrate> </results> </xsl:template> </xsl:stylesheet>
The challenge here is that you want the user of
the
integrate templates to pass in any old function of
x
. However, you need to compute a sum that is a
function of that function. Hence, you need a way to define a
higher-order function. In addition, you must pass the higher-order
function an additional parameter—in this case, the delta used
to approximate the integration. You can do this by passing an element
that you synthesize on the fly, <params f-of-x="{$func}"
dx="{$delta}"/>
. This compound parameter is used in the
higher-order functions f-of-x-dx
and
f-of-x-dx-2
. Unfortunately, XSLT 1.0 forces you to
use exsl:node-set
to extract information from the
compound parameter.
Here is the output of the stylesheet shown earlier:
<results> <integrate desc="intgr x from 0 to 1"> 0.4950000000000004 </integrate> <integrate desc="intgr x from 0 to 1 with more precision"> 0.4995000000000005 </integrate> <integrate desc="intgr x from 0 to 1 with better algorithm"> 0.5000000000000001 </integrate> <integrate desc="intgr x**2 from 0 to 1"> 0.32835000000000036 </integrate> <integrate desc="intgr x**2 from 0 to 1 with better algorithm"> 0.33335000000000037 </integrate> </results>
You are unlikely to need numerical integration in XSLT. Numerical integration is not the point of this example. Instead, it demonstrates the power of the generic programming style to accomplish much by using generic reusable code.
The solution involves recursively processing the elements in
$nodes
and invoking the generic function,
$func
, on each element. You allow the possibility
that the function specified by $func
is
parameterized. This parameter can be specified by
$func-param
. You further state that the default
value of the $func-param
is obtained from an
attribute, @param1
, in the generic functions tag.
This stipulation allows the default to be a function of the specified
generic:
<xsl:template name="generic:map"> <xsl:param name="nodes" select="/.."/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1" select="$generic:generics[self::generic:func and @name = $func]/@param1"/> <xsl:param name="i" select="1"/> <xsl:param name="result" select="/.."/> <xsl:choose> <xsl:when test="$nodes"> <xsl:variable name="temp"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$nodes[1]"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="$nodes[position() > 1]"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i +1"/> <xsl:with-param name="result" select="$result | exslt:node-set($temp)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="$result" mode="generic:map"/> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="/ | node() | @*" mode="generic:map"> <node> <xsl:copy-of select="."/> </node> </xsl:template>
You can see the effect of this process by considering the
incr
generic function:
<generic:func name="incr" param1="1"/> <xsl:template match="generic:func[@name='incr']"> <xsl:param name="x"/> <xsl:param name="param1" select="@param1"/> <xsl:value-of select="$x + $param1"/> </xsl:template>
The incr
generic function’s
parameter specifies the amount by which to increment and is defaulted
to one. Here you create a stylesheet that maps
incr
across a node set of numbers, first using the
default parameter and then by setting it to 10
.
For good measure, you also extend the set of generics to include a
reciprocal
function and map the numbers using that
function:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="exslt" exclude-result-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!--Add a generic element function for computing reciprocal --> <generic:func name="reciprocal"/> <xsl:template match="generic:func[@name='reciprocal']"> <xsl:param name="x"/> <xsl:value-of select="1 div $x"/> </xsl:template> <!--Test map functionality --> <xsl:template match="numbers"> <results> <incr> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'incr' "/> </xsl:call-template> </incr> <incr10> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'incr' "/> <xsl:with-param name="func-param1" select="10"/> </xsl:call-template> </incr10> <recip> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'reciprocal' "/> </xsl:call-template> </recip> </results> </xsl:template> </xsl:stylesheet>
This results in the following output:
<incr> <node>11</node> <node>4.5</node> <node>5.44</node> <node>78.7777</node> <node>-7</node> <node>2</node> <node>445</node> <node>2.1234</node> <node>8.77</node> <node>4.1415927</node> </incr> <incr10> <node>20</node> <node>13.5</node> <node>14.440000000000001</node> <node>87.7777</node> <node>2</node> <node>11</node> <node>454</node> <node>11.1234</node> <node>17.77</node> <node>13.1415927</node> </incr10> <recip> <node>0.1</node> <node>0.2857142857142857</node> <node>0.2252252252252252</node> <node>0.012857155714298572</node> <node>-0.125</node> <node>1</node> <node>0.0022522522522522522</node> <node>0.8901548869503294</node> <node>0.1287001287001287</node> <node>0.31830988148145367</node> </recip> </results>
Map can extract a subset of a node set that meets specified criteria.
Here you use a generic function that is a predicate. You achieve the
desired effect by structuring your predicates so that they return
their input when the predicate is true
and nothing
when the predicate is false
. For example:
<generic:func name="less-than"/> <xsl:template match="generic:func[@name='less-than']"> <xsl:param name="x"/> <!-- limit --> <xsl:param name="param1"/> <xsl:if test="$x < $param1"><xsl:value-of select="$x"/></xsl:if> </xsl:template>
You then capture the nodes of the result with a simple filtering template:
xsl:template match="/ | node() | @*" mode="generic:map"> <xsl:if test="string(.)"> <node> <xsl:copy-of select="."/> </node> </xsl:if> </xsl:template>
Here is a sample of the technique in action:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="exslt" exclude-result-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!--Test map functionality --> <xsl:template match="numbers"> <results> <less-than-5> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'less-than' "/> <xsl:with-param name="func-param1" select="5"/> </xsl:call-template> </less-than-5> <greater-than-5> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" 'greater-than' "/> <xsl:with-param name="func-param1" select="5"/> </xsl:call-template> </greater-than-5> </results> </xsl:template> <xsl:template match="/ | node() | @*" mode="generic:map"> <xsl:if test="string(.)"> <node> <xsl:copy-of select="."/> </node> </xsl:if> </xsl:template>
A test of this stylesheet produces the following output:
<results> <less-than-5> <node>3.5</node> <node>4.44</node> <node>-8</node> <node>1</node> <node>1.1234</node> <node>3.1415927</node> </less-than-5> <greater-than-5> <node>10</node> <node>77.7777</node> <node>444</node> <node>7.77</node> </greater-than-5> </results>
Mapping is not limited to numerical processing. Consider the
following stylesheet that finds the length of all
para
elements in a DocBook document:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="exslt" exclude-result-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!--Add a generic element function for computing reciprocal --> <generic:func name="length"/> <xsl:template match="generic:func[@name='length']"> <xsl:param name="x"/> <xsl:value-of select="string-length($x)"/> </xsl:template> <!--Test map functionality --> <xsl:template match="/"> <para-lengths> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="//para"/> <xsl:with-param name="func" select=" 'length' "/> </xsl:call-template> </para-lengths> </xsl:template> <xsl:template match="/ | node() | @*" mode="generic:map"> <length> <xsl:copy-of select="."/> </length> </xsl:template> </xsl:stylesheet>
Or this example, which creates a document summary by extracting the first three sentences of each paragraph:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="exslt" exclude-result-prefixes="generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!--A generic function for extracting sentences --> <generic:func name="extract-sentences" param1="1"/> <xsl:template match="generic:func[@name='extract- sentences']" name="generic:extract-sentences"> <xsl:param name="x"/> <xsl:param name="param1" select="@param1"/> <xsl:choose> <xsl:when test="$param1 >= 1 and contains($x,'.')"> <xsl:value-of select="substring-before($x,'.')"/> <xsl:text>.</xsl:text> <xsl:call-template name="generic:extract-sentences"> <xsl:with-param name="x" select="substring-after($x,'.')"/> <xsl:with-param name="param1" select="$param1 - 1"/> </xsl:call-template> </xsl:when> <xsl:otherwise/> </xsl:choose> </xsl:template> <xsl:template match="/"> <summary> <xsl:call-template name="generic:map"> <xsl:with-param name="nodes" select="//para"/> <xsl:with-param name="func" select=" 'extract-sentences' "/> <xsl:with-param name="func-param1" select="3"/> </xsl:call-template> </summary> </xsl:template> <xsl:template match="/ | node() | @*" mode="generic:map"> <para> <xsl:copy-of select="."/> </para> </xsl:template> </xsl:stylesheet>
These examples are convoluted ways of creating results that can easily be obtained with more straightforward stylesheets. Creating stylesheets that perform transformations a node at a time is easy. For example, the summary stylesheet is more clearly implemented as follows:
<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 name="extract-sentences"> <xsl:param name="text"/> <xsl:param name="num-sentences" select="1"/> <xsl:choose> <xsl:when test="$num-sentences >= 1 and contains($text,'.')"> <xsl:value-of select="substring-before($text,'.')"/> <xsl:text>.</xsl:text> <xsl:call-template name="extract-sentences"> <xsl:with-param name="text" select="substring-after($text,'.')"/> <xsl:with-param name="num-sentences" select="$num-sentences - 1"/> </xsl:call-template> </xsl:when> <xsl:otherwise/> </xsl:choose> </xsl:template> <xsl:template match="/"> <summary> <xsl:apply-templates select=".//para"/> </summary> </xsl:template> <xsl:template match="para"> <para> <xsl:call-template name="extract-sentences"> <xsl:with-param name="text" select="."/> <xsl:with-param name="num-sentences" select="3"/> </xsl:call-template> </para> </xsl:template> </xsl:stylesheet>
However, if in a single stylesheet you need to perform several map-like operations, then the generic implementation could result in less custom-written code.
An alternate mapping generic function is
generic:map2
. Rather than applying a unary
function to a single node set, generic:map2
applies a binary function to the elements of two node sets and
returns the resulting node set:
<xsl:template name="generic:map2"> <xsl:param name="nodes1" select="/.."/> <xsl:param name="nodes2" select="/.."/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1" select="$generic:generics[self::generic:func and @name = $func]/@param1"/> <xsl:param name="i" select="1"/> <xsl:param name="result" select="/.."/> <xsl:choose> <xsl:when test="$nodes1 and $nodes2"> <xsl:variable name="temp"> <xsl:apply-templates select="$generic:generics[self::generic:aggr-func and @name = $func]"> <xsl:with-param name="x" select="$nodes1[1]"/> <xsl:with-param name="accum" select="$nodes2[1]"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable> <xsl:call-template name="generic:map2"> <xsl:with-param name="nodes1" select="$nodes1[position() > 1]"/> <xsl:with-param name="nodes2" select="$nodes2[position() > 1]"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i +1"/> <xsl:with-param name="result" select="$result | exslt:node-set($temp)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="$result" mode="generic:map"/> </xsl:otherwise> </xsl:choose> </xsl:template>
generic:map2
operates on two independent node sets
in parallel and thus can apply any binary generic operation to the
successive nodes. As with generic:map
, the utility
is a function of the frequency of
usage.
The first generic function in this category generates a node set by executing a function over successive values, as defined by an incrementing function, until an upper bound is reached:
<xsl:template name="generic:gen-set"> <xsl:param name="x" select="1"/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1" select="$generic:generics[self::generic:func and @name = $func]/@param1"/> <xsl:param name="test-func" select=" 'less-than' "/> <xsl:param name="test-param1" select="$x + 1"/> <xsl:param name="incr-func" select=" 'incr' "/> <xsl:param name="incr-param1" select="1"/> <xsl:param name="i" select="1"/> <xsl:param name="result" select="/.."/> <!-- Check if aggregation should continue --> <xsl:variable name="continue"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $test-func]"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="param1" select="$test-param1"/> </xsl:apply-templates> </xsl:variable> <xsl:choose> <xsl:when test="string($continue)"> <!--Compute func($x) --> <xsl:variable name="f-of-x"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable> <!-- Compute the next value of $x--> <xsl:variable name="next-x"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $incr-func]"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="param1" select="$incr-param1"/> </xsl:apply-templates> </xsl:variable> <xsl:call-template name="generic:gen-set"> <xsl:with-param name="x" select="$next-x"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="test-func" select="$test-func"/> <xsl:with-param name="test-param1" select="$test-param1"/> <xsl:with-param name="incr-func" select="$incr-func"/> <xsl:with-param name="incr-param1" select="$incr-param1"/> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="result" select="$result | exslt:node-set($f-of-x)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="$result" mode="generic:gen-set"/> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="node()" mode="generic:gen-set"> <gen-set> <xsl:copy-of select="."/> </gen-set> </xsl:template>
Here you use this template to generate a list of squares of the first ten integers:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="text" /> <xsl:template match="/"> <xsl:call-template name="generic:gen-set"> <xsl:with-param name="x" select="1"/> <xsl:with-param name="func" select=" 'square' "/> <xsl:with-param name="incr-param1" select="1"/> <xsl:with-param name="test-func" select=" 'less-than-eq' "/> <xsl:with-param name="test-param1" select="10"/> </xsl:call-template> </xsl:template> <xsl:template match="node()" mode="generic:gen-set"> <xsl:value-of select="."/> <xsl:text> </xsl:text> </xsl:template> 1 4 9 16 25 36 49 64 81 100
The second generic function in this category generates a node set by
n
successive applications of a function starting
with an initial seed value:
<xsl:template name="generic:gen-nested"> <xsl:param name="x" select="1"/> <xsl:param name="func" select=" 'identity' "/> <xsl:param name="func-param1" select="$generic:generics[self::generic:func and @name = $func]/@param1"/> <xsl:param name="i" select="1"/> <xsl:param name="n" select="2"/> <xsl:param name="result"> <xsl:value-of select="$x"/> </xsl:param> <xsl:choose> <xsl:when test="$i <= $n"> <!--Compute func($x) --> <xsl:variable name="f-of-x"> <xsl:apply-templates select="$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="i" select="$i"/> <xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable> <xsl:call-template name="generic:gen-nested"> <xsl:with-param name="x" select="$f-of-x"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="n" select="$n"/> <xsl:with-param name="result" select="exslt:node-set($result) | exslt:node-set($f-of-x)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="$result" mode="generic:gen-nested"/> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="node()" mode="generic:gen-nested"> <gen-nested> <xsl:copy-of select="."/> </gen-nested> </xsl:template>
Here you use this template to build the series 2, 2 ** 2, (2 ** 2) **
2, ((2 ** 2) ** 2) ** 2, (((2 ** 2) ** 2) ** 2) ** 2, where
**
means to the power of
:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="text" /> <xsl:template match="/"> <xsl:call-template name="generic:gen-nested"> <xsl:with-param name="x" select="2"/> <xsl:with-param name="func" select=" 'square' "/> <xsl:with-param name="n" select="4"/> </xsl:call-template> </xsl:template> <xsl:template match="node()" mode="generic:gen-nested"> <xsl:value-of select="."/> <xsl:text> </xsl:text> </xsl:template> </xsl:stylesheet> 2 4 16 256 65536
Recipe 16.2 and Recipe 16.3 were many-to-one generic transformations, and Recipe 16.5 explained many-to-many transformations. Naturally, this chapter would not be complete without a one-to-many generic transform.
With a generator, you can create random numbers that can select random nodes from an XML document. This chapter uses a simple linear congruential generator (see, for example, http://www.taygeta.com/rwalks/node1.html). Here is a stylesheet that displays a random selection of names from an input document:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"> <xsl:import href="aggregation.xslt"/> <xsl:output method="xml" indent="yes"/> <!-- Extend the available generic functions --> <xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/generic:*"/> <!-- These values give good random results but you can tweak --> <xsl:variable name="a" select="16807"/> <xsl:variable name="c" select="0"/> <xsl:variable name="m" select="2147483647"/> <!-- Store the root for later use --> <xsl:variable name="doc" select="/"/> <!-- The random generator --> <generic:func name="linear-congruential"/> <xsl:template match="generic:func[@name='linear-congruential']"> <xsl:param name="x"/> <xsl:value-of select="($a * $x + $c) mod $m"/> </xsl:template> <xsl:template match="/"> <names> <xsl:call-template name="generic:gen-nested"> <xsl:with-param name="x" select="1"/> <xsl:with-param name="func" select=" 'linear-congruential' "/> <xsl:with-param name="n" select="100"/> <!-- Don't include initial seed --> <xsl:with-param name="result" select="/.."/> </xsl:call-template> </names> </xsl:template> <xsl:template match="node()" mode="generic:gen-nested"> <!-- Restrict the range to 1 through 100 --> <xsl:variable name="random" select=". mod 99 + 1"/> <name> <xsl:value-of select="$doc/names/name[$random]"/> </name> </xsl:template> </xsl:stylesheet>
[1] Not everyone would agree with this assessment. In fact, some would argue that using this approach slows down development because of the complexity caused by extra levels of indirection. However, repeated usage often makes the complex appear idiomatic. For example, recall how you felt when you first struggled with vanilla XSLT.