None
None

Reputation: 5670

Group XML elements by attribute

I have a variable named filt holding an xml like this:

<filters>
  <ISP_WebItem FILTER="Farve" FILTERNAME="Transparent" UNITCODE="" />
  <ISP_WebItem FILTER="Antal" FILTERNAME="10" UNITCODE="mapper" />
  <ISP_WebItem FILTER="Indpakning" FILTERNAME="Æske" UNITCODE="" />
  <ISP_WebItem FILTER="Materiale" FILTERNAME="PP" UNITCODE="" />
  <ISP_WebItem FILTER="Bredde" FILTERNAME="35.6" UNITCODE="cm" />
  <ISP_WebItem FILTER="Farve" FILTERNAME="blue" UNITCODE="" />
  <ISP_WebItem FILTER="Dybde" FILTERNAME="5" UNITCODE="mm" />
  <ISP_WebItem FILTER="Farve" FILTERNAME="red" UNITCODE="" />
</filters>

I want to group these elements by its 'FILTER' attribute. That is I want an output xml like this (please note that xml elements are rearranged by filter atribute i.e. all elements with FILTER as farve is at adjacent position now)

<filters>
  <ISP_WebItem FILTER="Farve" FILTERNAME="Transparent" UNITCODE="" />
  <ISP_WebItem FILTER="Farve" FILTERNAME="blue" UNITCODE="" />
  <ISP_WebItem FILTER="Farve" FILTERNAME="red" UNITCODE="" />
  <ISP_WebItem FILTER="Antal" FILTERNAME="10" UNITCODE="mapper" />
  <ISP_WebItem FILTER="Indpakning" FILTERNAME="Æske" UNITCODE="" />
  <ISP_WebItem FILTER="Materiale" FILTERNAME="PP" UNITCODE="" />
  <ISP_WebItem FILTER="Bredde" FILTERNAME="35.6" UNITCODE="cm" />
  <ISP_WebItem FILTER="Dybde" FILTERNAME="5" UNITCODE="mm" />
</filters>

I have tried something like this:

  <xsl:variable name="grouped_filt" select="$filt//ISP_WebItem[@FILTER = preceding-sibling::*[1]/@FILTER ]"></xsl:variable>

but of no use. I am unable to find anything wrong in this. Can anyone help?

Upvotes: 0

Views: 146

Answers (1)

Ian Roberts
Ian Roberts

Reputation: 122364

The standard approach to this in XSLT 1.0 is called Muenchian Grouping:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:strip-space elements="*"/>
  <xsl:output method="xml" indent="yes" />
  <xsl:key name="itemByFilter" match="ISP_WebItem" use="@FILTER" />

  <xsl:template match="filters">
    <filters>
      <xsl:apply-templates select="ISP_WebItem[
          generate-id() = generate-id(key('itemByFilter', @FILTER)[1])]" />
    </filters>
  </xsl:template>

  <xsl:template match="ISP_WebItem">
    <xsl:copy-of select="key('itemByFilter', @FILTER)" />
  </xsl:template>
</xsl:stylesheet>

The generate-id() = generate-id(key('itemByFilter', @FILTER)[1]) is a way to select just the first ISP_WebItem element with each FILTER value and apply the ISP_WebItem template to that element. In the template we then copy all elements that have the same FILTER value.

Edit:

You say the <filters> element is in a "variable named filt" rather than being something you're matching directly from the input document. In that case, you can use the same key definition

  <xsl:key name="itemByFilter" match="ISP_WebItem" use="@FILTER" />

but instead of <xsl:template match="filters"> you use <xsl:for-each select="$filt/filters">. If the filt variable is a result tree fragment rather than a node set - i.e. it was created as

<xsl:variable name="filt">
  <filters>
    <!-- ... -->
  </filters>
</xsl:variable>

the you will need an extension function to turn it back into a node set

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
                xmlns:exslt="http://exslt.org/common"
                exclude-result-prefixes="exslt">
  <xsl:strip-space elements="*"/>
  <xsl:output method="xml" indent="yes" />
  <xsl:key name="itemByFilter" match="ISP_WebItem" use="@FILTER" />


  <xsl:variable name="filt">
    <filters>
      <ISP_WebItem FILTER="Farve" FILTERNAME="Transparent" UNITCODE="" />
      <ISP_WebItem FILTER="Antal" FILTERNAME="10" UNITCODE="mapper" />
      <ISP_WebItem FILTER="Farve" FILTERNAME="blue" UNITCODE="" />
    </filters>
  </xsl:variable>

  <xsl:variable name="grouped_filt">
    <xsl:for-each select="exslt:node-set($filt)/filters">
      <filters>
        <xsl:apply-templates select="ISP_WebItem[
            generate-id() = generate-id(key('itemByFilter', @FILTER)[1])]" />
      </filters>
    </xsl:for-each>
  </xsl:variable>

  <xsl:template match="/">
    <!-- Demonstrate that the grouping did the right thing -->
    <xsl:copy-of select="$grouped_filt" />
  </xsl:template>

  <xsl:template match="ISP_WebItem">
    <xsl:copy-of select="key('itemByFilter', @FILTER)" />
  </xsl:template>
</xsl:stylesheet>

This style sheet, when run over any input document (e.g. <foo/>) will output

<?xml version="1.0"?>
<filters>
  <ISP_WebItem FILTER="Farve" FILTERNAME="Transparent" UNITCODE=""/>
  <ISP_WebItem FILTER="Farve" FILTERNAME="blue" UNITCODE=""/>
  <ISP_WebItem FILTER="Antal" FILTERNAME="10" UNITCODE="mapper"/>
</filters>

Upvotes: 1

Related Questions