Fabio
Fabio

Reputation: 59

XSLT - Merge nodes into collection

I've the following source data:

<DATA>
  <ITEM>
     <ID>28</ID>
     <BRAND>TGR</BRAND>
     <ACC_NO>143080</ACC_NO>
     <ACC_DESCR>account description</ACC_DESCR>
  </ITEM>
  <ITEM>
     <ID>28</ID>
     <BRAND>TGR</BRAND>
     <ACC_NO>272180</ACC_NO>
     <ACC_DESCR>account description</ACC_DESCR>
  </ITEM>
  <ITEM>
     <ID>32</ID>
     <BRAND>TGR</BRAND>
     <ACC_NO>159880</ACC_NO>
     <ACC_DESCR>account description</ACC_DESCR>
  </ITEM>
</DATA>

Which i want to transform into:

<DATA>
  <ITEM>
     <ID>28</ID>
     <BRAND>TGR</BRAND>
     <ACCOUNTS>
       <ACCOUNT>
         <ACC_NO>143080</ACC_NO>
         <ACC_DESCR>account description</ACC_DESCR>
       <ACCOUNT>
       <ACCOUNT>
         <ACC_NO>272180</ACC_NO>
         <ACC_DESCR>account description</ACC_DESCR>
       <ACCOUNT>
     <ACCOUNTS>
  </ITEM>
  <ITEM>
     <ID>32</ID>
     <BRAND>TGR</BRAND>
     <ACCOUNTS>
       <ACCOUNT>
         <ACC_NO>159880</ACC_NO>
         <ACC_DESCR>account description</ACC_DESCR>
       <ACCOUNT>
     <ACCOUNTS>
  </ITEM>
</DATA>    

Applying the following transformation:

<xsl:template match="/">
   <xsl:for-each select="/DATA/ITEM">
      <ITEM>
        <ID>
          <xsl:value-of select="ID"/>
        </ID>
        <BRAND>
          <xsl:value-of select="BRAND"/>
        <BRAND>
        <ACCOUNTS>
          <ACCOUNT>
            <AccountNumber>
              <xsl:value-of select="ACC_NO"/>
            <AccountNumber>
            <AccountDescription>
              <xsl:value-of select="ACC_DESCR"/>
            <AccountDescription>
          </ACCOUNT>
        </ACCOUNTS>
      </ITEM>
    </xsl:for-each>
</xsl:template>

I will get ACCOUNTS list inside ITEM, but still not merging ITEM's with same ID. Basically i'll get always a collection with a single element, instead of all the accounts under same ID.

Any suggestion how can i do it and produce the desired output?

Thanks

Upvotes: 1

Views: 873

Answers (1)

Ian Roberts
Ian Roberts

Reputation: 122364

In XSLT 2.0 you simply replace the outer for-each with a for-each-group and then add an inner for-each to create one ACCOUNT per group member:

<xsl:template match="/">
   <xsl:for-each-group select="/DATA/ITEM" group-by="ID">
      <ITEM>
        <ID>
          <xsl:value-of select="ID"/>
        </ID>
        <BRAND>
          <xsl:value-of select="BRAND"/>
        <BRAND>
        <ACCOUNTS>
          <xsl:for-each select="current-group()">
            <ACCOUNT>
              <AccountNumber>
                <xsl:value-of select="ACC_NO"/>
              <AccountNumber>
              <AccountDescription>
                <xsl:value-of select="ACC_DESCR"/>
              <AccountDescription>
            </ACCOUNT>
          </xsl:for-each>
        </ACCOUNTS>
      </ITEM>
    </xsl:for-each-group>
</xsl:template>

In 1.0 the most efficient approach is a technique known as Muenchian grouping - define a key giving the grouping criteria and then use a trick with generate-id to pull out just the first instance of a given ID as a proxy for the group as a whole.

<xsl:key name="itemById" match="ITEM" use="ID" />

<xsl:template match="/">
   <xsl:for-each select="/DATA/ITEM[
          generate-id() = generate-id(key('itemById', ID)[1])]">
      <ITEM>
        <ID>
          <xsl:value-of select="ID"/>
        </ID>
        <BRAND>
          <xsl:value-of select="BRAND"/>
        <BRAND>
        <ACCOUNTS>
          <xsl:for-each select="key('itemById', ID)">
            <ACCOUNT>
              <AccountNumber>
                <xsl:value-of select="ACC_NO"/>
              <AccountNumber>
              <AccountDescription>
                <xsl:value-of select="ACC_DESCR"/>
              <AccountDescription>
            </ACCOUNT>
          </xsl:for-each>
        </ACCOUNTS>
      </ITEM>
    </xsl:for-each>
</xsl:template>

key('itemById', ID)[1] gives you the first ITEM element which has the same ID value as the current one we're testing, and then comparing the generate-id values of the two allows you to determine whether or not they are the same node.

(Personally I would approach the problem using templates rather than for-each but the principle remains the same)

Upvotes: 1

Related Questions