Search code examples
xmlxsltxpathmuenchian-grouping

Sums and Category Grouping with XSLT


With XSLT, how can I change the following:

<root>
  <element id="1" State="Texas" County="Dallas" Population="2412827" />
  <element id="2" State="Texas" County="Harris" Population="3984349" />
  <element id="3" state="Georgia" County="Fulton" Population="1014932" />
  <element id="4" state="Georgia" County="Richmond" Population="212775" />
</root>

into:

<body>
  <h2>Texas</h2>
  <table>
    <tr><td>Dallas</td><td>2412827</td></tr>
    <tr><td>Harris</td><td>3984349</td></tr>
    <tr><td>Total</td><td>6397176</td></tr>
  <h2>Georgia</h2>
  <table>
    <tr><td>Fulton</td><td>1014932</td></tr>
    <tr><td>Richmond</td><td>212775</td></tr>
    <tr><td>Total</td><td>1227707</td></tr>
  </table>
</body>

without explicitly coding each state's name, because I would be screwed if Puerto Rico ever became a state.


Solution

  • XSLT 1.0
    Define a key "state", from which we can easily select all states given a state name. Than apply Muenchian grouping to find the unique states in the input.

    Then it gets simple. The "element" template will be applied once per state name, and uses the key() to fetch all entries for that state.

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
        <xsl:output method="xml" indent="yes" />
    
        <xsl:key name="state" match="element" use="@State" />
    
        <xsl:template match="root">
            <body>
                <xsl:apply-templates select="element[generate-id(.)=generate-id(key('state',@State)[1])]"/>
            </body>
        </xsl:template>
    
        <xsl:template match="element">
            <h2><xsl:value-of select="@State" /></h2>
            <table>
                <xsl:for-each select="key('state',@State)">
                    <tr>
                        <td>
                            <xsl:value-of select="@County" />
                        </td>
                        <td>
                            <xsl:value-of select="@Population" />
                        </td>
                    </tr>
                </xsl:for-each>
    
                <tr>
                    <td>
                        <xsl:text>Total</xsl:text>
                    </td>
                    <td>
                        <xsl:value-of select="sum(key('state',@State)/@Population)"/>
                    </td>
                </tr>
    
            </table>
        </xsl:template>
    
    </xsl:stylesheet>
    

    XSLT 2.0

    <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
        <xsl:output method="xml" indent="yes" />
    
        <xsl:template match="root">
            <body>
                <xsl:for-each-group select="element" group-by="@State">
                    <h2><xsl:value-of select="@State" /></h2>
                    <table>
                        <xsl:for-each select="current-group()">
                            <tr>
                                <td>
                                    <xsl:value-of select="@County" />
                                </td>
                                <td>
                                    <xsl:value-of select="@Population" />
                                </td>
                            </tr>
                        </xsl:for-each>
                        <tr>
                            <td>
                                <xsl:text>Total</xsl:text>
                            </td>
                            <td>
                                <xsl:value-of select="format-number(sum(current-group()/@Population), '#########')"/>
                            </td>
                        </tr>
                    </table>
                </xsl:for-each-group>
            </body>
        </xsl:template>
    
    </xsl:stylesheet>