Arithmetic is being able to count up to twenty without taking off your shoes.
Chapter 1 lamented the absence of sophisticated string-processing facilities in native XSLT. By comparison, XSLT’s handling of numerical computation is truly “Mickey Mouse”! XSLT 1.0 gives you facilities for basic arithmetic, counting, summing, and formatting numbers, but the remaining mathematics is up to your sheer wit. Fortunately, as with strings, XSLT’s recursive powers permit reasonable mathematical feats with reasonable effort.
Do not expect to find matrix multiplication or Fast-Fourier transform recipes in this section. If you really need them to perform on XML-encoded data then XSLT is not the language for you. Instead, bring the data into a C, C++, or Fortran program using an XSLT frontend converter or native SAX or DOM interface. Nevertheless, a web page called “Gallery of Stupid XSL and XSLT Tricks” (http://www.incrementaldevelopment.com/xsltrick/) contains some interesting mathematical XSLT curiosities such as computing primes and differentiating polynomials. These tricks can be instructive because they might extend your understanding of XSLT. Instead, this chapter concentrates on recipes that demonstrate commonly used mathematics that can be implemented economically within the confines of XSLT.
Some of this section’s early examples read more like tutorials on how to use native functionality in XSLT. I include these examples because they represent XSLT facilities that are sometimes misunderstood.
Many of the recipes shown here are implementations of
EXSLT’s math definitions. When a pure XSLT
implementation is available at EXSLT.org, we will discuss that first
and then consider alternative solutions.[1] Pure XSLT
implementations are provided for all extensions defined in EXSLT math
except for trigonometric functions (sin
,
cos
, etc.). If you desperately need a pure XSLT
implementation of trigonometric functions, then Recipe 2.5 will point you in one general direction.
Many discussion sections in this chapter will explore alternative implementations of the solution. Readers uninterested in technical details are encouraged simply to use the example shown in the solution section since it will be the best solution or as good as the alternatives.
This problem has two general solutions.
The top-level element xsl:decimal-format
establishes a named formatting rule that can be referenced by the
format-number( )
function whenever that format rule is
required. xsl:decimal-format
has a rich set of
attributes that describe the formatting rules. Table 2-1 explains each attribute and shows its default
value in parentheses.
Table 2-1. Attributes of the xsl:decimal-format element
Attribute |
Purpose |
---|---|
name |
An optional name for the rule. If absent, this rule becomes the default rule. There can be only one default, and all names must be unique (even when there is a difference in import precedence). |
decimal-separator (.) |
The character used to separate the whole and fractional parts of a number. |
grouping-separator (,) |
The character used to separate groups of digits. |
infinity (Infinity) |
The string that represents infinity. |
minus-sign (-) |
The character used as a minus sign. |
NaN (NaN) |
The string representing the numerical value “not a number.” |
percent (%) |
The percent sign. |
per-mille (‰) |
The per-mille sign. |
zero-digit (0) |
The character used in formatting pattern to indicate where a leading or trailing zero should be placed. Setting this character resets the origin of the numbering system. See Example 2-1 in Section 2.1.3. |
digit (#) |
The character used in a formatting pattern to indicate where a digit will appear, provided it is significant. |
pattern-separator (;) |
The character used in a formatting pattern to separate the positive subpattern from the negative subpattern. |
The format-number( )
function takes the arguments
shown in Table 2-2.
The most common use of
xsl:number
is to number nodes sequentially.
However, it can also format numbers. When used to perform the later,
the relevant attributes are shown in Table 2-3.
Table 2-3. Attributes of xsl:number
Name |
Purpose |
---|---|
value |
The number to be formatted. |
format |
A format string (see later). |
lang |
A language code as defined by the the |
letter-value |
Must be either |
grouping-separator |
A single character used to separate groups. For example, in the US, the comma (,) is standard. |
grouping-size |
The number of digits in each group. |
Table 2-4 shows how formatting tokens are used with a format attribute.
Table 2-4. Example behavior of formatting tokens used with a format attribute
Format token |
Example value |
Resulting output |
---|---|---|
1 |
1 |
1 |
1 |
99 |
99 |
01 |
1 |
01 |
001 |
1 |
001 |
a |
1 |
a |
a |
10 |
j |
a |
27 |
aa |
A |
1 |
A |
A |
27 |
AA |
i |
1 |
i |
i |
10 |
x |
I |
1 |
I |
I |
11 |
XI |
The format string is an alternating sequence of format and punctuation tokens. Using multiple format tokens makes sense only when the value contains a set of numbers.
Given the formatting machinery defined earlier, almost any numeric-formatting task can be handled.
Here we can take advantage of leading and trailing zero padding and then map the leading zeros to spaces and use a trailing minus sign. This solution gives a nice columnar, right-justified output when the final display medium uses a fixed-width font. Example 2-1 through Example 2-3 show more conventional ways of padding. The examples illustrate the behavior of the 0 digit when used as a format character.
Example 2-1. Input
<numbers> <number>10</number> <number>3.5</number> <number>4.44</number> <number>77.7777</number> <number>-8</number> <number>1</number> <number>444</number> <number>1.1234</number> <number>7.77</number> <number>3.1415927</number> <number>10</number> <number>9</number> <number>8</number> <number>7</number> <number>666</number> <number>5555</number> <number>-4444444</number> <number>22.33</number> <number>18</number> <number>36.54</number> <number>43</number> <number>99999</number> <number>999999</number> <number>9999999</number> <number>32</number> <number>64</number> <number>-64.0001</number> </numbers>
Example 2-2. format-numbers-into-columns.xslt
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text" /> <xsl:variable name="numCols" select="3"/> <xsl:template match="numbers"> <xsl:for-each select="number[position( ) mod $numCols = 1]"> <xsl:apply-templates select=". | following-sibling::number[position( ) < $numCols]" mode="format"/> <xsl:text>
</xsl:text> </xsl:for-each> </xsl:template> <xsl:template match="number" name="format" mode="format"> <xsl:param name="number" select="." /> <xsl:call-template name="leading-zero-to-space"> <xsl:with-param name="input" select="format-number($number, '0000000.0000 ;0000000.0000- ')"/> </xsl:call-template> </xsl:template> <xsl:template name="leading-zero-to-space"> <xsl:param name="input"/> <xsl:choose> <xsl:when test="starts-with($input,'0')"> <xsl:value-of select="' '"/> <xsl:call-template name="leading-zero-to-space"> <xsl:with-param name="input" select="substring-after($input,'0')"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$input"/> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>
Example 2-4 gives a variation of the previous format template that will make your accountant happy.
Example 2-4. Accountant-friendly format
<xsl:template match="number" name="format" mode="format">
<xsl:param name="number" select="." />
<xsl:text> $ </xsl:text>
<xsl:call-template name="leading-zero-to-space">
<xsl:with-param name="input"
select="format-number($number,
' 0000000.00 ;(0000000.00)')"/>
</xsl:call-template>
</xsl:template>
Output:
$ 10.00 $ 3.50 $ 4.44
$ 77.78 $ (8.00) $ 1.00
$ 444.00 $ 1.12 $ 7.77
$ 3.14 $ 10.00 $ 9.00
$ 8.00 $ 7.00 $ 666.00
$ 5555.00 $ (4444444.00) $ 22.33
$ 18.00 $ 36.54 $ 43.00
$ 99999.00 $ 999999.00 $ 9999999.00
$ 32.00 $ 64.00 $ (64.00)
Example 2-5 demonstrates the use of a named format.
Example 2-5. European-number format
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:str="http://www.ora.com/XSLTCookbook/namespaces/strings"> <xsl:output method="text" /> <!-- From Recipe 1.5 ... --> <xsl:include href="../strings/str.dup.xslt"/> <xsl:variable name="numCols" select="3"/> <xsl:decimal-format name="WesternEurope" decimal-separator="," grouping-separator="."/> <xsl:template match="numbers"> <xsl:for-each select="number[position( ) mod $numCols = 1]"> <xsl:apply-templates select=". | following-sibling::number[position( ) < $numCols]" mode="format"/> <xsl:text>
</xsl:text> </xsl:for-each> </xsl:template> <xsl:template match="number" name="format" mode="format"> <xsl:param name="number" select="." /> <xsl:call-template name="pad"> <xsl:with-param name="string" select="format-number($number,'#.###,00','WesternEurope')"/> </xsl:call-template> </xsl:template> <xsl:template name="pad"> <xsl:param name="string"/> <xsl:param name="width" select="16"/> <xsl:call-template name="str:dup"> <xsl:with-param name="input" select="' '"/> <xsl:with-param name="count" select="$width - string-length($string)"/> </xsl:call-template> <xsl:value-of select="$string"/> </xsl:template> </xsl:stylesheet> Output: 10,00 3,50 4,44 77,78 -8,00 1,00 444,00 1,12 7,77 3,14 10,00 9,00 8,00 7,00 666,00 5.555,00 -4.444.444,00 22,33 18,00 36,54 43,00 99.999,00 999.999,00 9.999.999,00 32,00 64,00 -64,00
Example 2-6 uses xsl:number
as a Roman
numeral formatter to label columns as rows:
Example 2-6. Roman-numeral format
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:str="http://www.ora.com/XSLTCookbook/namespaces/strings"> <xsl:output method="text" /> <xsl:include href="../strings/str.dup.xslt"/> <xsl:variable name="numCols" select="3"/> <xsl:template match="numbers"> <xsl:for-each select="number[position( ) <= $numCols]"> <xsl:text> </xsl:text> <xsl:number value="position( )" format="I"/><xsl:text> </xsl:text> </xsl:for-each> <xsl:text>
 </xsl:text> <xsl:for-each select="number[position( ) <= $numCols]"> <xsl:text>———————— </xsl:text> </xsl:for-each> <xsl:text>
</xsl:text> <xsl:for-each select="number[position( ) mod $numCols = 1]"> <xsl:call-template name="pad"> <xsl:with-param name="string"> <xsl:number value="position( )" format="i"/> </xsl:with-param> <xsl:with-param name="width" select="4"/> </xsl:call-template>|<xsl:text/> <xsl:apply-templates select=". | following-sibling::number[position( ) < $numCols]" mode="format"/> <xsl:text>
</xsl:text> </xsl:for-each> </xsl:template> <xsl:template match="number" name="format" mode="format"> <xsl:param name="number" select="." /> <xsl:call-template name="pad"> <xsl:with-param name="string" select="format-number(.,'#,###.00')"/> </xsl:call-template> </xsl:template> <xsl:template name="pad"> <xsl:param name="string"/> <xsl:param name="width" select="16"/> <xsl:call-template name="str:dup"> <xsl:with-param name="input" select="' '"/> <xsl:with-param name="count" select="$width - string-length($string)"/> </xsl:call-template> <xsl:value-of select="$string"/> </xsl:template> </xsl:stylesheet> Output: I II III ---------------- -------------- -------------- i| 10.00 3.50 4.44 ii| 77.78 -8.00 1.00 iii| 444.00 1.12 7.77 iv| 3.14 10.00 9.00 v| 8.00 7.00 666.00 vi| 5,555.00 -4,444,444.00 22.33 vii| 18.00 36.54 43.00 viii| 99,999.00 999,999.00 9,999,999.00 ix| 32.00 64.00 -64.00
Spreadsheets number columns in the
alpha sequence A, B, C ... ZZ, and we can do the same using
xsl:number
(see Example 2-7).
Example 2-7. Spreadsheet-like column numbers
<xsl:template match="numbers"> <xsl:for-each select="number[position( ) <= $numCols]"> <xsl:text> </xsl:text> <xsl:number value="position( )" format="A"/><xsl:text> </xsl:text> <xsl:text> </xsl:text> </xsl:for-each> <xsl:text>
</xsl:text> <xsl:for-each select="number[position( ) <= $numCols]"> <xsl:text> ---------------- </xsl:text> </xsl:for-each> <xsl:text>
</xsl:text> <xsl:for-each select="number[position( ) mod $numCols = 1]"> <xsl:value-of select="position( )"/><xsl:text>|</xsl:text> <xsl:apply-templates select=". | following-sibling::number[position( ) < $numCols]" mode="format"/> <xsl:text>
</xsl:text> </xsl:for-each> </xsl:template> <xsl:template match="number" name="format" mode="format"> <xsl:param name="number" select="." /> <xsl:call-template name="pad"> <xsl:with-param name="string" select="format-number(.,'#,###.00')"/> </xsl:call-template> <xsl:text> </xsl:text> </xsl:template> Output: A B C ------------ ------------ ------------ 1| 10.0000 3.5000 4.4400 2| 77.7777 8.0000- 1.0000 3| 444.0000 1.1234 7.7700 4| 3.1416 10.0000 9.0000 5| 8.0000 7.0000 666.0000 6| 5555.0000 4444444.0000- 22.3300 7| 18.0000 36.5400 43.0000 8| 99999.0000 999999.0000 9999999.0000 9| 32.0000 64.0000 64.0001-
Numeric characters
from other languages can be used by setting the zero digit in
xsl:decimal-format
to the zero of that language.
This example uses the Unicode character 0x660 (Arabic-Indic Digit
Zero):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:str="http://www.ora.com/XSLTCookbook/namespaces/strings"> <xsl:output method="text" encoding="UTF-8"/> <xsl:include href="../strings/str.dup.xslt"/> <!-- This states that zero starts at character 0x660, which implies one is 0x661, etc. --> <xsl:decimal-format name="Arabic" zero-digit="٠"/> <xsl:template match="numbers"> <xsl:for-each select="number"> <xsl:call-template name="pad"> <xsl:with-param name="string" select="format-number(.,'#,###.00')"/> </xsl:call-template> = <xsl:text/> <xsl:value-of select="format-number(.,'#,###.٠٠','Arabic')"/> <xsl:text>
</xsl:text> </xsl:for-each> </xsl:template> <xsl:template name="pad"> <xsl:param name="string"/> <xsl:param name="width" select="16"/> <xsl:value-of select="$string"/> <xsl:call-template name="str:dup"> <xsl:with-param name="input" select="' '"/> <xsl:with-param name="count" select="$width - string-length($string)"/> </xsl:call-template> </xsl:template> </xsl:stylesheet>
Here is the output of this code:
[1] The EXSLT implementations shown are as they existed at the time of this writing. Naturally, these may be improved over time or deprecated as new version of XPath and XSLT emerge.