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 the XSLT 2.0 Online Tester — Saxon HE included, no install. 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. For xsl:stream, switch to the XSLT 3.0 tester.