XSLT - Grouping XML Data Using <xsl:for-each-group>?

Introduction

Grouping XML data is the process of arranging related XML nodes into meaningful groups based on a common value or relationship. In XSLT, grouping is particularly useful when an XML document contains many records that need to be organized according to a specific field.

For example, consider an XML document containing employee information:

<employees>
    <employee>
        <name>Rahul</name>
        <department>Sales</department>
    </employee>
    <employee>
        <name>Anita</name>
        <department>HR</department>
    </employee>
    <employee>
        <name>Vijay</name>
        <department>Sales</department>
    </employee>
    <employee>
        <name>Priya</name>
        <department>HR</department>
    </employee>
</employees>

Here, employees belong to different departments. Instead of displaying every employee individually, we may want to produce an output where employees are grouped under their respective departments.

XSLT 2.0 and later versions provide the <xsl:for-each-group> instruction specifically for this purpose.

What is <xsl:for-each-group>?

<xsl:for-each-group> is an XSLT instruction used to divide a sequence of nodes into groups and process each group separately.

Its basic structure is:

<xsl:for-each-group select="expression"
                    group-by="expression">
    
    <!-- Process each group -->

</xsl:for-each-group>

The select attribute identifies the nodes that should be grouped.

The group-by attribute determines how those nodes should be grouped.

For example:

<xsl:for-each-group select="employees/employee"
                    group-by="department">
    
    <h2>
        <xsl:value-of select="current-grouping-key()"/>
    </h2>

    <xsl:for-each select="current-group()">
        <p>
            <xsl:value-of select="name"/>
        </p>
    </xsl:for-each>

</xsl:for-each-group>

The employees are grouped according to their department value.

Why Grouping Is Important

Without grouping, an XSLT stylesheet would normally process each employee one after another. If several employees belong to the same department, the department name might have to be repeated for every employee.

Grouping allows the transformation to produce a more organized structure.

For example, instead of:

Sales
Rahul

HR
Anita

Sales
Vijay

HR
Priya

grouping can produce:

Sales
Rahul
Vijay

HR
Anita
Priya

This is especially useful for:

  • Employee reports

  • Product catalogs

  • Sales reports

  • Customer records

  • Department-wise information

  • Category-based lists

  • Monthly reports

  • Country-wise records

  • XML-to-HTML transformations

The group-by Attribute

The group-by attribute specifies the value used to determine group membership.

Consider:

<xsl:for-each-group select="employees/employee"
                    group-by="department">

If the XML contains:

<department>Sales</department>

then all employees with the value Sales are placed in the same group.

Similarly, employees with:

<department>HR</department>

are placed in another group.

The grouping key is therefore the value returned by the group-by expression.

Understanding current-group()

Inside <xsl:for-each-group>, the function current-group() returns all nodes belonging to the current group.

For example:

<xsl:for-each-group select="employees/employee"
                    group-by="department">

    <h2>
        <xsl:value-of select="current-grouping-key()"/>
    </h2>

    <xsl:for-each select="current-group()">
        <p>
            <xsl:value-of select="name"/>
        </p>
    </xsl:for-each>

</xsl:for-each-group>

Suppose the current group is the Sales department.

Then:

current-group()

contains:

Rahul
Vijay

This allows the stylesheet to process all members of the group.

Understanding current-grouping-key()

The function:

current-grouping-key()

returns the value used to create the current group.

For example:

<xsl:value-of select="current-grouping-key()"/>

If the current group is the Sales group, it returns:

Sales

If the current group is the HR group, it returns:

HR

This is useful when displaying the group heading.

Complete Example

Consider the following XML:

<employees>
    <employee>
        <name>Rahul</name>
        <department>Sales</department>
        <salary>35000</salary>
    </employee>

    <employee>
        <name>Anita</name>
        <department>HR</department>
        <salary>40000</salary>
    </employee>

    <employee>
        <name>Vijay</name>
        <department>Sales</department>
        <salary>38000</salary>
    </employee>

    <employee>
        <name>Priya</name>
        <department>HR</department>
        <salary>42000</salary>
    </employee>
</employees>

An XSLT stylesheet can group the employees by department:

<xsl:stylesheet version="2.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:template match="/">
        <html>
            <body>

                <xsl:for-each-group select="employees/employee"
                                    group-by="department">

                    <h2>
                        <xsl:value-of select="current-grouping-key()"/>
                    </h2>

                    <ul>
                        <xsl:for-each select="current-group()">
                            <li>
                                <xsl:value-of select="name"/>
                            </li>
                        </xsl:for-each>
                    </ul>

                </xsl:for-each-group>

            </body>
        </html>
    </xsl:template>

</xsl:stylesheet>

The resulting HTML structure would be conceptually:

<html>
    <body>
        <h2>Sales</h2>
        <ul>
            <li>Rahul</li>
            <li>Vijay</li>
        </ul>

        <h2>HR</h2>
        <ul>
            <li>Anita</li>
            <li>Priya</li>
        </ul>
    </body>
</html>

The important point is that the stylesheet first creates the groups and then processes the members of each group.

Grouping by More Than One Value

The grouping expression does not have to be a simple element.

For example:

<xsl:for-each-group select="employees/employee"
                    group-by="concat(department, '-', location)">

This can create groups based on a combination of department and location.

If an employee has:

<department>Sales</department>
<location>Bangalore</location>

the grouping key could become:

Sales-Bangalore

Another employee in the Sales department but located in Delhi would belong to a different group.

This makes grouping useful when a single XML element is not sufficient to define a category.

Sorting Groups

Groups can also be sorted.

For example:

<xsl:for-each-group select="employees/employee"
                    group-by="department">

    <xsl:sort select="current-grouping-key()"/>

    <h2>
        <xsl:value-of select="current-grouping-key()"/>
    </h2>

    <xsl:for-each select="current-group()">
        <p>
            <xsl:value-of select="name"/>
        </p>
    </xsl:for-each>

</xsl:for-each-group>

The groups will be processed according to the department name.

For example:

HR
Sales

instead of necessarily following the original order in the XML.

Groups can also be sorted using numeric values, dates, or other expressions.

Sorting Items Inside a Group

It is also possible to sort the individual records within each group.

For example:

<xsl:for-each-group select="employees/employee"
                    group-by="department">

    <h2>
        <xsl:value-of select="current-grouping-key()"/>
    </h2>

    <xsl:for-each select="current-group()">
        <xsl:sort select="name"/>

        <p>
            <xsl:value-of select="name"/>
        </p>
    </xsl:for-each>

</xsl:for-each-group>

Here, the departments are grouped first, and the employees within each department are sorted alphabetically.

Grouping Based on Adjacent Values

XSLT also provides another form of grouping using:

group-adjacent

This is different from group-by.

Suppose the XML contains:

<items>
    <item category="A">One</item>
    <item category="A">Two</item>
    <item category="B">Three</item>
    <item category="B">Four</item>
    <item category="A">Five</item>
</items>

Using:

<xsl:for-each-group select="items/item"
                    group-adjacent="@category">

the first two A items form one group, the two B items form another group, and the final A item forms a new group.

The result is conceptually:

Group A
One
Two

Group B
Three
Four

Group A
Five

This is different from group-by, where all items having the same grouping value can belong to the same group regardless of their position.

group-starting-with

Another grouping method is:

group-starting-with

It is useful when a new group begins whenever a particular pattern is encountered.

For example:

<xsl:for-each-group select="document/section"
                    group-starting-with="title">

This can be useful when processing document structures where a heading indicates the beginning of a new section.

group-ending-with

Similarly, XSLT provides:

group-ending-with

This creates groups that end when a particular pattern is encountered.

This can be useful when processing XML documents where a particular element marks the end of a logical section.

Difference Between group-by and group-adjacent

The difference is important.

With:

group-by="department"

all records having the same department value are grouped together.

With:

group-adjacent="department"

only consecutive records with the same department value are grouped together.

For example:

Sales
Sales
HR
HR
Sales

With group-by, the Sales records can be treated as one group.

With group-adjacent, the first two Sales records form one group and the final Sales record forms another group.

Advantages of <xsl:for-each-group>

The major advantages include:

  1. It provides a structured way to organize related XML nodes.

  2. It reduces the complexity of grouping logic in XSLT stylesheets.

  3. It supports grouping by values, adjacent values, and structural patterns.

  4. It makes reports easier to generate.

  5. It works particularly well with XSLT 2.0 and later.

  6. It allows groups and their members to be sorted independently.

  7. It provides convenient functions such as current-group() and current-grouping-key().

  8. It is useful for transforming large collections of XML records into organized HTML, XML, or text output.

Common Applications

Grouping with <xsl:for-each-group> can be used in many practical situations.

Employee Reports

Employees can be grouped by:

Department
Location
Job Title
Manager

Product Catalogs

Products can be grouped by:

Category
Brand
Product Type
Price Range

Sales Reports

Sales records can be grouped by:

Month
Region
Salesperson
Product
Customer

Student Records

Students can be grouped by:

Class
Department
Grade
Academic Year

Geographic Data

Records can be grouped by:

Country
State
City
Region

Important Points to Remember

<xsl:for-each-group> is primarily used when multiple XML nodes need to be processed as logical groups.

The select attribute specifies the nodes to process.

The group-by attribute specifies the value used for grouping.

current-group() returns the nodes belonging to the current group.

current-grouping-key() returns the key associated with the current group.

group-adjacent groups consecutive nodes having the same value.

group-starting-with starts a group when a specified pattern is encountered.

group-ending-with ends a group when a specified pattern is encountered.

Grouping is especially powerful in XSLT 2.0 and later because these grouping facilities are built directly into the language.

Conclusion

Grouping XML data using <xsl:for-each-group> is an important XSLT technique for organizing related nodes into meaningful collections. Instead of processing every XML node independently, the stylesheet can first identify logical groups and then process each group as a unit.

The most important concepts to understand are group-by, group-adjacent, group-starting-with, group-ending-with, current-group(), and current-grouping-key(). Once these concepts are understood, complex XML reports such as department-wise employee reports, category-wise product catalogs, and region-wise sales reports can be generated much more efficiently and clearly.