tags:

views:

572

answers:

3

I have an xml file like this:

<root>
    <item>
        <name>one</name>
        <status>good</status>
    </item>
    <item>
        <name>two</name>
        <status>good</status>
    </item>
    <item>
        <name>three</name>
        <status>bad</status>
    </item>
    <item>
        <name>four</name>
        <status>ugly</status>
    </item>
    <item>
        <name>five</name>
        <status>bad</status>
    </item>
</root>

I want to transform this using XSLT to get something like:

<root>
    <items><status>good</status>
        <name>one</name>
        <name>two</name>
    </items>
    <items><status>bad</status>
        <name>three</name>
        <name>five</name>
    </items>
    <items><status>ugly</status>
        <name>four</name>
    </items>
</root>

In other words, I get a list of items, each with a status, and I want to turn it into a list of statuses, each with a list of items.

My initial thought was to do apply-templates matching each status type in turn, but that means I have to know the complete list of statuses. Is there a better way to do it?

Thanks for any help.

A: 

It depends about your xslt engine. If you're using xslt 1.0 without any extension, then your approach is certainly the best.

On the other side, if you're allowed to use exslt (especially the node-set extension) or xslt 2.0, then you could do it in a more generic way:

  1. Collect all the available statuses
  2. Create a node-set from the obtained result
  3. Iterating on this node set, create your pivot by filtering you status base on the current element in your iteration.

But before doing that, consider that it may be overkill if you only have a few set of statuses and that adding another status is quite rare.

gizmo
+5  A: 

Muench to the rescue!

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

    <xsl:output method="xml" indent="yes" encoding="UTF-8"/>

    <xsl:key name="muench" match="/root/item/status" use="."/>

    <xsl:template match="/">
     <root>
     <xsl:for-each select="/root/item/status[generate-id() = generate-id(key('muench',.)[1])]">
      <xsl:call-template name="pivot">
       <xsl:with-param name="status" select="."/>
      </xsl:call-template>
     </xsl:for-each>
     </root>
    </xsl:template>

    <xsl:template name="pivot">
     <xsl:param name="status"/>
     <items>
      <status><xsl:value-of select="$status"/></status>
      <xsl:for-each select="/root/item[status=$status]">
       <name><xsl:value-of select="name"/></name>
      </xsl:for-each>
     </items>
    </xsl:template>

</xsl:stylesheet>
annakata
thank you thank you. that's fantastic. You've really helped me understand a whole new set of things I can do in xslt. :)
AJ
+3  A: 

Yes, this can be done in XSLT 1.0

<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="kStatByVal" 
         match="status" use="."/>
    <!--                                   -->
    <xsl:key name="kItemByStat" 
         match="item" use="status"/>
    <!--                                   -->
    <xsl:variable name="vDoc" select="/"/>

    <xsl:template match="/">
      <top>
        <xsl:for-each select=
        "/*/*/status[generate-id()
                    =
                     generate-id(key('kStatByVal',.)[1])
                    ]">
          <items>
            <status><xsl:value-of select="."/></status>
          <xsl:for-each select="key('kItemByStat', .)">
            <xsl:copy-of select="name"/>
          </xsl:for-each>
          </items>
        </xsl:for-each>
      </top>
    </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the original XML document:

<root>
    <item>
        <name>one</name>
        <status>good</status>
    </item>
    <item>
        <name>two</name>
        <status>good</status>
    </item>
    <item>
        <name>three</name>
        <status>bad</status>
    </item>
    <item>
        <name>four</name>
        <status>ugly</status>
    </item>
    <item>
        <name>five</name>
        <status>bad</status>
    </item>
</root>

The wanted result is produced:

<top>
    <items>
     <status>good</status>
     <name>one</name>
     <name>two</name>
    </items>
    <items>
     <status>bad</status>
     <name>three</name>
     <name>five</name>
    </items>
    <items>
     <status>ugly</status>
     <name>four</name>
    </items>
</top>

Do note the use of:

  1. The Muenchian method for grouping

  2. The use of <xsl:key> and the key() function

Dimitre Novatchev
thanks. and thanks for pointing out the bits to pay attention to. I need to read up on Muenchian grouping.
AJ
@AL - You are welcome :) I have edited the reply with links for Muenchian grouping, <xsl:key> and the key() function. Cheers,
Dimitre Novatchev