3

I have an XML similar to this:

<Test>
  <grapes>
     <a>TypeA</a>
     <b>value1</b>
  </grapes>
  <oranges>
     <a>TypeB</a>
     <b>value2</b>
  </oranges>
  <apples>
    <a>TypeA</a>
     <b>value3</b>
  </apples>
</Test>

where the values are unique but the Type, might be the same.

I am trying to sort it so that the output is similar to this:

<group type="TypeA">
  <value v="value1" />
  <value v="value3" />
</group>
<group type="TypeB">
  <value v="value2" />
</group>

I am having a hard time making sure the groups are unique in the output and the values are in the right group.

How should my XSL be structured?

deken
  • 387
  • 2
  • 3
  • 12
  • 1
    Since I didn't see grouping in the title, I'd just like to clarify: you want to group _and then_ sort? – Brian Sep 27 '11 at 20:37
  • Very useful question (and great answers). I agree title should be rephrased, since this is a common issue, and the answers would be of interest to many if they could better spot this question is theirs. – Alain BECKER Sep 21 '12 at 14:03

3 Answers3

2

Here is a much simpler solution (completely "push style", no <xsl:for-each>, no nesting, no <xsl:variable>, no current(), , no //, no axes):

<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="kGoodsByType" match="/*/*" use="a"/>

 <xsl:template match=
  "/*/*[generate-id()
       =
        generate-id(key('kGoodsByType', a)[1])
       ]
  ">
     <group type="{a}">
       <xsl:apply-templates select="key('kGoodsByType', a)/b"/>
     </group>
 </xsl:template>

 <xsl:template match="b">
  <value v="{.}"/>
 </xsl:template>

 <xsl:template match="*/* | text()" priority="-1"/>
</xsl:stylesheet>

when this transformation is applied on the provided XML document:

<Test>
    <grapes>
        <a>TypeA</a>
        <b>value1</b>
    </grapes>
    <oranges>
        <a>TypeB</a>
        <b>value2</b>
    </oranges>
    <apples>
        <a>TypeA</a>
        <b>value3</b>
    </apples>
</Test>

the wanted, correct result is produced:

<group type="TypeA">
   <value v="value1"/>
   <value v="value3"/>
</group>
<group type="TypeB">
   <value v="value2"/>
</group>

Explanation: Muenchian grouping of /*/* using as key the string values of their a children.

II. 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="/*">
  <xsl:for-each-group select="*/a" group-by=".">
   <group type="{current-grouping-key()}">
     <xsl:sequence select="current-group()/../b"/>
   </group>
  </xsl:for-each-group>
 </xsl:template>
</xsl:stylesheet>

When this transformation is performed on the same XML document (above), the same correct result is produced:

<group type="TypeA">
   <b>value1</b>
   <b>value3</b>
</group>
<group type="TypeB">
   <b>value2</b>
</group>

Explanation:

  1. <xsl:for-each-group>

  2. current-group()

  3. current-grouping-key()

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
1

XSLT 1.0 :

You start by creating unique groups for your types using the muenchian method. Google it to find out what it is. Then it's just a matter of iterating through them and printint out what you want, how you want it :

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="types" match="a" use="text()"/>
<xsl:template match="/">
<result>
  <xsl:for-each select="//a[generate-id(.) = generate-id(key('types', text())[1])]">
    <group type="{current()/text()}">
      <xsl:for-each select="//a[text() = current()/text()]">
        <xsl:variable name="values" select="following-sibling::b | preceding-sibling::b"/>
        <xsl:for-each select="$values">
          <value v="{current()}"/>
        </xsl:for-each>
      </xsl:for-each>
    </group>
  </xsl:for-each>
</result>
</xsl:template>
</xsl:stylesheet>

You will find that the output is identical to what you expect.

FailedDev
  • 26,680
  • 9
  • 53
  • 73
  • 1
    Make sure you understand what is written here. If you have any questions feel free to ask. – FailedDev Sep 27 '11 at 21:17
  • Can you explain what is happening here exactly? //a[generate-id(.) = generate-id(key('types', text())[1])] – deken Oct 05 '11 at 14:45
  • Basically you generate unique id's for all a elements based on their text value and you then iterate through them. – FailedDev Oct 05 '11 at 14:58
  • Ahh ok, one more thing, why would this be needed: if you are already looping though each that match the group name. – deken Oct 05 '11 at 15:03
  • Because once I had the groups I need to get all the siblings and iterate through them. In this case your siblings are named b but I didn't know that e.g. b will always follow a or vice-versa so this axis simply selects all b elements after and before the a element. – FailedDev Oct 05 '11 at 15:08
0

My proposal is slightly different from FailedDev's:

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

  <xsl:template match="/Test">
    <root>
      <xsl:for-each select="*[a != following::a]">
        <xsl:sort select="a" data-type="text" />
        <group type="{a}">
          <xsl:for-each select="/Test/*[a = current()/a]">
            <xsl:sort select="b" data-type="text" />
            <value v="{b}" />
          </xsl:for-each>
        </group>
      </xsl:for-each>
    </root>
  </xsl:template>
</xsl:stylesheet>

The outer

<xsl:for-each select="*[a != following::a]" />

select all unique types, ie. TypeA and TypeB. The

<xsl:sort select="a" data-type="text" />

sorts these according to name, making sure TypeA will appear above TypeB. The inner

<xsl:for-each select="/Test/*[a = current()/a]" />

selects a list of unique values for each type, ie. for TypeA the values value1 and value3 are extracted. Again, the resulting list is sorted to list value1 before value3.

Malibak
  • 17
  • 3
  • Quite wrong -- don't use the `!=` operator when one of the operands is a node-set. -1. Also, always run/test your proposed solution, before posting! – Dimitre Novatchev Sep 28 '11 at 01:27
  • This is tested seems to work without problems. Although I will agree your solution with Muenchian grouping is probably the way to go. – Malibak Sep 28 '11 at 13:49
  • 2
    @_Mace: Your answer is wrong -- you were just lucky that it "works" on the provided XML document. Try it with this XML document and see that it produces *two groups* for "TypeA": ` TypeA value1 TypeB value2 TypeA value3 TypeB value4 ` – Dimitre Novatchev Sep 28 '11 at 16:06