Somu
Somu

Reputation: 3760

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.

Upvotes: 0

Views: 131

Answers (2)

michael.hor257k
michael.hor257k

Reputation: 117140

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>

Upvotes: 1

Philipp
Philipp

Reputation: 4749

I did exactly the same thing as you already did and then went a step further and grouped again. Now I get the following output with your input:

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

This is the XSLT 2.0

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns="" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:avintis="http://www.avintis.com/esb" exclude-result-prefixes="#all" version="2.0">
    <xsl:output method="xml" encoding="UTF-8" indent="yes"/>

    <xsl:template match="/persons">
        <groups>
            <xsl:variable name="persons" select="."/>

            <!-- create a temporary variable containing all roles of a person -->
            <xsl:variable name="roles">
                <xsl:for-each select="distinct-values(person/@name)">
                    <xsl:sort select="."/>
                    <xsl:variable name="name" select="."/>
                    <xsl:element name="group">
                        <xsl:attribute name="roles">
                            <!-- sort the roles of each person -->
                            <xsl:variable name="rolesSorted">
                                <xsl:for-each select="$persons/person[@name=$name]">
                                    <xsl:sort select="@role"/>
                                    <xsl:copy-of select="."/>
                                </xsl:for-each>
                            </xsl:variable>
                            <xsl:value-of select="string-join($rolesSorted/person/@role,',')"/>
                        </xsl:attribute>
                        <xsl:attribute name="persons" select="."/>
                    </xsl:element>
                </xsl:for-each>
            </xsl:variable>

            <!-- now loop again over all roles of the persons and group persons having the same roles -->
            <xsl:for-each select="distinct-values($roles/group/@roles)">
                <xsl:element name="group">
                    <xsl:variable name="name" select="."/>
                    <xsl:attribute name="roles" select="$name"/>
                    <xsl:attribute name="persons">
                        <xsl:value-of select="string-join($roles/group[@roles=$name]/@persons,',')"/>
                    </xsl:attribute>
                </xsl:element>
            </xsl:for-each>
        </groups>
    </xsl:template>

    <xsl:template match="*|text()|@*">
        <xsl:copy>
            <xsl:apply-templates select="*|text()|@*"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

The roles get also sorted - so independent from the input order of the roles and persons.

Upvotes: 1

Related Questions