A Brute-Force Approach Grouping with xsl:variable

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