Pavel Suba
Pavel Suba

Reputation: 13

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.

Upvotes: 1

Views: 1948

Answers (2)

Valdi_Bo
Valdi_Bo

Reputation: 31011

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

Upvotes: 1

Daniel Haley
Daniel Haley

Reputation: 52888

If you change your first template match from / to /* (or RootNode) you'll get your groups correctly.

After that it's just a matter of grouping the items into sets of 4. One way to do that is by using mod in a predicate.

Here's an example. I use count() instead of generate-id() in my Muenchian grouping, but it can be done either way.

Example...

XML Input

<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>

XSLT 1.0

<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="keyfields" match="record"
    use="concat(KEYELEMENT1,'|',KEYELEMENT2,'|',KEYELEMENT3)"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="/*">
    <ResultXml>
      <xsl:for-each
        select="record[count(.|key('keyfields',concat(KEYELEMENT1,'|',KEYELEMENT2,'|',KEYELEMENT3))[1])=1]">
        <record>
          <header>
            <xsl:apply-templates
              select="*[starts-with(local-name(), 'KEYELEMENT') or starts-with(local-name(), 'HEADELEMENT')]"/>
          </header>
          <xsl:for-each select="key('keyfields',concat(KEYELEMENT1,'|',KEYELEMENT2,'|',KEYELEMENT3))/*[starts-with(local-name(),'ITEMELEMENT')][position() mod 4 = 1]">
            <item>
              <xsl:apply-templates 
                select=".|following-sibling::*[starts-with(local-name(),'ITEMELEMENT')][position() >= 1 and 4 > position()]"/>
            </item>
          </xsl:for-each>
        </record>
      </xsl:for-each>
    </ResultXml>
  </xsl:template>

</xsl:stylesheet>

Output

<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>

Fiddle: http://xsltfiddle.liberty-development.net/3Nqn5Yi

Upvotes: 1

Related Questions