xslt-1.0muenchian-grouping

Advanced muenchian grouping: group by items in child collection


I'm familiar with simple muenchian grouping in XSL, but I've encountered a problem, which I honestly don't even know, how to approach it.

So I've got an XML:

<whiskies>
    <whisky name="ABC" percent="" region="" type="">
        <tastingNotesCollection/>
        <bottles>
            <bottle price="" size="" level="0" date=""/>
            <bottle price="" size="" level="70" date=""/>
            <bottle price="" size="" level="100" date=""/>
        </bottles>
        <comments/>
    </whisky>
    <whisky name="DEF" percent="" region="" type="">
        <tastingNotesCollection/>
        <bottles>
            <bottle price="" size="" level="0" date=""/>
            <bottle price="" size="" level="100" date=""/>
        </bottles>
        <comments/>
    </whisky>
    <whisky name="GHI" percent="" region="" type="">
        <tastingNotesCollection/>
        <bottles>
            <bottle price="" size="" level="30" date=""/>
        </bottles>
        <comments/>
    </whisky>
<whiskies>

And the goal is to group the whiskies by levels of a bottle: So level="0" is considered empty. Anything from level="1" to level="99" is considered open. And level="100" is considered unopened.

So the transformed result (will be done in HTML) should look like this:

<h1>Empty</h1>
<ul>
    <li>ABC</li>
    <li>DEF</li>
</ul>
<h1>Open</h1>
<ul>
    <li>ABC</li>
    <li>GHI</li>
</ul>
<h1>Unopened</h1>
<ul>
    <li>ABC</li>
    <li>DEF</li>
</ul>

As you can see, the same whisky can show up in multiple groups, depending on how much bottles there are and how full those bottles are. The second problem is, that the "Open" group doesn't have an exact value and can be aynthing from 1 to 99.

So yeah, don't really know if this can be solved at all or how to even start on this. Any tips appreciated.


Solution

  • I don't think you want to use Muenchian grouping for this. You would need to define a key that enumerates all values in the range from 1 to 99 - and I believe that would take away any advantage that using a key would otherwise bring.

    Since your result has either exactly or at most 3 groups (your question is ambiguous in this respect), you could do simply:

    XSLT 1.0

    <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:template match="/whiskies">
        <output>
            <group status="empty">
                <xsl:for-each select="whisky[bottles/bottle/@level=0]">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
            <group status="open">
                <xsl:for-each select="whisky[bottles/bottle[@level>0 and @level &lt; 100]]">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
            <group status="unopened">
                <xsl:for-each select="whisky[bottles/bottle/@level=100]">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
        </output>
    </xsl:template>
    
    </xsl:stylesheet>
    

    to get (after fixing the input to be a well-formed XML!):

    Result

    <?xml version="1.0" encoding="utf-8"?>
    <output>
      <group status="empty">
        <name>ABC</name>
        <name>DEF</name>
      </group>
      <group status="open">
        <name>ABC</name>
        <name>GHI</name>
      </group>
      <group status="unopened">
        <name>ABC</name>
        <name>DEF</name>
      </group>
    </output>
    

    Make your own adjustment for HTML output.


    Added:

    Here is an alternative approach using keys (though still not Muenchian grouping) which might be more performant:

    <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="w" match="whisky" use="bottles/bottle/@level" />
    <xsl:key name="w1" match="whisky" use="boolean(bottles/bottle[@level!=0 and @level!=100])" />
    
    <xsl:template match="/whiskies">
        <output>
            <group status="empty">
                <xsl:for-each select="key('w', 0)">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
            <group status="open">
                <xsl:for-each select="key('w1', true())">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
            <group status="unopened">
                <xsl:for-each select="key('w', 100)">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
        </output>
    </xsl:template>
    
    </xsl:stylesheet>
    

    Or even:

    <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="bottle" match="bottle" use="ceiling(@level div 99)" />
    
    <xsl:template match="/whiskies">
        <output>
            <group status="empty">
                <xsl:for-each select="key('bottle', 0)/../..">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
            <group status="open">
                <xsl:for-each select="key('bottle', 1)/../..">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
            <group status="unopened">
                <xsl:for-each select="key('bottle', 2)/../..">
                    <name>
                        <xsl:value-of select="@name"/>
                    </name>
                </xsl:for-each>
            </group>
        </output>
    </xsl:template>
    
    </xsl:stylesheet>