-
Define a key for the property we want to use for grouping.
-
Select all of the nodes we want to group. We'll do some tricks with the key() and generate-id() functions to find the unique grouping values.
-
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, that's how the technique works -- let's start building the stylesheet that makes the magic happen. The first step, creating a key function, is easy. Here's 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.
Now that we've defined our key, we're ready for the complicated part. We use the key() and generate-id() functions together. Here's the syntax, which we'll discuss extensively in a minute:
<xsl:for-each select="//address[generate-id(.)=
generate-id(key('zipcodes', zip)[1])]">
Okay, let's take a deep, cleansing breath and start digging through this syntax. What we're 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, that's clear as crystal, isn't 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 ids 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 we've 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, that's how this technique works. At this point, we've 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, we'll 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. Here's how it looks in the stylesheet:
<xsl:for-each
select="//address[generate-id(.)=generate-id(key('zipcodes', 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="key('zipcodes', 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="name/last-name"/>
<xsl:sort select="name/first-name"/>
<tr>
<xsl:if test="position() = 1">
<td valign="center" bgcolor="#999999">
<xsl:attribute name="rowspan">
<xsl:value-of select="count(key('zipcodes', zip))"/>
</xsl:attribute>
<b>
<xsl:text>Zip code </xsl:text><xsl:value-of select="zip"/>
</b>
</td>
</xsl:if>
<td align="right">
<xsl:value-of select="name/first-name"/>
<xsl:text> </xsl:text>
<b><xsl:value-of select="name/last-name"/></b>
</td>
<td>
<xsl:value-of select="street"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="city"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="state"/>
<xsl:text> </xsl:text>
<xsl:value-of select="zip"/>
</td>
</tr>
</xsl:for-each>
</xsl:for-each>
We generate a table cell that contains the Zip Code common to all addresses, creating a rowspan attribute based on the number of matches for the current Zip Code. From there, we write the other data items into table cells.
Here's our complete stylesheet:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="no"/>
<xsl:key name="zipcodes" match="address" use="zip"/>
<xsl:template match="/">
<table border="1">
<xsl:for-each select="//address[generate-id(.)=
generate-id(key('zipcodes', zip)[1])]">
<xsl:sort select="zip"/>
<xsl:for-each select="key('zipcodes', zip)">
<xsl:sort select="name/last-name"/>
<xsl:sort select="name/first-name"/>
<tr>
<xsl:if test="position() = 1">
<td valign="center" bgcolor="#999999">
<xsl:attribute name="rowspan">
<xsl:value-of select="count(key('zipcodes', zip))"/>
</xsl:attribute>
<b>
<xsl:text>Zip code </xsl:text><xsl:value-of select="zip"/>
</b>
</td>
</xsl:if>
<td align="right">
<xsl:value-of select="name/first-name"/>
<xsl:text> </xsl:text>
<b><xsl:value-of select="name/last-name"/></b>
</td>
<td>
<xsl:value-of select="street"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="city"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="state"/>
<xsl:text> </xsl:text>
<xsl:value-of select="zip"/>
</td>
</tr>
</xsl:for-each>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>