Search code examples
xsltxslt-1.0xslt-groupingexslt

Grouping of grouped data


Input:

<persons>
    <person name="John" role="Writer"/>
    <person name="John" role="Poet"/>
    <person name="Jacob" role="Writer"/>
    <person name="Jacob" role="Poet"/>
    <person name="Joe" role="Poet"/>
</persons>

Expected Output:

<groups>
    <group roles="Wriet, Poet" persons="John, Jacob"/>
    <group roles="Poet" persons="Joe"/>
</groups>

As in the above example, I first need to group on person names and find everyone's roles. If more than one person is found to have the same set of roles (e.g. both John and Jacob are both Writer and Poet), then I need to group on each set of roles and list the person names.

I can do this for the first level of grouping using Muenchian method or EXSLT set:distinct etc.

<groups>
    <group roles="Wriet, Poet" persons="John"/>
    <group roles="Wriet, Poet" persons="Jacob"/>
    <group roles="Poet" persons="Joe"/>
</groups>

The above was transformed using XSLT 1.0 and EXSLT:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:sets="http://exslt.org/sets" extension-element-prefixes="sets">
    <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
    <xsl:key name="persons-by-name" match="person" use="@name"/>
    <xsl:template match="persons">
        <groups>
            <xsl:for-each select="sets:distinct(person/@name)">
                <group>
                    <xsl:attribute name="persons"><xsl:value-of select="."/></xsl:attribute>
                    <xsl:attribute name="roles">
                        <xsl:for-each select="key('persons-by-name', .)">
                            <xsl:value-of select="@role"/>
                            <xsl:if test="position()!=last()"><xsl:text>, </xsl:text></xsl:if>
                        </xsl:for-each>
                    </xsl:attribute>
                </group>
            </xsl:for-each>
        </groups>
    </xsl:template>
</xsl:stylesheet>

However, I need help to understand how to group on the grouped roles.

If XSLT 1.0 solution is not available, please feel free to recommend XSLT 2.0 approach.


Solution

  • Try it this way?

    XSLT 1.0
    (using EXSLT node-set() and distinct() functions)

    <xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:exsl="http://exslt.org/common"
    xmlns:set="http://exslt.org/sets"
    extension-element-prefixes="exsl set">
    <xsl:output method="xml" encoding="UTF-8" indent="yes" />
    
    <xsl:key name="person-by-name" match="person" use="@name" />
    <xsl:key name="person-by-roles" match="person" use="@roles" />
    
    <xsl:variable name="distinct-persons">
        <xsl:for-each select="set:distinct(/persons/person/@name)">
            <person name="{.}">
                <xsl:attribute name="roles">
                    <xsl:for-each select="key('person-by-name', .)/@role">
                        <xsl:sort/>
                        <xsl:value-of select="." />
                        <xsl:if test="position()!=last()">
                            <xsl:text>, </xsl:text>
                        </xsl:if>
                    </xsl:for-each>
                </xsl:attribute>
            </person>
        </xsl:for-each> 
    </xsl:variable>
    
    <xsl:template match="/">
        <groups>
            <xsl:for-each select="set:distinct(exsl:node-set($distinct-persons)/person/@roles)">
                <group roles="{.}">
                    <xsl:attribute name="names">
                        <xsl:for-each select="key('person-by-roles', .)/@name">
                            <xsl:value-of select="." />
                            <xsl:if test="position()!=last()">
                                <xsl:text>, </xsl:text>
                            </xsl:if>
                        </xsl:for-each>
                    </xsl:attribute>
                </group>
            </xsl:for-each>
        </groups>
    </xsl:template>
    
    
    </xsl:stylesheet>
    

    Result:

    <?xml version="1.0" encoding="UTF-8"?>
    <groups>
       <group roles="Poet, Writer" names="John, Jacob"/>
       <group roles="Poet" names="Joe"/>
    </groups>