Search code examples
xmlxsltxslt-1.0

XSLT 1.0 group on multiple values


My XML is structured like this (simplified)

    <Record>
        <Person>
            <name>Jim</name>
            <year>
                <value>2022</value>
            </year>
        </Person>
    </Record>
    <Record>
        <Person>
            <name>Mary</name>
            <year>
                <value>2022</value>
                <value>2023</value>
            </year>
        </Person>
    </Record>    
    <Record>
        <Person>
            <name>Linda</name>
            <year>
                <value>2022</value>
                <value>2021</value>
            </year>
        </Person>
    </Record>    

I need to create an HTML list grouped by year/value which can be shared across Person records.

2023
-Mary

2022
-Jim
-Linda
-Mary

2021
-Linda

I have tried standard Meunchian grouping, but all I get is the first year/value

2022
-Jim
-Linda
-Mary

XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
    xmlns:exsl="http://exslt.org/common">
    
    <xsl:key match="Person" name="person-year" use="year/value"/>
    <xsl:template match="/">
        <xsl:apply-templates select="Record">

        </xsl:apply-templates>
    </xsl:template>
    <xsl:template match="Record">
        <xsl:for-each select="Person[generate-id(.)=generate-id(key('person-year', year/value))]">
            <table class="table table-striped table-bordered" summary="list of {year/value} people">
                <tr>
                    <th>
                        <xsl:value-of select="year/value"/>
                    </th>
                </tr>
                <xsl:for-each select="key('person-year', year/value)">
                    <xsl:sort order="ascending" select="name"/>
                    <tr>
                        <td>
                            <a href="{link}" target="_blank">
                                <xsl:value-of select="name"/>
                            </a>
                        </td>
                    </tr>
                </xsl:for-each>
            </table>
        </xsl:for-each>
    </xsl:template>

Solution

  • You want to group every Person by year/value so you first need to know what all the year/value values are. You can add another xsl:key for this.

    Then you process all of the years and use the current year to access your original key to get the people for that year.

    Example...

    XML (updated to be well-formed)

    <doc>
        <Record>
            <Person>
                <name>Jim</name>
                <year>
                    <value>2022</value>
                </year>
            </Person>
        </Record>
        <Record>
            <Person>
                <name>Mary</name>
                <year>
                    <value>2022</value>
                    <value>2023</value>
                </year>
            </Person>
        </Record>
        <Record>
            <Person>
                <name>Linda</name>
                <year>
                    <value>2022</value>
                    <value>2021</value>
                </year>
            </Person>
        </Record>
    </doc>
    

    XSLT 1.0

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
        <xsl:output indent="yes"/>
        
        <xsl:key name="years" match="Person/year/value" use="."/>
        <xsl:key name="person-year" match="Person" use="year/value"/>
            
        <xsl:template match="/">
            <xsl:for-each select=".//Record/Person/year/value[generate-id()=generate-id(key('years',.)[1])]">
                <xsl:sort select="." order="descending"/>
                <table class="table table-striped table-bordered" summary="list of {.} people">
                    <tr>
                        <th>
                            <xsl:value-of select="."/>
                        </th>
                    </tr>
                    <xsl:for-each select="key('person-year', .)">
                        <xsl:sort order="ascending" select="name"/>
                        <tr>
                            <td>
                                <a href="{link}" target="_blank">
                                    <xsl:value-of select="name"/>
                                </a>
                            </td>
                        </tr>
                    </xsl:for-each>
                </table>
            </xsl:for-each>
        </xsl:template>
        
    </xsl:stylesheet>
    

    Output (@href is empty because your source didn't have link elements)

    <?xml version="1.0" encoding="utf-8"?>
    <table class="table table-striped table-bordered" summary="list of 2023 people">
       <tr>
          <th>2023</th>
       </tr>
       <tr>
          <td>
             <a href="" target="_blank">Mary</a>
          </td>
       </tr>
    </table>
    <table class="table table-striped table-bordered" summary="list of 2022 people">
       <tr>
          <th>2022</th>
       </tr>
       <tr>
          <td>
             <a href="" target="_blank">Jim</a>
          </td>
       </tr>
       <tr>
          <td>
             <a href="" target="_blank">Linda</a>
          </td>
       </tr>
       <tr>
          <td>
             <a href="" target="_blank">Mary</a>
          </td>
       </tr>
    </table>
    <table class="table table-striped table-bordered" summary="list of 2021 people">
       <tr>
          <th>2021</th>
       </tr>
       <tr>
          <td>
             <a href="" target="_blank">Linda</a>
          </td>
       </tr>
    </table>