Chapter 9

Tweaking the Results to Get What You Want

In This Chapter

bullet Sorting text and numeric results

bullet Changing sort order

bullet Sorting with multiple keys

bullet Auto-numbering your result document

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

Sorting Elements in the Results Tree

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 type

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:

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

bullet 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

Changing sort order

The xsl:sort instruction has two additional attributes that enable you to further tweak the order in which the nodes are sorted:

bullet order specifies whether text is sorted in ascending or descending order.

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

Tip

The case-order attribute is ignored when you sort by number (use data-type=”number”).

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.

Tip

Think of case-order as a tie-breaker between similar values, not as the sole determinant of the xsl:sort instruction’s sorting order.

Sorting with multiple keys

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

Adding Automatic Numbering

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

Tip

When you use xsl:number without any of its formatting attributes, the result is simply a number. Therefore, I added a (period and space) as literal text between the xsl:number and xsl:value-of instructions to generate a formatted list. However, I show you in the next section how you can add those literal text formatting characters inside the xsl:number definition.

Adjusting the format

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:

bullet Numbers (1, 2, 3, and so on)

bullet Roman numerals (I, II, III, and so on or i, ii, iii, and so on)

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

Table 9-1 Number Formats
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

Tip

If you are numbering a result document that can count into the thousands or even millions, you can use xsl:number’s grouping-size and grouping-separator attributes to add grouping formatting:

bullet grouping-size=”number” specifies the size of a group of digits. This value is normally 3.

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

Handling multiple levels

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:

bullet 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

bullet 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

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset