Chapter 9 - Tweaking the Results to Get What You Want | |
XSLT For Dummies | |
by Richard Wagner | |
Hungry Minds 2002 |
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 . Remember You can use the xsl:sort element only inside xsl:apply-templates and xsl:for-each instructions. 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"/> Tip In the preceding xsl:sort instruction, the name element is often called the sort key . 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 Sorting by typeBy default, the xsl:sort instruction decides how to sort based on the string result of the select attribute value. Because Im 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 :
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 Changing sort orderThe xsl:sort instruction has two additional attributes that enable you to further tweak the order in which the nodes are sorted:
Tip The case-order attribute is ignored when you sort by number (use data-type="number" ). To demonstrate the order attribute, Ive 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, Ive 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 . Tip Think of case-order as a tie-breaker between similar values, not as the sole determinant of the xsl:sort instructions sorting order. Sorting with multiple keysTwo 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
|