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:

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: