Search code examples
xmlxsltxslt-1.0xslt-grouping

XSLT 1.0 grouping by multiple elements


I came through various answers regarding this topic but I couldn't find a solution so far.

I have input XML with the structure like this:

<RootNode>
  <record>
    <KEYELEMENT1>001</KEYELEMENT1>
    <KEYELEMENT2>ABC</KEYELEMENT2>
    <KEYELEMENT3>EFG</KEYELEMENT3>
    <HEADELEMENT1>HEAD11</HEADELEMENT1>
    <HEADELEMENT2>HEAD12</HEADELEMENT2>
    <HEADELEMENT3>HEAD13</HEADELEMENT3>
    <HEADELEMENT4>HEAD14</HEADELEMENT4>
    <ITEMELEMENT1>ITEM11</ITEMELEMENT1>
    <ITEMELEMENT2>ITEM21</ITEMELEMENT2>
    <ITEMELEMENT3>ITEM31</ITEMELEMENT3>
    <ITEMELEMENT4>ITEM41</ITEMELEMENT4>
  </record>
  <record>
    <KEYELEMENT1>001</KEYELEMENT1>
    <KEYELEMENT2>ABC</KEYELEMENT2>
    <KEYELEMENT3>EFG</KEYELEMENT3>
    <HEADELEMENT1>HEAD11</HEADELEMENT1>
    <HEADELEMENT2>HEAD12</HEADELEMENT2>
    <HEADELEMENT3>HEAD13</HEADELEMENT3>
    <HEADELEMENT4>HEAD14</HEADELEMENT4>
    <ITEMELEMENT1>ITEM21</ITEMELEMENT1>
    <ITEMELEMENT2>ITEM22</ITEMELEMENT2>
    <ITEMELEMENT3>ITEM23</ITEMELEMENT3>
    <ITEMELEMENT4>ITEM24</ITEMELEMENT4>
  </record>
  <record>
    <KEYELEMENT1>001</KEYELEMENT1>
    <KEYELEMENT2>ABD</KEYELEMENT2>
    <KEYELEMENT3>EFG</KEYELEMENT3>
    <HEADELEMENT1>HEAD21</HEADELEMENT1>
    <HEADELEMENT2>HEAD22</HEADELEMENT2>
    <HEADELEMENT3>HEAD23</HEADELEMENT3>
    <HEADELEMENT4>HEAD24</HEADELEMENT4>
    <ITEMELEMENT1>ITEM31</ITEMELEMENT1>
    <ITEMELEMENT2>ITEM32</ITEMELEMENT2>
    <ITEMELEMENT3>ITEM33</ITEMELEMENT3>
    <ITEMELEMENT4>ITEM34</ITEMELEMENT4>
  </record>
  <record>
    <KEYELEMENT1>002</KEYELEMENT1>
    <KEYELEMENT2>ABC</KEYELEMENT2>
    <KEYELEMENT3>EFG</KEYELEMENT3>
    <HEADELEMENT1>HEAD31</HEADELEMENT1>
    <HEADELEMENT2>HEAD32</HEADELEMENT2>
    <HEADELEMENT3>HEAD33</HEADELEMENT3>
    <HEADELEMENT4>HEAD34</HEADELEMENT4>
    <ITEMELEMENT1>ITEM41</ITEMELEMENT1>
    <ITEMELEMENT2>ITEM42</ITEMELEMENT2>
    <ITEMELEMENT3>ITEM43</ITEMELEMENT3>
    <ITEMELEMENT4>ITEM44</ITEMELEMENT4>
  </record>
  <record>
    <KEYELEMENT1>001</KEYELEMENT1>
    <KEYELEMENT2>ABC</KEYELEMENT2>
    <KEYELEMENT3>EFG</KEYELEMENT3>
    <HEADELEMENT1>HEAD11</HEADELEMENT1>
    <HEADELEMENT2>HEAD12</HEADELEMENT2>
    <HEADELEMENT3>HEAD13</HEADELEMENT3>
    <HEADELEMENT4>HEAD14</HEADELEMENT4>
    <ITEMELEMENT1>ITEM51</ITEMELEMENT1>
    <ITEMELEMENT2>ITEM52</ITEMELEMENT2>
    <ITEMELEMENT3>ITEM53</ITEMELEMENT3>
    <ITEMELEMENT4>ITEM54</ITEMELEMENT4>
  </record>
</RootNode>

The result of the transformation should look like this:

<ResultXml>
  <record>
    <header>
      <KEYELEMENT1>001</KEYELEMENT1>
      <KEYELEMENT2>ABC</KEYELEMENT2>
      <KEYELEMENT3>EFG</KEYELEMENT3>
      <HEADELEMENT1>HEAD11</HEADELEMENT1>
      <HEADELEMENT2>HEAD12</HEADELEMENT2>
      <HEADELEMENT3>HEAD13</HEADELEMENT3>
      <HEADELEMENT4>HEAD14</HEADELEMENT4>
    </header>
    <item>
      <ITEMELEMENT1>ITEM11</ITEMELEMENT1>
      <ITEMELEMENT2>ITEM21</ITEMELEMENT2>
      <ITEMELEMENT3>ITEM31</ITEMELEMENT3>
      <ITEMELEMENT4>ITEM41</ITEMELEMENT4>       
    </item>     
    <item>
      <ITEMELEMENT1>ITEM21</ITEMELEMENT1>
      <ITEMELEMENT2>ITEM22</ITEMELEMENT2>
      <ITEMELEMENT3>ITEM23</ITEMELEMENT3>
      <ITEMELEMENT4>ITEM24</ITEMELEMENT4>       
    </item>     
    <item>
      <ITEMELEMENT1>ITEM51</ITEMELEMENT1>
      <ITEMELEMENT2>ITEM52</ITEMELEMENT2>
      <ITEMELEMENT3>ITEM53</ITEMELEMENT3>
      <ITEMELEMENT4>ITEM54</ITEMELEMENT4>       
    </item>
  </record>
  <record>
    <header>
      <KEYELEMENT1>001</KEYELEMENT1>
      <KEYELEMENT2>ABD</KEYELEMENT2>
      <KEYELEMENT3>EFG</KEYELEMENT3>
      <HEADELEMENT1>HEAD21</HEADELEMENT1>
      <HEADELEMENT2>HEAD22</HEADELEMENT2>
      <HEADELEMENT3>HEAD23</HEADELEMENT3>
      <HEADELEMENT4>HEAD24</HEADELEMENT4>
    </header>
    <item>
      <ITEMELEMENT1>ITEM31</ITEMELEMENT1>
      <ITEMELEMENT2>ITEM32</ITEMELEMENT2>
      <ITEMELEMENT3>ITEM33</ITEMELEMENT3>
      <ITEMELEMENT4>ITEM34</ITEMELEMENT4>       
    </item>
  </record>
  <record>
    <header>
      <KEYELEMENT1>002</KEYELEMENT1>
      <KEYELEMENT2>ABC</KEYELEMENT2>
      <KEYELEMENT3>EFG</KEYELEMENT3>
      <HEADELEMENT1>HEAD31</HEADELEMENT1>
      <HEADELEMENT2>HEAD32</HEADELEMENT2>
      <HEADELEMENT3>HEAD33</HEADELEMENT3>
      <HEADELEMENT4>HEAD34</HEADELEMENT4>
    </header>
    <item>
      <ITEMELEMENT1>ITEM41</ITEMELEMENT1>
      <ITEMELEMENT2>ITEM42</ITEMELEMENT2>
      <ITEMELEMENT3>ITEM43</ITEMELEMENT3>
      <ITEMELEMENT4>ITEM44</ITEMELEMENT4>       
    </item>
  </record>   
</ResultXml>

For each distinct values in KEYELEMENT1, KEYELEMENT2, and KEYELEMENT3 I have to create one record in the result. Other header fields are the same and are transformed to header element with the key fields. Items should be mapped under the record with the same keys.

I tried the Muenchian method with something like this:

    <xsl:key name="keyfields" match="record" use="concat(KEYELEMENT1, '|', KEYELEMENT2, '|', KEYELEMENT3)"/>

<xsl:template match="/">
    <ResultXml>
        <xsl:apply-templates select="record[generate-id() = generate-id(key('keyfields',concat(KEYELEMENT1, '|', KEYELEMENT2, '|', KEYELEMENT3))[1])]" mode="header"/>
    </ResultXml>
</xsl:template>

<xsl:template match="record" mode="header">
    <record>
        <header>
            <KEYELEMENT1><xsl:value-of select="KEYELEMENT1"/></KEYELEMENT1>
            <KEYELEMENT2><xsl:value-of select="KEYELEMENT2"/></KEYELEMENT2>
            <KEYELEMENT3><xsl:value-of select="KEYELEMENT3"/></KEYELEMENT3>
            <HEADELEMENT1><xsl:value-of select="HEADELEMENT1"/></HEADELEMENT1>
            <HEADELEMENT2><xsl:value-of select="HEADELEMENT2"/></HEADELEMENT2>
            <HEADELEMENT3><xsl:value-of select="HEADELEMENT3"/></HEADELEMENT3>
            <HEADELEMENT4><xsl:value-of select="HEADELEMENT4"/></HEADELEMENT4>          
        </header>
    </record>       
</xsl:template>

But I am not able to produce even header records. Any help would be appreciated.


Solution

  • Muenchiann grouping in XSLT 1.0 requires a bit of ordered approach and careful usage of a number of grouping idioms.

    We must start from creation of a key, to group records, in this case on KEYELEMENT1 / ...2 / ...3.

    Then the main template (matching RootNode) applies "group" template to the first record from each group.

    The "group" template for record:

    • prints opening record tag,
    • prints head element filled with KEY... and HEAD... source elements,
    • calls "normal" template to all members of the current group,
    • and finally, prints closing record tag.

    The "normal" template for record prints item element filled with ITEM... source elements.

    And the last thing you need is the identity template.

    So the whole script looks like below:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="xml" indent="yes"/>
      <xsl:key name="recs" match="record"
        use="concat(KEYELEMENT1, '|', KEYELEMENT2, '|', KEYELEMENT3)"/>
    
      <xsl:template match="RootNode">
        <ResultXml>
          <!-- Apply "group" template to the first record in group -->
          <xsl:apply-templates select="record[generate-id() = generate-id(
            key('recs', concat(KEYELEMENT1, '|', KEYELEMENT2, '|', KEYELEMENT3))
            [1])]" mode="group"/>
        </ResultXml>
      </xsl:template>
    
      <!-- "Group" template for record -->
      <xsl:template match="record" mode="group">
        <record>
          <head>
            <xsl:copy-of select="*[starts-with(name(), 'KEY') or starts-with(name(), 'HEAD')]"/>
          </head>
          <!-- Apply "normal" template to all members of the current group -->
          <xsl:apply-templates select="key('recs',
            concat(KEYELEMENT1, '|', KEYELEMENT2, '|', KEYELEMENT3))"/>
        </record>
      </xsl:template>
    
      <!-- "Normal" template for record -->
      <xsl:template match="record">
        <item>
          <xsl:copy-of select="*[starts-with(name(), 'ITEM')]"/>
        </item>
      </xsl:template>
    
      <xsl:template match="@*|node()">
        <xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy>
      </xsl:template>
    </xsl:stylesheet>
    

    For a working example see http://xsltfiddle.liberty-development.net/6qM2e2k