Sorting text and numeric results
Changing sort order
Sorting with multiple keys
Auto-numbering your result document
Handling outline-style numbering
I bet you’ve seen those commercials that promise you what you want when you need it: news at your fingertips, unmatched performance from your car, great taste, less filling. However, I don’t need a firm voice and a well-groomed toupee or a slick song culled from a little-known album to convince you that you can get what you want from XSLT.
However, to do so might involve a little “tweaking” of your stylesheets. You will find the practice of tweaking to be both useful and necessary when working with XSLT result documents. In this chapter, I look at how you can refine and fine-tune your output documents by sorting and auto-numbering the results.
Until now, all the nodes in the result trees have always been in the same order in which they appeared in the source document. But in many cases, you want to sort the results in a particular order. To this end, the xsl:sort instruction comes to the rescue, allowing you to greatly enhance your sorting capabilities when used with either xsl:apply-templates or xsl:for-each.
xsl:sort enables you to sort the nodes of a transformation based on the result of the XPath expression in its select attribute value. For example, the following instruction sorts the results based on the alphabetical value of the name child element returned for the current node:
<xsl:sort select=”name”/>
To illustrate how you can use xsl:sort, consider the following XML snippet:
<beverages>
<beverage>Coffee</beverage>
<beverage>Tea</beverage>
<beverage>Milk</beverage>
<beverage>Cola</beverage>
<beverage>Diet Cola</beverage>
<beverage>Root Beer</beverage>
<beverage>Water</beverage>
<beverage>Lemonade</beverage>
<beverage>Iced Tea</beverage>
<beverage>Wine</beverage>
</beverages>
Suppose you want to create a simple list of beverages from the preceding source document. To do so, create a template rule for the beverage element that prints its content as text:
<xsl:template match=”beverage”>
* <xsl:value-of select=”.”/>
</xsl:template>
By using only this template, the list generated is in the same order as what is in the original source:
* Coffee
* Tea
* Milk
* Cola
* Diet Cola
* Root Beer
* Water
* Lemonade
* Iced Tea
* Wine
However, by adding xsl:sort to the stylesheet, you can spruce up the output to make this an alphabetized list of beverages. To do so, create a new template rule for the sorting:
<xsl:template match=”beverages”>
<xsl:apply-templates>
<xsl:sort select=”.”/>
</xsl:apply-templates>
</xsl:template>
In this template rule, the parent element beverages is used as the match pattern. (I cannot use beverage as the pattern; it would conflict with the template rule I already defined.) Nestled within the xsl:apply-templates instruction is xsl:sort, which uses . as its select expression to sort by the value of the content for each beverage element. With this new template rule, the entire XSLT stylesheet looks like this:
<xsl:stylesheet xmlns:xsl=”http://www.w3.org/1999/XSL/Transform” version=”1.0”>
<!-- Sort by beverage -->
<xsl:template match=”beverages”>
<xsl:apply-templates>
<xsl:sort select=”.”/>
</xsl:apply-templates>
</xsl:template>
<!-- List beverage -->
<xsl:template match=”beverage”>
* <xsl:value-of select=”.”/>
</xsl:template>
</xsl:stylesheet>
After the transformation, the sorted result is as follows:
* Coffee
* Cola
* Diet Cola
* Iced Tea
* Lemonade
* Milk
* Root Beer
* Tea
* Water
* Wine
By default, the xsl:sort instruction decides how to sort based on the string result of the select attribute value. Because I’m working with alphabetical characters, this behavior is exactly what is expected in the preceding example. But this behavior becomes problematic when you want to sort with numeric values. Take, for example, the addition of a price attribute to the XML snippet:
<beverages>
<beverage price=”150”>Coffee</beverage>
<beverage price=”90”>Tea</beverage>
<beverage price=”80”>Milk</beverage>
<beverage price=”80”>Cola</beverage>
<beverage price=”80”>Diet Cola</beverage>
<beverage price=”80”>Root Beer</beverage>
<beverage price=”25”>Water</beverage>
<beverage price=”125”>Lemonade</beverage>
<beverage price=”90”>Iced Tea</beverage>
<beverage price=”200”>Wine</beverage>
</beverages>
By changing xsl:sort to arrange the output based on the value of the price attribute and adding the price to the resulting string, the updated stylesheet looks like:
<xsl:stylesheet xmlns:xsl=”http://www.w3.org/1999/XSL/Transform” version=”1.0”>
<!-- Sort by beverage price -->
<xsl:template match=”beverages”>
<xsl:apply-templates>
<xsl:sort select=”@price”/>
</xsl:apply-templates>
</xsl:template>
<!-- List beverage -->
<xsl:template match=”beverage”>
* <xsl:value-of select=”.”/> costs <xsl:value-of select=”@price”/>
</xsl:template>
</xsl:stylesheet>
The list generated appears as follows:
* Lemonade costs 125
* Coffee costs 150
* Wine costs 200
* Water costs 25
* Milk costs 80
* Cola costs 80
* Diet Cola costs 80
* Root Beer costs 80
* Tea costs 90
* Iced Tea costs 90
Well, the list was sorted on price all right, but not the way I wanted it to. The reason is that sort is treating the price value as plain text. Alphabetical sorting rules evaluate each character in a string one at a time. Therefore, when a number is sorted using these rules, values which start with 1 come before 2, whether the number is 1, 10, or 1,000,000,000. Looking at the preceding example, although 125 is actually a higher number than 25, it is still placed earlier when the sort is alphabetical.
To get the results I want, I need to sort by actual numeric value. To do so, use the xsl:sort’s data-type attribute to specify the type of sort. The two common data-type values are text and number:
text denotes that the sort order is based on the alphabetical rules for the language specified by any declared lang value. (This is the default.)
number specifies that the sort order is done by converting the result to a number and then sorting based on the numeric value.
By adding data-type to xsl:sort, I get:
<xsl:template match=”beverages”>
<xsl:apply-templates>
<xsl:sort select=”@price” data-type=”number”/>
</xsl:apply-templates>
</xsl:template>
My updated list is now sorted based on price:
* Water costs 25
* Milk costs 80
* Cola costs 80
* Diet Cola costs 80
* Root Beer costs 80
* Tea costs 90
* Iced Tea costs 90
* Lemonade costs 125
* Coffee costs 150
* Wine costs 200
The xsl:sort instruction has two additional attributes that enable you to further tweak the order in which the nodes are sorted:
order specifies whether text is sorted in ascending or descending order.
case-order specifies how text is sorted based on case. Use upper-first if you want all uppercase letters to sort before lowercase or lower-first for lowercase first.
To demonstrate the order attribute, I’ve changed the sorting template rule to sort in descending order:
<xsl:template match=”beverages”>
<xsl:apply-templates>
<xsl:sort select=”.” data-type=”text” order=”descending”/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match=”beverage”>
* <xsl:value-of select=”.”/>
</xsl:template>
The result is:
* Wine
* Water
* Tea
* Root Beer
* Milk
* Lemonade
* Iced Tea
* Diet Cola
* Cola
* Coffee
To illustrate the usage of the case-order attribute, I’ve modified the original XML code with some additional case-specific characters:
<beverages>
<beverage>coffee</beverage>
<beverage>COFFEE</beverage>
<beverage>Tea</beverage>
<beverage>milk</beverage>
<beverage>Cola</beverage>
<beverage>cola</beverage>
<beverage>diet Cola</beverage>
<beverage>Root Beer</beverage>
<beverage>Water</beverage>
<beverage>Lemonade</beverage>
<beverage>Iced Tea</beverage>
<beverage>WINE</beverage>
</beverages>
A case-order attribute is then added to xsl:sort:
<xsl:template match=”beverages”>
<xsl:apply-templates>
<xsl:sort select=”.” data-type=”text” case-order=”upper-first”/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match=”beverage”>
* <xsl:value-of select=”.”/>
</xsl:template>
In the result that follows, notice that the sorting takes place with uppercase values going first:
* COFFEE
* coffee
* Cola
* cola
* diet Cola
* Iced Tea
* Lemonade
* milk
* Root Beer
* Tea
* Water
* WINE
Notice that case-order is applied after the main text sorting is finished. For example, take the second and third items in the list: coffee and Cola. Even though Cola has an uppercase C, it appears after coffee because the word coffee is sorted alphabetically before the word cola.
Two or more xsl:sort instructions can be applied to sort using multiple keys. When the XSLT processor encounters multiple xsl:sort instructions, it sorts first by the initial one encountered, then by the second, and so on.
Consider the following XML:
<customers>
<customer id=”100”>
<firstname>Joan</firstname>
<lastname>Arc</lastname>
</customer>
<customer id=”101”>
<firstname>Bill</firstname>
<lastname>Shakespaire</lastname>
</customer>
<customer id=”102”>
<firstname>Ned</firstname>
<lastname>Ryerson</lastname>
</customer>
<customer id=”103”>
<firstname>Gerald</firstname>
<lastname>Smith</lastname>
</customer>
<customer id=”104”>
<firstname>Rock</firstname>
<lastname>Randels</lastname>
</customer>
<customer id=”105”>
<firstname>Ted</firstname>
<lastname>Narlybolyson</lastname>
</customer>
<customer id=”106”>
<firstname>Tim</firstname>
<lastname>Smith</lastname>
</customer>
<customer id=”107”>
<firstname>Thomas</firstname>
<lastname>Smith</lastname>
</customer>
</customers>
To create a list of customers sorted by the lastname and firstname elements, I set up the following stylesheet:
<xsl:stylesheet xmlns:xsl=”http://www.w3.org/1999/XSL/Transform” version=”1.0”>
<!-- Sort by lastname and firstname -->
<xsl:template match=”customers”>
<xsl:apply-templates>
<xsl:sort select=”lastname”/>
<xsl:sort select=”firstname”/>
</xsl:apply-templates>
</xsl:template>
<!-- List customers -->
<xsl:template match=”customer”>
<xsl:value-of select=”lastname”/><xsl:text>, </xsl:text><xsl:value-of select=”firstname”/><xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
In the first template rule, two xsl:sort elements are added so that the list is sorted by lastname first and then by firstname. In the second template rule, I employ two xsl:value-of instructions to convert the content of lastname and firstname elements to strings. I use an xsl:text element to add a carriage return to the end of each line. The result is:
Arc, Joan
Narlybolyson, Ted
Randels, Rock
Ryerson, Ned
Shakespaire, Bill
Smith, Gerald
Smith, Thomas
Smith, Tim
People frequently need to automatically generate sequential numbers for tree nodes in order to create the results documents they want. My earlier examples are cases in point: I use ordinary asterisks before each list item to form a bulleted list. But suppose I want to get fancy and change the bulleted list to a numbered list. Consider the same XML snippet as the source:
<beverages>
<beverage>Coffee</beverage>
<beverage>Tea</beverage>
<beverage>Milk</beverage>
<beverage>Cola</beverage>
<beverage>Diet Cola</beverage>
<beverage>Root Beer</beverage>
<beverage>Water</beverage>
<beverage>Lemonade</beverage>
<beverage>Iced Tea</beverage>
<beverage>Wine</beverage>
</beverages>
The xsl:number element is used to generate a unique number for each item and write a formatted number to the result tree. For a basic list, I use just the instruction without any attributes:
<xsl:template match=”beverage”>
<xsl:number/>. <xsl:apply-templates/>
</xsl:template>
The resulting output is:
1. Coffee
2. Tea
3. Milk
4. Cola
5. Diet Cola
6. Root Beer
7. Water
8. Lemonade
9. Iced Tea
10. Wine
The default number format is a simple 1, 2, 3, and so on until the end of the nodes. However, xsl:number provides an optional format attribute to provide a kind of numbering template. The XSLT processor looks at the format attribute value and bases its auto-numbering output on it. There are three basic types of tokens:
Numbers (1, 2, 3, and so on)
Roman numerals (I, II, III, and so on or i, ii, iii, and so on)
Letters (A, B, C, and so on or a, b, c, and so on)
You can intermix a token with literal text to create a template that you want reproduced for each xsl:number instruction processed. For example:
<xsl:number format=”1. “/>
This format value has the 1 token along with literal text (a period and a space), so that when the instruction is converted to a string, a unique sequential number plus the literal text is added to the output.
Table 9-1 lists a variety of common format values and the sequences for each one.
format Value | Sequence | Example Output |
---|---|---|
format=”1” | 1, 2, 3 . . . 10, 11, 12 . . . 101, 102, 103 . . . | 1Casablanca |
format=”01” | 01, 02, 03 . . . 10, 11, 12 . . . 101, | 01Casablanca |
102, 103 . . . | ||
format=”001” | 001, 002, 003 . . . 010, 011, 012 . . . 101, | 001Casablanca |
102, 103 . . . | ||
format=”1. “ | 1, 2, 3 . . . 10, 11, 12 . . . 101, 102, 103 . . . | 1. Casablanca |
format=”(1) “ | 1, 2, 3 . . . 10, 11, 12 . . . 101, 102, 103 . . . | (1) Casablanca |
format=”I “ | I, II, III, IV . . . | I Casablanca |
format=”i “ | i, ii, iii, iv . . . | i Casablanca |
format=”i) “ | i, ii, iii, iv . . . | i) Casablanca |
format=”A “ | A, B, C . . . Z, AA, BB, CC . . . | A Casablanca |
format=”a “ | a, b, c . . . z, aa, bb, cc . . . | a Casablanca |
To illustrate, suppose you add a format attribute to the xsl:number element in the preceding example:
<xsl:template match=”beverage”>
<xsl:number format=”(01) “/>
<xsl:apply-templates/>
</xsl:template>
The output is then changed to:
(01) Coffee
(02) Tea
(03) Milk
(04) Cola
(05) Diet Cola
(06) Root Beer
(07) Water
(08) Lemonade
(09) Iced Tea
(10) Wine
grouping-size=”number” specifies the size of a group of digits. This value is normally 3.
grouping-separator=”character” defines the character used to separate the number groups. This value is normally a comma.
For example, if you used grouping-size=”3” and grouping-separator=”,”, you get the following values: 1,000 and 1,000,000.
The xsl:number instruction provides a level attribute to enable you to handle numbering in a result document that needs numbering at multiple levels. For example, suppose I add a second level of beverage elements to the beverages structure:
<beverages>
<beverage>Coffee
<beverage>Drip Coffee</beverage>
<beverage>Latte</beverage>
<beverage>Espresso</beverage>
<beverage>Cappuccino</beverage>
</beverage>
<beverage>Tea
<beverage>Earl Grey</beverage>
<beverage>Green Tea</beverage>
</beverage>
<beverage>Milk
<beverage>Skim Milk</beverage>
<beverage>2 Percent Milk</beverage>
<beverage>Whole Milk</beverage>
</beverage>
<beverage>Cola
<beverage>Coca Cola</beverage>
<beverage>Pepsi</beverage>
<beverage>RC</beverage>
</beverage>
<beverage>Diet Cola
<beverage>Diet Coke</beverage>
<beverage>Diet Pepsi</beverage>
</beverage>
<beverage>Root Beer</beverage>
<beverage>Water
<beverage>Sparking Mineral Water</beverage>
<beverage>Spring Water</beverage>
</beverage>
<beverage>Lemonade</beverage>
<beverage>Iced Tea</beverage>
<beverage>Wine</beverage>
</beverages>
Using xsl:number’s level attribute, I can specify how I want to number the result document across these levels. The following template rule shows the level attribute being added:
<xsl:template match=”beverage”>
<xsl:number format=”1. “ level=”single”/>
<xsl:apply-templates/>
</xsl:template>
The level attribute accepts three values:
level=”single” (the default) numbers each level on its own, ignoring other levels that may exist above it or below it. The resulting output is:
1. Coffee
1. Drip Coffee
2. Latte
3. Espresso
4. Cappuccino
2. Tea
1. Earl Grey
2. Green Tea
3. Milk
1. Skim Milk
2. 2 Percent Milk
3. Whole Milk
4. Cola
1. Coca Cola
2. Pepsi
3. RC
5. Diet Cola
1. Diet Coke
2. Diet Pepsi
6. Root Beer
7. Water
1. Sparking Mineral Water
2. Spring Water
8. Lemonade
9. Iced Tea
10. Wine
level=”multiple” treats the document as something like an outline, maintaining an interrelationship across hierarchical levels:
1. Coffee
1.1. Drip Coffee
1.2. Latte
1.3. Espresso
1.4. Cappuccino
2. Tea
2.1. Earl Grey
2.2. Green Tea
3. Milk
3.1. Skim Milk
3.2. 2 Percent Milk
3.3. Whole Milk
4. Cola
4.1. Coca Cola
4.2. Pepsi
4.3. RC
5. Diet Cola
5.1. Diet Coke
5.2. Diet Pepsi
6. Root Beer
7. Water
7.1. Sparking Mineral Water
7.2. Spring Water
8. Lemonade
9. Iced Tea
10. Wine
level=”any” ignores hierarchy altogether and simply numbers each matching node sequentially:
1. Coffee
2. Drip Coffee
3. Latte
4. Espresso
5. Cappuccino
6. Tea
7. Earl Grey
8. Green Tea
9. Milk
10. Skim Milk
11. 2 Percent Milk
12. Whole Milk
13. Cola
14. Coca Cola
15. Pepsi
16. RC
17. Diet Cola
18. Diet Coke
19. Diet Pepsi
20. Root Beer
21. Water
22. Sparking Mineral Water
23. Spring Water
24. Lemonade
25. Iced Tea
26. Wine
By default, the XSLT processor uses all the nodes it encounters that match the sort key in determining the list count. So, in the earlier template rule examples that returned beverage element nodes, the numbering scheme is based on this type of element. However, XSLT allows you to base the count on more than just the selected node by using its count attribute. The xsl:number’s count attribute specifies the nodes that need to be counted at the levels defined by the level attribute.
I illustrate the usage of the count attribute by adding a size element to the beverages document:
<beverages>
<beverage>Coffee
<size>Small</size>
<size>Medium</size>
<size>Large</size>
<size>Extra Large</size>
</beverage>
<beverage>Tea
<size>Small</size>
<size>Large</size>
</beverage>
<beverage>Milk
<size>Small</size>
<size>Large</size>
</beverage>
<beverage>Cola
<size>Small</size>
<size>Medium</size>
<size>Large</size>
<size>Extra Large</size>
</beverage>
<beverage>Diet Cola
<size>Small</size>
<size>Medium</size>
<size>Large</size>
<size>Extra Large</size>
</beverage>
</beverages>
My objective is to number the result document like an outline (1., 1.1, 1.2, 2., 2.1, and so on), so I first add a level=”multiple” attribute value. I want to base the first part of my size number on the position of the parent beverage element.
To do so, I am going to create two template rules: the first to number the beverage elements like I did before, and the second to number the size elements. In the size template rule, I add the count attribute to specify that the first number in a multilevel number is based on the number of the beverage element, while the second value is based on the size element:
<xsl:template match=”beverage”>
<xsl:number format=”1. “ level=”multiple”/>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match=”size”>
<xsl:number format=”1. “ level=”multiple” count=”beverage|size”/>
<xsl:apply-templates/>
</xsl:template>
The result of this transformation is as follows:
1. Coffee
1.1. Small
1.2. Medium
1.3. Large
1.4. Extra Large
2. Tea
2.1. Small
2.2. Large
3. Milk
3.1. Small
3.2. Large
4. Cola
4.1. Small
4.2. Medium
4.3. Large
4.4. Extra Large
5. Diet Cola
5.1. Small
5.2. Medium
5.3. Large
5.4. Extra Large
The following example shows how you can use count to span your sequencing across multiple levels and elements. By adding a types element to the beverages structure, the following XML snippet becomes the source:
<beverages>
<types>Starbucks Crowd
<beverage>Coffee
<size>Small</size>
<size>Medium</size>
<size>Large</size>
<size>Extra Large</size>
</beverage>
<beverage>Tea
<size>Small</size>
<size>Large</size>
</beverage>
</types>
<types>Natural Types
<beverage>Milk
<size>Small</size>
<size>Large</size>
</beverage>
<beverage>Water
<size>Small</size>
<size>Medium</size>
<size>Large</size>
</beverage>
</types>
<types>Popular Sodas
<beverage>Cola
<size>Small</size>
<size>Medium</size>
<size>Large</size>
<size>Extra Large</size>
</beverage>
<beverage>Diet Cola
<size>Small</size>
<size>Medium</size>
<size>Large</size>
<size>Extra Large</size>
</beverage>
<beverage>Root Beer
<size>Medium</size>
</beverage>
</types>
</beverages>
To use the number of the types element in the outline numbering scheme of the beverage and size elements, add the types element to the count attribute value:
<xsl:template match=”beverage”>
<xsl:number format=”1. “ level=”multiple” count=”types|beverage”/>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match=”size”>
<xsl:number format=”1. “ level=”multiple” count=”types|beverage|size”/>
<xsl:apply-templates/>
</xsl:template>
The result is as follows:
Starbucks Crowd
1.1. Coffee
1.1.1. Small
1.1.2. Medium
1.1.3. Large
1.1.4. Extra Large
1.2. Tea
1.2.1. Small
1.2.2. Large
Natural Types
2.1. Milk
2.1.1. Small
2.1.2. Large
2.2. Water
2.2.1. Small
2.2.2. Medium
2.2.3. Large
Popular Sodas
3.1. Cola
3.1.1. Small
3.1.2. Medium
3.1.3. Large
3.1.4. Extra Large
3.2. Diet Cola
3.2.1. Small
3.2.2. Medium
3.2.3. Large
3.2.4. Extra Large
3.3. Root Beer
3.3.1. Medium
In this example, the types elements aren’t numbered, but their index value is factored into the overall number sequence of the beverage and size elements.