tags:

views:

201

answers:

3

I have the following XML:

<option>
    <title>ABC</title>
    <desc>123</desc>
</option>
<option>
    <title>ABC</title>
    <desc>12345</desc>
</option>
<option>
    <title>ABC</title>
    <desc>123</desc>
</option>
<option>
    <title>EFG</title>
    <desc>123</desc>
</option>
<option>
    <title>EFG</title>
    <desc>456</desc>
</option>

Using XSLT, I want to transform it into:

<choice>
    <title>ABC</title>
    <desc>123</desc>
    <desc>12345</desc>
</choice>
<choice>
    <title>EFG</title>
    <desc>123</desc>
    <desc>456</desc>
</choice>
+2  A: 

I would suggest looking into "grouping" to solve this problem. Either the built-in grouping functions of XSLT 2.0, like for-each-group, or, if you're using XSLT 1, the technique called "Muenchian Grouping".

Matt Gibson
This has solved an issue i've had for like 3 hours, thanks heaps, for-each-group worked great.
AJ
+1 Good answer.
markusk
+1  A: 

Here is a minimal XSLT 2.0 solution:

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

 <xsl:template match="/*">
  <choices>
   <xsl:for-each-group select="*/title" group-by=".">
     <choice>
       <title>
         <xsl:sequence select="current-grouping-key()"/>
       </title>
       <xsl:for-each-group select="current-group()/../desc" group-by=".">
         <xsl:sequence select="."/>
       </xsl:for-each-group>
     </choice>
   </xsl:for-each-group>
  </choices>
 </xsl:template>
</xsl:stylesheet>

Do note the use of the functions current-group() and current-grouping-key()

Dimitre Novatchev
+1 Good answer.
markusk
@Dimitre, could you elaborate on `<xsl:sequence ... />` a bit, what's it purpuse? I have never used it and never had a feeling that I am missing something, but perhaps using it could have saved me some trouble?
Patrick
@Patrick: `<xsl:sequence>` is similar to `<xsl:copy-of>`, however it doesn't create copies of the nodes selected in its `"select"` attribute. Rather it produces something like "references" to these nodes. To summarize, `<xsl:sequence>` is more economical and efficient than `<xsl:copy-of>`
Dimitre Novatchev
@Dimitre, thanks!
Patrick
A: 

You've gotten good answers already. In the chase of brevity, I present my 16 line solution, based on Dimitres answer:

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

  <xsl:template match="/*">
    <choices>
      <xsl:for-each-group select="option" group-by="title">
        <choice>
          <xsl:sequence select="title"/>
          <xsl:for-each-group select="current-group()/desc" group-by=".">
            <xsl:sequence select="."/>
          </xsl:for-each-group>
        </choice>
      </xsl:for-each-group>
    </choices>
  </xsl:template>
</xsl:stylesheet>

Note that the current context node inside for-each-group is the first item in the current group, while current-group() returns the list of all items in the current group. I exploit the fact that the titleelement is identical for input and output, and copy the first title from each group.

And for completeness, the XSLT 1.0 solution using Muenchian grouping (20 lines):

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

  <xsl:key name="title" match="option/title" use="."/>
  <xsl:key name="desc" match="option/desc" use="."/>

  <xsl:template match="/*">
    <choices>
      <xsl:for-each select="option/title[count(.|key('title',.)[1]) = 1]">
        <choice>
          <xsl:copy-of select="."/>
          <xsl:for-each select="key('title',.)/../desc
              [count(.|key('desc', .)[../title=current()][1]) = 1]">
            <xsl:copy-of select="."/>
          </xsl:for-each>
        </choice>
      </xsl:for-each>
    </choices>
  </xsl:template>
</xsl:stylesheet>
markusk