Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Grouping xml nodes by value of a child in Xsl

Tags:

xml

xslt

xpath

<root>
<element>
<id>1</id>
<group>first</group>
</element>

<element>
<id>2</id>
<group>second</group>
</element>


<element>
<id>3</id>
<group>first</group>
</element>
...
<root>

How I can group my elements by the group name in xslt 1.0. the output:

<root>
<group name="first">
 <element>
    <id>1</id>
    <group>first</group>
 </element>
 <element>
    <id>3</id>
    <group>first</group>
 </element>
</group>
<group name="second">
 <element>
    <id>2</id>
    <group>second</group>
    </element>
</group>
</root>

Any ideas?

like image 669
Harold Sota Avatar asked Jan 17 '11 07:01

Harold Sota


3 Answers

This is a job for Muenchian Grouping. You will numerous examples of it within the XSLT tag here on StackOverflow.

First, you need to define a key to help you group the groups

<xsl:key name="groups" match="group" use="."/>

This will look up group elements for a given group name.

Next, you need to match all the occurrences of the first instance of each distince group name. This is done with this scary looking statement

<xsl:apply-templates select="element/group[generate-id() = generate-id(key('groups', .)[1])]"/>

i.e Match group elements which happen to be the first occurence of that element in our key.

When you have matched the distinct group nodes, you can then loop through all other group nodes with the same name (where $currentGroup is a variable holding the current group name)

<xsl:for-each select="key('groups', $currentGroup)">

Putting this altogether gives

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

   <xsl:key name="groups" match="group" use="."/>

   <xsl:template match="/root">
      <root>
         <xsl:apply-templates select="element/group[generate-id() = generate-id(key('groups', .)[1])]"/>
      </root>
   </xsl:template>

   <xsl:template match="group">
      <xsl:variable name="currentGroup" select="."/>
      <group>
         <xsl:attribute name="name">
            <xsl:value-of select="$currentGroup"/>
         </xsl:attribute>
         <xsl:for-each select="key('groups', $currentGroup)">
            <element>
               <id>
                  <xsl:value-of select="../id"/>
               </id>
               <name>
                  <xsl:value-of select="$currentGroup"/>
               </name>
            </element>
         </xsl:for-each>
      </group>
   </xsl:template>

</xsl:stylesheet>

Applying this on your sample XML gives the following result

<root>
   <group name="first">
      <element>
         <id>1</id>
         <name>first</name>
      </element>
      <element>
         <id>3</id>
         <name>first</name>
      </element>
   </group>
   <group name="seccond">
      <element>
         <id>2</id>
         <name>seccond</name>
      </element>
   </group>
</root>
like image 83
Tim C Avatar answered Nov 14 '22 09:11

Tim C


I. Here is a complete and very short XSLT 1.0 solution:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 
 <xsl:key name="kElsByGroup" match="element" use="group"/>
 
 <xsl:template match="/*">
  <root>
   <xsl:apply-templates/>
  </root>
 </xsl:template>

 <xsl:template match=
   "element[generate-id()=generate-id(key('kElsByGroup',group)[1])]">

  <group name="{group}">
   <xsl:copy-of select="key('kElsByGroup',group)"/>
  </group>
 </xsl:template>
 
 <xsl:template match=
   "element[not(generate-id()=generate-id(key('kElsByGroup',group)[1]))]"/>

</xsl:stylesheet>

when this transformation is applied on the provided XML document:

<root>
    <element>
        <id>1</id>
        <group>first</group>
    </element>
    <element>
        <id>2</id>
        <group>second</group>
    </element>
    <element>
        <id>3</id>
        <group>first</group>
    </element>
</root>

the wanted, correct result is produced:

<root>
    <group name="first"><element>
        <id>1</id>
        <group>first</group>
    </element><element>
        <id>3</id>
        <group>first</group>
    </element></group>
    <group name="second"><element>
        <id>2</id>
        <group>second</group>
    </element></group>
</root>

Do note:

  1. The use of the Muenchian method for grouping. This is the most efficient grouping method in XSLT 1.0.

  2. The use of AVT (Attribute Value Template) to specify a literal result element and its variable - value attribute as one whole. Using AVTs simplifies coding and yields shorter and more understandable code.

II. An even shorter XSLT 2.0 solution:

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

 <xsl:template match="/*">
     <root>
      <xsl:for-each-group select="element" group-by="group">
       <group name="{current-grouping-key()}">
        <xsl:copy-of select="current-group()"/>
       </group>
      </xsl:for-each-group>
     </root>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the same XML document (above), the same correct result is again produced.

Do note:

.1. The use of the <xsl:for-each-group> XSLT 2.0 instruction.

.2. The use of the standard XSLT 2.0 functions current-group() and current-grouping-key()

like image 20
Dimitre Novatchev Avatar answered Nov 14 '22 09:11

Dimitre Novatchev


<xsl:template match="group[not(.=preceding::group)]">
  <xsl:variable name="current-group" select="." />
  <xsl:for-each select="//root/element[group=$current-group]">
    <group>
      <id><xsl:value-of select="id"/></id>
    </group>
  </xsl:for-each>
</xsl:template>
like image 43
lweller Avatar answered Nov 14 '22 08:11

lweller