Grouping / distinct values

Often its needed to group a set of nodes using node properties etc. A similar problem is getting all distinct values of a set of values, e.g. all months represented in a set of dates.

Generally the best overall approach is using xsl keys and id's, not least due to performance issues - this is known as the Muenchian Method. Keys are mapping nodes to a specific key value that can be used in selecting and grouping nodes. Id's are unique strings identifying a node.

A basic example of using keys and id's is adding a key for all nodes, using the month-part (substring(@updateDate, 9,2)) as group-separator/id. This example displays the unique updatedate-months for nodes in the system:

<xsl:key name="date" match="node" use="substring(@updateDate, 9,2)"/>
...

<xsl:for-each select="$currentPage/node[generate-id() = generate-id(key('date', substring(@updateDate, 9,2))[1])]">
  <xsl:value-of select="substring(@updateDate, 9,2)"/><br/>
</xsl:for-each>

 

Deciphering keys and id's

One thing to note is, that the use-clause dictates how the key-string should be generated for all nodes matching the match-clause. In the upper example a "date"-key is generated for all "node"-nodes in the current document using the "substring(@updateDate, 9,2)" key-string, thus all "node"-nodes updated in March will have key "03". Since this is global to the document, a key('date', '08') function call is also global to the document, and will search the full document for nodes matching the '08'-key.

But lets go back a bit, and start with a simpler example.

Say we have a set of strings:

January
February
March
January
December
January
February
April
April

and we want to find and list the distinct strings only:

January
February
March
December
April

We need a key definition to match that, something like this:
    For all content nodes of type DateNode their key should be the node-name  (e.g. January, February etc.)

So our key definition would look like this:

<xsl:key  name="MyKey" match="DateNode" use="@nodeName"/>


Now the key is in place. Next step is using it, and to iterate the nodes to find and output the distinct string we can do like this:

<xsl:for-each select="$currentPage/DateNode[generate-id() = generate-id(key('MyKey', @nodeName)[1])]">
    <xsl:value-of select="@nodeName"/>
</xsl:for-each>

This needs a bit of explanation. The xslt function key(keyname, usestring) returns the set of nodes that are included in the key keyname and that matches usestring as their key. E.g. key("MyKey", "January") would return the set of all DateNodes in the document with @nodeName = "January".

KeysAndIdsExplained_3

One important lesson to learn from this is, that if you want to limit the nodes searched you need to elaborate a bit on the key. Obviously you can change the match-clause to be more specific using normal xslt syntax. In Umbraco you may want to search e.g. subnodes of some specific node only, and unfortunately you cannot use variable- or parameter references in key match- and use-clauses. Thus, instead you can add context specific info to the generated key, e.g. add the node-id, parent node-id or whatever you like.

In this example the distinct updateDate-months of the currentPage subnodes are written - the key definition adds parent node-id's to the key, which in the for-each loop matches the $currentPage:

<xsl:key name="date" match="node" use="concat(../@id,substring(@updateDate, 9,2))"/>
...

<xsl:for-each select="$currentPage/node[generate-id() = generate-id(key('date', concat($currentPage/@id,substring(@updateDate, 9,2)))[1])]">
  <xsl:value-of select="substring(@updateDate, 9,2)"/><br/>
</xsl:for-each>

 

One final example

This snippets displays the distinct months of a set of 4 subnodes with a document type date-field (here SomeDate)

The idea is:

  1. Define some special key/function used for grouping the nodes - in your case it's simply using the month part of a date property called "SomeDate", but it could be less/more advanced. It's a bit similar to a hash-function. All nodes with the same key value defines a group.
  2. Find the first node of every group. This is done by comparing unique node-id's (fetched by generate-id() with the first node-id of the group). This is the outer for-each
  3. Iterate over all nodes in a group using the key() function for fetching all nodes with a specific value. The Key() function returns a node-set.

 

<xsl:param name="currentPage"/>
<!-- This is the key definition using the month of the SomeDate documenttype field
     - all nodes with the same month get same key id-->
<xsl:key name="months" match="node" use="umbraco.library:FormatDateTime(data[@alias='SomeDate'], 'MMMM')"/>

<xsl:template match="/">

  <!-- Iterate over all nodes with an id similar to the first in every group -->
  <xsl:for-each select="$currentPage/node[generate-id() = generate-id(key('months', umbraco.library:FormatDateTime(data[@alias='SomeDate'], 'MMMM'))[1])]">
    <h5><xsl:value-of select="umbraco.library:FormatDateTime(data[@alias='SomeDate'], 'MMMM')"/></h5>
      <!-- Iterate over all nodes in a group, i.e. with the same key -->
      <xsl:for-each select="key('months',umbraco.library:FormatDateTime(data[@alias='SomeDate'], 'MMMM'))">
        <xsl:sort select="data[@alias='SomeDate']" />
        <xsl:value-of select="umbraco.library:LongDate(data[@alias='SomeDate'])"/> :
        <xsl:value-of select="@nodeName" /><br />
      </xsl:for-each>
  </xsl:for-each>

</xsl:template>

 The result looks like this:

marts
1. marts 2009 : DateTest1
23. marts 2009 : DateTest3
april
9. april 2009 : DateTest2
juni
9. juni 2009 : DateTest4

 

 

Additional samples

The previous example only shows the concept of using keys and id's, leaving out e.g. functionality for handling nodes from multiple years. Here is the previous example enhanced to handle years also by Scott Hugh Alexander Petersen:

<xsl:param name="currentPage"/>

<!-- This is the key definitions using the year and month of the the node createDate property - all nodes with the same month get same month-key id, all nodes with the same year get the same year-key id --> <xsl:key name="years" match="node" use="umbraco.library:FormatDateTime(@createDate, 'yyyy')"/> <xsl:key name="months" match="node" use="umbraco.library:FormatDateTime(@createDate, 'yyyy-MM')"/> <xsl:template match="/"> <ul> <!-- Iterate over all nodes with a year-id similar to the first in every group --> <xsl:for-each select="$currentPage/ancestor-or-self::node [ (generate-id() = generate-id(key('years', Exslt.ExsltDatesAndTimes:year(@createDate))[1]))]"> <xsl:sort select="@createDate" order="descending" /> <li> <xsl:value-of select="Exslt.ExsltDatesAndTimes:year(@createDate)" /> <ul> <xsl:variable name="months" select="key('years', Exslt.ExsltDatesAndTimes:year(@createDate))" /> <!-- Iterate over all nodes with a month-id similar to the first in every group --> <xsl:for-each select="$months[generate-id() = generate-id(key('months',umbraco.library:FormatDateTime(@createDate, 'yyyy-MM'))[1])]"> <xsl:sort select="@createDate" order="ascending" /> <li> <xsl:value-of select="umbraco.library:FormatDateTime(@createDate, 'MM')"/> <ul> <!-- Iterate over all nodes in a group, i.e. with the same months-key --> <xsl:for-each select="key('months',umbraco.library:FormatDateTime(@createDate, 'yyyy-MM'))"> <xsl:sort select="@createDate" order="ascending" /> <li> <xsl:value-of select="umbraco.library:LongDate(@createDate)"/> : <xsl:value-of select="@nodeName" /> </li> </xsl:for-each> </ul> </li> </xsl:for-each> </ul> </li> </xsl:for-each> </ul> </xsl:template>

 

 


Comments:

1. juli 2009:

I have used above code in my xslt. but i am not getting month june and january in my list

Commented by: Dapran

11. august 2009:

I did like this (please feel free to update your post, I will soon post it on my blog and put the link here): >xsl:param name="currentPage"/< >xsl:variable name="StartFromLevel" select="$currentPage/ancestor-or-self::node [@nodeTypeAlias='BlogFrontpage']/@level" /< >xsl:key name="years" match="node" use="umbraco.library:FormatDateTime(@createDate, 'yyyy')"/< >xsl:key name="months" match="node" use="umbraco.library:FormatDateTime(@createDate, 'yyyy-MM')"/< >xsl:template match="/"< >ul< >!-- Iterate over all nodes with an id similar to the first in every group --< >xsl:for-each select="$currentPage/ancestor-or-self::node [@level=$StartFromLevel]//node [(generate-id() = generate-id(key('years', Exslt.ExsltDatesAndTimes:year(@createDate))[1]))]"< >xsl:sort select="@createDate" order="descending" /< >li< >xsl:value-of select="Exslt.ExsltDatesAndTimes:year(@createDate)" /< >ul< >xsl:variable name="months" select="key('years', Exslt.ExsltDatesAndTimes:year(@createDate))" /< >xsl:for-each select="$months[generate-id() = generate-id(key('months',umbraco.library:FormatDateTime(@createDate, 'yyyy-MM'))[1])]"< >xsl:sort select="@createDate" order="ascending" /< >li< >xsl:value-of select="umbraco.library:FormatDateTime(@createDate, 'MM')"/< >ul< >!-- Iterate over all nodes in a group, i.e. with the same key --< >xsl:for-each select="key('months',umbraco.library:FormatDateTime(@createDate, 'yyyy-MM'))"< >xsl:sort select="@createDate" order="ascending" /< >li< >xsl:value-of select="umbraco.library:LongDate(@createDate)"/< : >xsl:value-of select="@nodeName" /< >/li< >/xsl:for-each< >/ul< >/li< >/xsl:for-each< >/ul< >/li< >/xsl:for-each< >/ul< >/xsl:template< >/xsl:stylesheet<

Commented by: Scott Hugh Alexandar Petersen

11. august 2009:

My bad switched out > and < in code so here it is correctly: <xsl:param name="currentPage"/> <xsl:key name="years" match="node" use="umbraco.library:FormatDateTime(@createDate, 'yyyy')"/> <xsl:key name="months" match="node" use="umbraco.library:FormatDateTime(@createDate, 'yyyy-MM')"/> <xsl:template match="/"> <ul> <!-- Iterate over all nodes with an id similar to the first in every group --> <xsl:for-each select="$currentPage/ancestor-or-self::node [ (generate-id() = generate-id(key('years', Exslt.ExsltDatesAndTimes:year(@createDate))[1]))]"> <xsl:sort select="@createDate" order="descending" /> <li> <xsl:value-of select="Exslt.ExsltDatesAndTimes:year(@createDate)" /> <ul> <xsl:variable name="months" select="key('years', Exslt.ExsltDatesAndTimes:year(@createDate))" /> <xsl:for-each select="$months[generate-id() = generate-id(key('months',umbraco.library:FormatDateTime(@createDate, 'yyyy-MM'))[1])]"> <xsl:sort select="@createDate" order="ascending" /> <li> <xsl:value-of select="umbraco.library:FormatDateTime(@createDate, 'MM')"/> <ul> <!-- Iterate over all nodes in a group, i.e. with the same key --> <xsl:for-each select="key('months',umbraco.library:FormatDateTime(@createDate, 'yyyy-MM'))"> <xsl:sort select="@createDate" order="ascending" /> <li> <xsl:value-of select="umbraco.library:LongDate(@createDate)"/> : <xsl:value-of select="@nodeName" /> </li> </xsl:for-each> </ul> </li> </xsl:for-each> </ul> </li> </xsl:for-each> </ul> </xsl:template>

Commented by: Scott Hugh Alexandar Petersen