Skip to content. | Skip to navigation

Personal tools

Navigation

You are here: Home / weblog / Muenchian groupings in XSLT

Muenchian groupings in XSLT

Posted by Dominic Cronin at May 13, 2007 08:35 PM |
Filed under:

On Friday, a colleage at work asked me how to do something in XSLT. After a minute or so humming and hah'ing I said I'd have to think about it for a while. As it turned out, it took most of the weekend. (OK - as a parent of 2 under-3s, the amount of free time in a weekend is limited, but still, this was a tricky problem.)


The source XML document was something like this:

<list>
    <item>
        <Message ID="first" ">
            <Title>Foo</Title>
            <Body>You can now find details of the blah blah blah</Body>
            <Date>20050612</Date>
        </Message>
    </item>
    <item>
        <Message ID="second" >
            <Title>Bar</Title>
            <Body>Flibble de dee</Body>
            <Date>20050805</Date>
        </Message>
    </item>
</list>

 

Of course, there were lot more messages, but the important point was that my colleague wanted to group the messages by month. That meant that any Message whose Date element's first six characters were the same should be displayed together, no matter where they came in the source document.

 

This turns out to be a non-trivial problem in XSLT, and has been explicitly addressed as a weakness by the people responsible for XSLT 2.0. Anyway - the following works in XSLT 1.1; it's based on the Meunchian grouping technique (aka Fu-Muench-u).

 

 

<?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" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:key name="messages" match="/list/item/Message" use="substring(Date, 1,6)"/>
    <xsl:template match="/">
        <xsl:variable name="messagesWithUniqueMonth" 
	select="/list/item/Message[generate-id()=generate-id(key('messages', substring(Date,1,6)))]"/>
        <xsl:for-each select="$messagesWithUniqueMonth">
            <xsl:sort select="substring(Date,1,6)"/>
            <xsl:call-template name="doMonth">
                <xsl:with-param name="month">
                    <xsl:value-of select="substring(Date,1,6)"/>
                </xsl:with-param>
            </xsl:call-template>
        </xsl:for-each>
    </xsl:template>
    <xsl:template name="doMonth">
        <xsl:param name="month"/>
        <div month='{$month}'>
            <xsl:for-each select="key('messages', $month)">
               <xsl:sort select="Date"/>
                <p>
                    <xsl:value-of select="Title"/><xsl:value-of select="Date"/>
                </p>
            </xsl:for-each>
        </div>
    </xsl:template>
</xsl:stylesheet>

I had wanted to do something less arcane. I tried various approaches beginning with a variable containing all the dates, and then refining it, but most of the time I ended up with a <xsl:for-each/> unable to consume a result fragment or some such. The Muenchian technique works, even if it is opaque, and nothing else I tried did. I think it's probably possible using a recursive template to create a delimited string of months, but this way is definitely getting added to my toolbox.

 

For what it's worth, I'll recommend that we don't do this at all. The XML being transformed is already generated programatically, and it should be simple enough to add a month field that will make life a lot simpler.

Filed under: