Posts
XSLT grouping with xsl:for-each-group: complete guide
01 Apr 2025
How to group XML nodes in XSLT 2.0 using for-each-group. Covers group-by, group-adjacent, group-starting-with, and group-ending-with with examples.
Grouping is one of the most powerful features introduced in XSLT 2.0. Before it, grouping in XSLT 1.0 required the Muenchian method — a clever but verbose technique involving keys and node-set comparisons. In 2.0, xsl:for-each-group makes grouping straightforward.
Basic grouping with group-by
group-by groups nodes that share the same value for a given expression. The result is one iteration per distinct group value.
Input:
<orders>
<order><country>DE</country><amount>120</amount></order>
<order><country>US</country><amount>85</amount></order>
<order><country>DE</country><amount>200</amount></order>
<order><country>FR</country><amount>60</amount></order>
<order><country>US</country><amount>140</amount></order>
</orders>
Stylesheet:
<xsl:template match="orders">
<summary>
<xsl:for-each-group select="order" group-by="country">
<xsl:sort select="current-grouping-key()"/>
<group country="{current-grouping-key()}">
<count><xsl:value-of select="count(current-group())"/></count>
<total><xsl:value-of select="sum(current-group()/amount)"/></total>
</group>
</xsl:for-each-group>
</summary>
</xsl:template>
Output:
<summary>
<group country="DE"><count>2</count><total>320</total></group>
<group country="FR"><count>1</count><total>60</total></group>
<group country="US"><count>2</count><total>225</total></group>
</summary>
Key functions inside for-each-group:
current-grouping-key()— returns the value that defines the current groupcurrent-group()— returns the sequence of all nodes in the current group
Nested grouping
Groups can be nested. Group orders by country, then within each country by status:
<xsl:for-each-group select="order" group-by="country">
<country name="{current-grouping-key()}">
<xsl:for-each-group select="current-group()" group-by="status">
<status value="{current-grouping-key()}">
<xsl:value-of select="count(current-group())"/>
</status>
</xsl:for-each-group>
</country>
</xsl:for-each-group>
group-adjacent
Groups consecutive nodes that share the same key value. Unlike group-by, it starts a new group when the key changes, even if the same key appeared earlier. This is useful for processing structured text or segmented data.
<log>
<entry level="INFO">Starting</entry>
<entry level="INFO">Processing</entry>
<entry level="ERROR">Failed</entry>
<entry level="ERROR">Retrying</entry>
<entry level="INFO">Done</entry>
</log>
<xsl:for-each-group select="entry" group-adjacent="@level">
<block level="{current-grouping-key()}" count="{count(current-group())}">
<xsl:value-of select="current-group()[1]"/>
</block>
</xsl:for-each-group>
This produces three blocks: two INFO (positions 1-2), one ERROR (3-4), one INFO (5). With group-by, the two INFO groups would be merged into one.
group-starting-with and group-ending-with
These group nodes based on a pattern match rather than a key value. Every time a node matches the pattern, a new group starts (or ends).
group-starting-with example — treat every <h2> as the start of a section:
<xsl:for-each-group select="*" group-starting-with="h2">
<section>
<title><xsl:value-of select="self::h2"/></title>
<xsl:apply-templates select="current-group()[position() > 1]"/>
</section>
</xsl:for-each-group>
group-ending-with example — group lines until a blank line:
<xsl:for-each-group select="line" group-ending-with="line[. = '']">
<paragraph>
<xsl:apply-templates select="current-group()[. != '']"/>
</paragraph>
</xsl:for-each-group>
Computing aggregates
current-group() returns a sequence, so you can apply any XPath aggregate function directly:
<xsl:for-each-group select="transaction" group-by="currency">
<currency code="{current-grouping-key()}">
<count><xsl:value-of select="count(current-group())"/></count>
<total><xsl:value-of select="sum(current-group()/amount)"/></total>
<average><xsl:value-of select="avg(current-group()/amount)"/></average>
<max><xsl:value-of select="max(current-group()/amount)"/></max>
</currency>
</xsl:for-each-group>
Try it in XSLT Playground
Paste any of the examples above into XSLT Playground with version set to 2.0 or 3.0. Grouping is one of the features that benefits most from live testing — you can immediately see how changing the grouping key or switching between group-by and group-adjacent affects the output structure.