Search code examples
xslt-1.0xslt-grouping

XSLT 1.0 grouping on one or multiple levels


Today's challenge was grouping in XSLT 1.0. Found out there are something called keys and the Muenchian grouping.

Input XML:

<Items>
    <Item>
        <ID>1</ID>
        <Name>A</Name>
        <Country>Sweden</Country>
        <Region>Småland</Region>
    </Item>
    <Item>
        <ID>2</ID>
        <Name>B</Name>
        <Country>Sweden</Country>
        <Region>Norrland</Region>
    </Item>
    <Item>
        <ID>3</ID>
        <Name>C</Name>
        <Country>USA</Country>
        <Region>Alaska</Region>
    </Item>
    <Item>
        <ID>4</ID>
        <Name>D</Name>
        <Country>USA</Country>
        <Region>Texas</Region>
    </Item>
    <Item>
        <ID>5</ID>
        <Name>E</Name>
        <Country>Sweden</Country>
        <Region>Norrland</Region>
    </Item>
</Items>

I need to make thins XML into a better structure, and from this sample XML I't like to get items structured by country and region. Below is wanted result where country and region gets sorted as well:

<Items>
  <Country Name="Sweden">
    <Region Name="Norrland">
      <Item>
        <ID>2</ID>
        <Name>B</Name>
      </Item>
      <Item>
        <ID>5</ID>
        <Name>E</Name>
      </Item>
    </Region>
    <Region Name="Småland">
      <Item>
        <ID>1</ID>
        <Name>A</Name>
      </Item>
    </Region>
  </Country>
  <Country Name="USA">
    <Region Name="Alaska">
      <Item>
        <ID>3</ID>
        <Name>C</Name>
      </Item>
    </Region>
    <Region Name="Texas">
      <Item>
        <ID>4</ID>
        <Name>D</Name>
      </Item>
    </Region>
  </Country>
</Items>

EDIT:

I also want to make sure regions end up in their own country, even if there are duplicates. I edited the answer accordingly.

Also, I'd like to hint about xsltfiddle.liberty-development.net as an easy way of doing trial-and-error XSLT development...


Solution

  • Inspired by this article, I found a neat solution to this problem:

    I have included comments for using it for single or double grouping, see comments in the code. Notice how I use first key (index) as input to the secon for-each loop:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0" 
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output indent="yes"/>
      <xsl:strip-space elements="*"/>
    
      <xsl:key name="country" match="Item" use="Country" />
      <xsl:key name="region" match="Item" use="concat(Region, '|', Country)" />
    
      <xsl:template match="/Items">
        <Items>
          <xsl:for-each select="Item[generate-id(.) = generate-id(key('country', Country))]">
            <xsl:sort select="Country" />
            <xsl:variable name="_country" select="Country" />
    
            <xsl:element name="Country">
              <xsl:attribute name="Name"><xsl:value-of select="$_country" /></xsl:attribute>
    
              <!-- single level grouping -->
              <!--<xsl:apply-templates select="key('country', Country)" />-->
    
    
              <!-- double grouping -->
              <!-- START -->
              <xsl:for-each select="key('country', Country)[generate-id(.) = generate-id(key('region', concat(Region, '|', Country)))]">
                  <xsl:sort select="Region" />
                  <xsl:variable name="_region" select="Region" />
    
                  <xsl:element name="Region">
                    <xsl:attribute name="Name"><xsl:value-of select="$_region" /></xsl:attribute>
    
                    <xsl:apply-templates select="key('region', concat(Region, '|', Country))" />
                  </xsl:element>
              </xsl:for-each>
              <!-- END -->
    
            </xsl:element>    
          </xsl:for-each>
        </Items>
      </xsl:template>
    
      <xsl:template match="Item">
        <xsl:element name="Item">
          <xsl:element name="ID"><xsl:value-of select="ID" /></xsl:element>
          <xsl:element name="Name"><xsl:value-of select="Name" /></xsl:element>
        </xsl:element>
      </xsl:template>
    </xsl:stylesheet>