Question

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.

Was it helpful?

Solution

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

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

OTHER TIPS

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

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.

In XSLT 2.0 you can replace the muenchian grouping by its standard grouping mechanism. Applied to the given answer the xslt would look like:

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

    <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-group select="/root/item/status" group-by="key('muench', .)">
            <xsl:call-template name="pivot">
                <xsl:with-param name="status" select="."/>
            </xsl:call-template>
        </xsl:for-each-group>
        </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>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top