page 115 xsl:variable
xsl:template match= xsl:textAddresses sorted by Zip Codexsl:text
xsl:value-of select=newline xsl:for-each select=addressbookaddress
xsl:sort select=zip xsl:sort select=namelast-name
xsl:sort select=namefirst-name xsl:variable name=lastZip select=zip
xsl:if test=notpreceding-sibling::address[zip=lastZip] xsl:textZip code xsl:text
xsl:value-of select=zip xsl:text: xsl:text
xsl:value-of select=newline xsl:for-each select=addressbookaddress[zip=lastZip]
xsl:sort select=namelast-name xsl:sort select=namefirst-name
xsl:if test=nametitle xsl:value-of select=nametitle
xsl:text xsl:text xsl:if
xsl:value-of select=namefirst-name xsl:text xsl:text
xsl:value-of select=namelast-name xsl:value-of select=newline
xsl:value-of select=street xsl:value-of select=newline
xsl:value-of select=newline xsl:for-each
xsl:if xsl:for-each
xsl:template xsl:stylesheet
This stylesheet generates what we want:
Addresses sorted by Zip Code Zip code 00218:
Ms. Natalie Attired 707 Breitling Way
Zip code 02718: Harry Backstayge
283 First Avenue Mary Backstayge
283 First Avenue Zip code 02930:
Ms. Amanda Reckonwith 930-A Chestnut Street
Zip code 27318: Mary McGoon
103 Bryant Street Zip code 48392:
Mr. Chester Hasbrouck Frisby 1234 Main Street
page 116
So why does this approach work when our first attempt didnt? The answer is: we dont count on the sorted order of the elements to generate the output. The downside of this approach is
that we go through several steps to get the results we want:
1. We sort all the addresses by Zip Code:
xsl:sort select=zip
2. We store the current
zip
elements value in the variable
lastZip
:
xsl:variable name=lastZip select=zip
3. For each
zip
element, we look at all of its preceding siblings to see if this is the first time weve encountered this particular value stored in
lastZip
. If it is, there wont be any preceding siblings that match.
xsl:if test=notpreceding-sibling::address[zip=lastZip]
4. If this is the first time weve encountered this value in the
zip
element, we go back and reselect all
address
elements with
zip
children that match this value. Once we have that group, we sort them by first name within last name and print each
address.
5. xsl:for-each select=addressbookaddress[zip=lastZip] 6. xsl:sort select=namelast-name
xsl:sort select=namefirst-name
So, weve found a way to get the results we want, but its really inefficient. We sort the data, then we look at each Zip Code in sorted order, then see if weve encountered that value before
in document order, then we reselect all the items that match the current Zip Code and resort them before we write them out. Whew Theres got to be a better way, right? Well, since
were not at the end of the chapter, its a safe bet well find a better way in the next section. Read on....
6.2.4 The xsl:key Approach
In this section, well look at using
xsl:key
to group items in an XML document. This approach is commonly referred to as the Muench method, after Oracle XML Evangelist
and OReilly author Steve Muench, who first suggested this technique. The Muench method has three steps:
1. Define a
key
for the property we want to use for grouping. 2. Select all of the nodes we want to group. Well do some tricks with the
key
and
generate-id
functions to find the unique grouping values. 3. For each unique grouping value, use the
key
function to retrieve all nodes that match it. Because the
key
function returns a node-set, we can do further sorts on the set of nodes that match any given grouping value.
Well, thats how the technique works—lets start building the stylesheet that makes the magic happen. The first step, creating a key function, is easy. Heres how it looks:
xsl:key name=zipcodes match=address use=zip
This
xsl:key
element defines a new index called
zipcodes
. It indexes
address
elements based on the value of the
zip
element they contain.
page 117
Now that weve defined our
key
, were ready for the complicated part. We use the
key
and
generate-id
functions together. Heres the syntax, which well discuss extensively in a minute:
xsl:for-each select=address[generate-id.= generate-idkeyzipcodes, zip[1]]
Okay, lets take a deep, cleansing breath and start digging through this syntax. What were selecting here is all
address
elements in which the automatically generated
id
matches the automatically generated
id
of the first node returned by the
key
function when we ask for all
address
elements that match the current
zip
element. Well, thats clear as crystal, isnt it? Let me try to explain that again from a slightly different
perspective. For each
address
, we use the
key
function to retrieve all
address
es that have the same
zip
. We then take the first node from that node-set. Finally, we use the
generate-id
function to generate an
id
for both nodes. If the two generated
id
s are identical, then the two nodes are the same.
Whew. Let me catch my breath. If this
address
matches the first node returned by the
key
function, then we know weve found the first
address
that matches this grouping value. Selecting all of the first values remember, our previous predicate ends with
[1]
gives us a node-set of some number of
address
elements, each of which contains one of the unique grouping values we need. Well, thats how this technique works. At this point, weve got a way to generate a node-set
that contains all of the unique grouping values; now we need to process those nodes. From this point, well do several things, all of which are comparatively simple:
•
Sort all nodes based on the grouping property. In this example, the property is the
zip
element. We start by selecting the first occurrence of every unique
zip
element in the document, then we sort those
zip
elements. Heres how it looks in the stylesheet:
xsl:for-each select=address[generate-id.=generate-idkeyzipcodes, zip[1]]
xsl:sort select=zip
•
The outer
xsl:for-each
element selects all the unique values of the
zip
element. Next, we use the
key
function to retrieve all
address
elements that match the current
zip
element:
xsl:for-each select=keyzipcodes, zip
•
The
key
function gives us a node-set of all matching
address
elements. We sort that node-set based on the
last-name
and
first-name
elements, then process them in turn:
xsl:sort select=namelast-name xsl:sort select=namefirst-name
tr xsl:if test=position = 1
td valign=center bgcolor=999999 xsl:attribute name=rowspan
xsl:value-of select=countkeyzipcodes, zip xsl:attribute
b xsl:textZip code xsl:textxsl:value-of select=zip
b