Re: [xsl] 2 level Grouping through attributes

Subject: Re: [xsl] 2 level Grouping through attributes
From: Jeni Tennison <jeni@xxxxxxxxxxxxxxxx>
Date: Mon, 11 Feb 2002 19:09:42 +0000
Ronald Heller wrote:
> Try the following:
> <?xml version='1.0'?>
> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>
> <xsl:output method="xml" indent="yes"/>
>     <xsl:key name="level1" match="XML_OUTPUT/@UserID" use="." />
>
>     <xsl:key name="level2" match="XML_OUTPUT/@CategoryID" use="." />
>
>     <xsl:template match="/">
>        <ROOT>
>           <xsl:for-each select="//XML_OUTPUT[generate-id(@UserID) = 
> generate-id(key('level1', @UserID)[1])]">
>              <xsl:variable name="user">
>                 <xsl:value-of select="@UserID" />
>              </xsl:variable>
>
>              <xsl:comment>
>                 <xsl:value-of select="generate-id(@UserID)" />
>              </xsl:comment>
>
>              <USER ID="{$user}">
>                 <xsl:for-each select="//XML_OUTPUT[generate-id(@CategoryID) 
> = generate-id(key('level2', @CategoryID)[1]) and @UserID=$user]">
>                    <xsl:variable name="category">
>                       <xsl:value-of select="@CategoryID" />
>                    </xsl:variable>
>
>                    <CATEGORY ID="{$category}" TITLE="{@CategoryTitle}">
>                       <xsl:for-each 
> select="//XML_OUTPUT[@CategoryID=$category and @UserID=$user]">
>                          <CONTENT_ITEM ID="{@ContentItemID}" 
> TITLE="{@ItemTitle}">
>                          </CONTENT_ITEM>
>                       </xsl:for-each>
>                    </CATEGORY>
>                 </xsl:for-each>
>              </USER>
>           </xsl:for-each>
>        </ROOT>
>     </xsl:template>
> </xsl:stylesheet>

Unfortunately this runs into the classic trap with multi-level
grouping using the Muenchian Method when identifying the unique values
at the second level. See:

> <xsl:for-each select="//XML_OUTPUT[generate-id(@CategoryID)
> = generate-id(key('level2', @CategoryID)[1]) and @UserID=$user]">

Here, you go through all the XML_OUTPUT elements in the document, and
then identify those that:

  (a) are the first in the document with that particular CategoryID;
  and
  (b) have the UserID equal to $user

This will miss out XML_OUTPUT elements that have the same user ID, but
*aren't* the first in the document with the particular category ID. So
you won't pick up everything in the document.

To do two-level grouping using the Muenchian method, the key for the
second level has to incorporate information from the first level. I'd
use the keys:

<xsl:key name="level1" match="XML_OUTPUT" use="@UserID" />
<xsl:key name="level2" match="XML_OUTPUT"
         use="concat(@UserID, '+', @CategoryID)" />

This has two advantages: it gives you the right answer, and it means
you can use the 'level2' key to find all the XML_OUTPUT elements with
a particular user and category, which is a lot more efficient than
searching through the entire document again.
         
Then:

<xsl:template match="/">
  <ROOT>
    <xsl:for-each
      select="//XML_OUTPUT[generate-id() =
                           generate-id(key('level1', @UserID)[1])]">
      <xsl:variable name="user" select="@UserID" />

      <xsl:comment>
        <xsl:value-of select="generate-id(@UserID)" />
      </xsl:comment>

      <USER ID="{$user}">
        <xsl:for-each
          select="key('level1', $user)
                    [generate-id() =
                     generate-id(key('level2',
                                     concat($user, '+', @CategoryID)[1])]">
          <xsl:variable name="category" select="@CategoryID" />

          <CATEGORY ID="{$category}" TITLE="{@CategoryTitle}">
            <xsl:for-each select="key('level2',
                                      concat($user, '+', $category)">
              <CONTENT_ITEM ID="{@ContentItemID}" TITLE="{@ItemTitle}">
              </CONTENT_ITEM>
            </xsl:for-each>
          </CATEGORY>
        </xsl:for-each>
      </USER>
    </xsl:for-each>
  </ROOT>
</xsl:template>

---

The XSLT 2.0 solution is very similar:

<xsl:template match="/">
  <ROOT>
    <xsl:for-each-group select="//XML_OUTPUT" group-by="@UserID">
      <xsl:variable name="user" select="@UserID" />

      <xsl:comment>
        <xsl:value-of select="generate-id(@UserID)" />
      </xsl:comment>

      <USER ID="{$user}">
        <xsl:for-each-group select="current-group()"
                            group-by="@CategoryID">
          <xsl:variable name="category" select="@CategoryID" />

          <CATEGORY ID="{$category}" TITLE="{@CategoryTitle}">
            <xsl:for-each select="current-group()">
              <CONTENT_ITEM ID="{@ContentItemID}" TITLE="{@ItemTitle}">
              </CONTENT_ITEM>
            </xsl:for-each>
          </CATEGORY>
        </xsl:for-each>
      </USER>
    </xsl:for-each>
  </ROOT>
</xsl:template>

Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread