XSLT 2.0 introduced the xsl:for-each-group instruction, which enables powerful grouping operations on node sequences. This capability was a significant enhancement over XSLT 1.0, eliminating the need for the Muenchian method and complex key-based workarounds. The instruction processes a sequence of items by dividing them into groups based on specified criteria, then iterates over each group sequentially.
Two builtin functions complement this instruction:
current-group(): Returns all items belonging to the current group being processedcurrent-grouping-key(): Returns the key value that identifies the current group
The schema definition reveals the complete structure of this instruction:
<xs:element name="for-each-group" substitutionGroup="xsl:instruction">
<xs:complexType mixed="true">
<xs:complexContent mixed="true">
<xs:extension base="xsl:versioned-element-type">
<xs:sequence>
<xs:element ref="xsl:sort" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="xsl:sequence-constructor-group" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="select" type="xsl:expression" use="required"/>
<xs:attribute name="group-by" type="xsl:expression"/>
<xs:attribute name="group-adjacent" type="xsl:expression"/>
<xs:attribute name="group-starting-with" type="xsl:pattern"/>
<xs:attribute name="group-ending-with" type="xsl:pattern"/>
<xs:attribute name="collation" type="xs:anyURI"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
The select attribute is mandatory and defines the input sequence to be grouped. One of the four grouping attributes must be specified to determine how items are partitioned.
Source Data Structure
Consider an XML document containing person records with demographic information:
<?xml version="1.0" encoding="UTF-8"?>
<directory>
<entry gender="female">
<name first="Jennie" last="Anderson"/>
</entry>
<entry gender="male">
<name first="Ricky" last="Anderson"/>
</entry>
<entry gender="male">
<name first="Peter" last="Smith"/>
</entry>
<entry gender="female">
<name first="Fang" last="Anderson"/>
</entry>
<entry gender="female">
<name first="FangFang" last="Wilson"/>
</entry>
<entry gender="male">
<name first="Paul" last="Anderson"/>
</entry>
</directory>
Complete Stylesheet Example
The following stylesheet demonstrates all four grouping strategies:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:util="http://example.com/util"
exclude-result-prefixes="xs util">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:variable name="source-data" select="document('source.xml')"/>
<!-- Template demonstrating all grouping strategies -->
<xsl:template name="process-data">
<xsl:result-document href="output-by-gender.xml">
<categorized>
<xsl:comment>Grouping entries by gender attribute</xsl:comment>
<xsl:for-each-group select="$source-data/directory/entry" group-by="@gender">
<category type="{current-grouping-key()}">
<xsl:perform-sort select="current-group()">
<xsl:sort select="name/@last"/>
<xsl:sort select="name/@first"/>
</xsl:perform-sort>
<xsl:apply-templates select="current-group()"/>
</category>
</xsl:for-each-group>
</categorized>
</xsl:result-document>
<xsl:result-document href="output-by-surname.xml">
<grouped>
<xsl:comment>Clustering by surname: Anderson versus others</xsl:comment>
<xsl:variable name="sorted-input" select="util:sort-by-surname($source-data/directory/entry)"/>
<xsl:for-each-group select="$sorted-input/entry"
group-adjacent="if (name/@last = 'Anderson') then 'surname-anderson' else 'other-surnames'">
<cluster identifier="{current-grouping-key()}">
<xsl:for-each select="current-group()">
<entry first="{name/@first}" last="{name/@last}"/>
</xsl:for-each>
</cluster>
</xsl:for-each-group>
</grouped>
</xsl:result-document>
<xsl:result-document href="output-segmented.xml">
<segments>
<xsl:comment>Partitioning into fixed-size groups of four</xsl:comment>
<xsl:for-each-group select="$source-data/directory/entry"
group-starting-with="*[position() mod 4 = 1]">
<segment number="{position()}">
<xsl:for-each select="current-group()">
<record first="{name/@first}" last="{name/@last}"/>
</xsl:for-each>
</segment>
</xsl:for-each-group>
</segments>
</xsl:result-document>
<xsl:result-document href="output-ending-groups.xml">
<grouped-output>
<xsl:comment>Grouping with terminal marker pattern</xsl:comment>
<xsl:for-each-group select="$source-data/directory/entry"
group-ending-with="*[name/@last = 'Smith']">
<bundle index="{position()}">
<xsl:apply-templates select="current-group()"/>
</bundle>
</xsl:for-each-group>
</grouped-output>
</xsl:result-document>
</xsl:template>
<!-- Helper function for surname-based sorting -->
<xsl:function name="util:sort-by-surname" as="element()*">
<xsl:param name="items" as="element(entry)*"/>
<xsl:sequence select="$items">
<xsl:perform-sort select="$items">
<xsl:sort select="if (name/@last = 'Anderson') then 0 else 1"/>
<xsl:sort select="name/@last"/>
<xsl:sort select="name/@first"/>
</xsl:perform-sort>
</xsl:sequence>
</xsl:function>
<xsl:template match="entry">
<person first="{name/@first}" last="{name/@last}" gender="{@gender}"/>
</xsl:template>
</xsl:stylesheet>
Understanding Grouping Attributes
group-by Attribute
The group-by attribute creates groups based on the evaluation of an XPath expression for each item in the selection. Items producing the same value become part of the same group. This attribute excels at aggregating data by categorical values such as attributes, element content, or computed expressions.
In the first example, entries are partitioned by their gender attribute. The current-grouping-key() function returns either "male" or "female", enabling downstream processing to distinguish between categories. Each group maintains document order relative to the original sequence, with optional sorting applied via xsl:sort children.
group-adjacent Attribute
The group-adjacent attribute implements adjacency-based grouping where consecutive items sharing the same key value form a group. This differs from group-by in that separated items with identical keys belong to different groups if they are not adjacent.
The second example demonstrates this by clustering entries where "Anderson" is the surname. Notice how entries sorted to place Andersons together create a single group, whereas an unsorted input would generate multiple Anderson groups. The grouping key for each cluster is determined by the adjacent expression evaluation.
group-starting-with Attribute
This attribute accepts a pattern that identifies the first element of each group. When an element matches the pattern, a new group begins at that position, continuing until the next matching element or the end of the sequence.
The third example partitions the input into segments where each group starts at positions 1, 5, and 9 (positions where position() mod 4 = 1). This pattern-based approach provides flexibility for scenarios like pagination, chunked processing, or section demarcation.
group-ending-with Attribute
Operating conversely to group-starting-with, this attribute identifies terminal elements that conclude a group. The group extends from the element after the previous terminator (or the beginning) through the matching element.
In the fourth example, groups terminate when an entry with surname "Smith" is encountered. This pattern is useful for document chunking based on structural markers or semantic boundaries.
Key Implementation Considerations
The current-group() function returns nodes in their original document order within the group, which is crucial for maintaining sequence integrity. Combined with xsl:sort, developers can achieve both grouping and within-group ordering.
Grouping operations establish a new context for each iteration. Inside xsl:for-each-group, the context item and position reflect the first node of each group, while current-group() provides access to all group members.
The collation attribute enibles locale-aware grouping and sorting, supporting internationalized applications where character ordering varies by language and region.