Grouping with XSLT

I've been working on integrating FriendFeed into this site. For those who are unfamiliar with FriendFeed, it aggregates your activities across the social web and gathers them into a single feed, to which your friends can subscribe. It's a nifty utility with a wide range of supported services, from Twitter to Flickr to Disqus to Digg.

FriendFeed provides a full REST API that can provide results in JSON, XML, RSS, and Atom. Since my implementation doesn't require any client-side interactivity, I decided to use XML, transform it with XSLT on the server, and display the result in the page. The provided XML consists of a <feed> full of <entry>'s. Each entry has details such as the posted date, service (Flicker, Twitter, etc), and title.

One of my presentation requirements is that the items be grouped by date. I wanted to shw and "post" for each day, with each of the days entries beneath it. I found a good method that provides exactly what I need.

The crux of the functionality is using XSLT keys. Keys are essentially like database indexes, allowing you to quickly select without repeatedly traversing all of the nodes. You define a key as follows:

<xsl:key name="name-of-key" match="nodes-to-apply" use="value-to-index" /> 

The name is simply the string you want to use to refer to this key later. The match is the node you want to apply this key to. The use can be any child element, attribute, or computed value (using a function) defining what you want to be indexed.

In my case, I wanted to use the <updated> element, which containes the time and date the item was last updated. Since I was going to be grouping on the date and not the time, I used the Microsoft-specific time formatting function to truncate the time and format the date as a number:

<xsl:key name="entries-by-date" match="/feed/entry" use="ms:format-date(updated, 'yyyyMMdd')" /> 

Now I've defined a key called "entries-by-date" that will quickly pull up all matching <entry> elements for a given date in the format "20080614". The next step is to iterate through all of the dates and show the corresponding entries. Unfortunately, we can't just iterate through the key we created. Instead, we have to sort the <entry> elements by their formatted <updated> element (which we created the key for), then grab all of the other <entry>'s with the same key. We want to be sure to grab each <entry> only once. Here's a larger snippet showing how to acomplish this:

<!-- Root -->
<xsl:template match="/feed">
 
    <!-- Group by date -->
    <xsl:for-each select="entry[count(. | key('entries-by-date', ms:format-date(updated, 'yyyyMMdd'))[1]) = 1]">
 
        <!-- Sort by date -->
        <xsl:sort select="updated" data-type="number" order="descending" />
    
        <!-- Show date groups -->
        <xsl:call-template name="dateGrouping" />
 
    </xsl:for-each>
 
</xsl:template>

The magic happens in the for-each. First, we take the current node and the first node from the key (aka index) with the same calculated date value. We create a set with these two nodes using the "|" operator. If the nodes are different, we'll get a set containing two nodes. If they are the same, there will only be one node in the set. We then check the number of nodes in the set using the count() function. If the count is one, we use it in the for-each; otherwise, we skip it. In the end, this gives us the first entry each date.

After we have our target date to group by, we want to display each <entry> with the same date. In my stylesheet, I implemented it in the "dateGrouping" template. The actual code is pretty easy:

<xsl:template name="dateGrouping">
    <!-- You could output the date header here -->
    <xsl:for-each select="key('entries-by-date', ms:format-date(updated, 'yyyyMMdd'))">
        <xsl:apply-templates select="." />
    </xsl:for-each>
</xsl:template>

We use the key() function to get every node in the given key with the matching value. We then apply the formatting to each node to get our final result.