How Does group-ending-with Work in XSLT?
The group-ending-with attribute in XSLT 2.0 and later
versions provides a pattern-based mechanism for partitioning flat
sequences into structured, hierarchical groups using explicit terminal
delimiters. Unlike value-based grouping using group-by,
group-ending-with evaluates a pattern against each item in
a sequence, closing the current group immediately when an item matches
that pattern and including the matching item as the final element of
that group.
Positional Partitioning with Terminal Delimiters
In XML transformations, data often arrives as a flat list of sibling
elements where specific tokens or elements signify the boundary of a
logical record. The xsl:for-each-group instruction
addresses this through pattern matching. When you specify
group-ending-with="pattern", the processor iterates through
the sequence supplied in the select attribute and applies
the following grouping logic:
- Item Evaluation: The processor evaluates the
pattern defined in
group-ending-withagainst each node in the input sequence in document order. - Boundary Creation: When an item matches the
pattern, it becomes the last member of the current group
(
current-group()). - Sequence Continuation: The item immediately following the matched node initializes a new group.
Because matching nodes are included in the group they terminate, the resulting group always contains the delimiter itself as its tail item, allowing stylesheets to process or strip the delimiter cleanly.
Practical Delimiter Scenario
Consider a flat sequence of mixed elements where individual
<record-end/> markers separate distinct transaction
blocks:
<events>
<data id="1">Start A</data>
<data id="2">Process A</data>
<record-end/>
<data id="3">Start B</data>
<record-end/>
<data id="4">Start C (Incomplete)</data>
</events>To transform these flat items into nested
<transaction> blocks, group-ending-with
matches against the delimiter element:
<xsl:template match="events">
<transactions>
<xsl:for-each-group select="*" group-ending-with="record-end">
<transaction>
<xsl:copy-of select="current-group()[not(self::record-end)]"/>
</transaction>
</xsl:for-each-group>
</transactions>
</xsl:template>In this transformation, the first group contains items with IDs 1 and
2 followed by <record-end/>. The predicate
[not(self::record-end)] strips the structural marker,
leaving only the payload data inside the generated
<transaction> wrapper.
Handling Edge Cases and Trailing Items
Structured delimiter processing with group-ending-with
involves several specific runtime behaviors:
- Unterminated Trailing Sequences: If the input sequence does not end with a matching delimiter, any remaining elements after the final delimiter are still gathered into a final group. The final group simply lacks a matching terminating node.
- Consecutive Delimiters: When two delimiter elements occur back-to-back, the second delimiter forms a group consisting solely of itself.
- Initial Delimiters: If the first item in the sequence matches the delimiter pattern, the first group contains only that single delimiter element.
- Pattern Context: The expression in
group-ending-withis an XSLT pattern (matching syntax) rather than a general XPath expression, meaning it tests node type and attributes directly against the candidate item.