Grouping with xslt and child nodes

Tag: xslt , grouping Author: laoxu1979091 Date: 2012-08-26

I have tried some of the examples here, but I still can't get the correct output. I haven't worked much with XSLT before. I want to group on the "Detail" element and get all the "Data" elements matching that group as children to the "Detail" element.

Example:

input

<?xml version="1.0" encoding="utf-8"?>
<File>
  <Detail type="A" group="1" >
    <Data>
      <Nr>1</Nr>
    </Data>
    <Data>
      <Nr>2</Nr>
    </Data>
  </Detail>
  <Detail type="B" group="1">
    <Data>
      <Nr>3</Nr>
    </Data>
    <Data>
      <Nr>4</Nr>
    </Data>
  </Detail>
  <Detail type="B" group="2">
    <Data>
      <Nr>5</Nr>
    </Data>
  </Detail>
  <Detail type="A" group="1">
    <Data>
      <Nr>6</Nr>
    </Data>
  </Detail>
</File>

Wanted output ("Detail type="A" group="1"> elements grouped together and all the elements matching that group as children)

<?xml version="1.0" encoding="utf-8"?>
<File>
  <Detail type="A" group="1" >
    <Data>
      <Nr>1</Nr>
    </Data>
    <Data>
      <Nr>2</Nr>
    </Data>
    <Data>
      <Nr>6</Nr>
    </Data>
  </Detail>
  <Detail type="B" group="1">
    <Data>
      <Nr>3</Nr>
    </Data>
    <Data>
      <Nr>4</Nr>
    </Data>
  </Detail>
  <Detail type="B" group="2">
    <Data>
      <Nr>5</Nr>
    </Data>
  </Detail>
</File>

Thanks for help :)

Best Answer

NOTE: This question languished in the bin for 6 hours before I took it up. My answer languished in the bin for two hours before someone else disguised some non-essential comments as an answer.


Study up on Muenchian grouping. It's handy for these grouping problems.

The heavy lifters are <xsl:key>, which creates a key based on a concat of @type and @group, and this line here, <xsl:for-each select="File/Detail[count(. | key('details', concat(@type,'_',@group))[1]) = 1]">, which isolates the first occurrence of the Detail node having a particular key and by extension a particular pairing of @group and @type.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
     <xsl:key name="details" match="Detail" 
             use="concat(@type,'_',@group)"/>
    <xsl:template match='/'>
      <File>
        <xsl:for-each select="File/Detail[count(. | key('details', concat(@type,'_',@group))[1]) = 1]">
          <xsl:sort select="concat(@type,'_',@group)" />
          <Detail type="[email protected]}" group="[email protected]}">
            <xsl:for-each select="key('details', concat(@type,'_',@group))">
              <xsl:copy-of select="Data"/>  
           </xsl:for-each>
          </Detail>
       </xsl:for-each>
     </File>
   </xsl:template>   
</xsl:stylesheet>

comments:

Thanks for help and explanation, and for being the first person that took up my question. Both answers helped me understand how xslt grouping works.
@user1663498 Glad to help. I understand it better now, too. Thanks for approving my answer.

Other Answer1

I. XSLT 1.0

Here's a solution that makes use of push-oriented templates, rather than <xsl:for-each> (which is normally a more reusable approach).

When this XSLT:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
  <xsl:output omit-xml-declaration="no" indent="yes" />
  <xsl:strip-space elements="*" />

  <xsl:key name="kDetailAttr" match="Detail" use="concat(@type, '+', @group)" />

  <xsl:template match="/*">
    <File>
      <xsl:apply-templates select="Detail[generate-id() = generate-id(key('kDetailAttr', concat(@type, '+', @group))[1])]" />
    </File>
  </xsl:template>

  <xsl:template match="Detail">
    <Detail type="[email protected]}" group="[email protected]}">
      <xsl:copy-of select="key('kDetailAttr', concat(@type, '+', @group))/Data" />
    </Detail>
  </xsl:template>

</xsl:stylesheet>

...is applied to the original XML:

<?xml version="1.0" encoding="UTF-8"?>
<File>
  <Detail type="A" group="1">
    <Data>
      <Nr>1</Nr>
    </Data>
    <Data>
      <Nr>2</Nr>
    </Data>
  </Detail>
  <Detail type="B" group="1">
    <Data>
      <Nr>3</Nr>
    </Data>
    <Data>
      <Nr>4</Nr>
    </Data>
  </Detail>
  <Detail type="B" group="2">
    <Data>
      <Nr>5</Nr>
    </Data>
  </Detail>
  <Detail type="A" group="1">
    <Data>
      <Nr>6</Nr>
    </Data>
  </Detail>
</File>

...the wanted result is produced:

<?xml version="1.0" encoding="utf-8"?>
<File>
  <Detail type="A" group="1">
    <Data>
      <Nr>1</Nr>
    </Data>
    <Data>
      <Nr>2</Nr>
    </Data>
    <Data>
      <Nr>6</Nr>
    </Data>
  </Detail>
  <Detail type="B" group="1">
    <Data>
      <Nr>3</Nr>
    </Data>
    <Data>
      <Nr>4</Nr>
    </Data>
  </Detail>
  <Detail type="B" group="2">
    <Data>
      <Nr>5</Nr>
    </Data>
  </Detail>
</File>

Explanation:

  • By properly applying the Muenchian Grouping Method (which is necessary in XSLT 1.0, given that it does not have any "inline" grouping mechanism), we can find unique nodes and group their descendants.

II. XSLT 2.0

When this XSLT:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
  <xsl:output omit-xml-declaration="no" indent="yes" />
  <xsl:strip-space elements="*" />

  <xsl:template match="/*">
    <File>
      <xsl:for-each-group select="Detail"
      group-by="concat(@type, '+', @group)">
        <Detail type="[email protected]}" group="[email protected]}">
          <xsl:copy-of select="current-group()/Data" />
        </Detail>
      </xsl:for-each-group>
    </File>
  </xsl:template>
</xsl:stylesheet>

...is applied to the original XML, the same correct result is produced.

Explanation:

  • By properly applying XSLT 2.0's for-each-group element, we can arrive at the same result.