Search code examples
xmlxsltxslt-1.0xslt-grouping

XSLT beginner: Group XML based on unique element value XSLT 1.0


I am a beginner and I am trying to group an XML input based on similar category, using XSLT 1.0 Here is the input xml which contains category and location. The out put must group all elements with the same category and list unique locations:

<?xml version="1.0" ?>
<Data>
    <Row>
       <id>123</id>
       <location>/example/games/data.php</location>
       <category>gamedata</category>
    </Row>
    <Row>
        <id>456</id>
       <location>/example/games/data.php</location>
       <category>gamedata</category>
    </Row>
<Row>
        <id>789</id>
       <location>/example/games/score.php</location>
       <category>gamedata</category>
    </Row>
<Row>
       <id>888</id>
       <location>/example/games/title.php</location>
       <category>gametitle</category>
    </Row>
<Row>
        <id>777</id>
       <location>/example/games/title.php</location>
       <category>gametitle</category>
    </Row>
<Row>
        <id>999</id>
       <location>/example/score/title.php</location>
       <category>gametitle</category>
    </Row>
</Data>

Looking for output as(list only unique location grouped by category):

<project>
     <item>
        <data>
<category>gamedata</category>
           <id>456</id>
            <id>789</id>
             <id>123</id>
       <location>/example/games/data.php</location>   
       <location>/example/games/score.php</location>
       </data>
  <data> <category>gametitle</category>
       <id>888</id>
       <id>777</id>
        <id>999</id>
       <location>/example/games/title.php</location>
       <location>/example/score/title.php</location> 
    </data>
</item></project>

What I have tried so far:

 <?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="keyCategory" match="Row" use="category"/>
    <xsl:template match="/">
        <project xmlns="xyz.com">
            <item >
                <name lang="en">Example</name>
                <xsl:for-each select="//Row[generate-id(.) = generate-id(key('keyCategory', category)[1])]">

                    <xsl:for-each select="key('keyCategory', category)">
                          <data>
                            <category><xsl:value-of select="category"/></category>
                            <id><xsl:value-of select="id"/></id>
                             <location><xsl:value-of select="location"/></location></data>
                    </xsl:for-each>
             </xsl:for-each>
</item>
</project>

What I am actually getting:

<project>
     <item>
        <data>
<category>gamedata</category>
           <id>456</id>
       <location>/example/games/data.php</location>

         </data>
         <data>
<category>gamedata</category>
            <id>789</id>
       <location>/example/games/score.php</location>

         </data>
        <data>
<category>gamedata</category>
            <id>789</id>
       <location>/example/games/score.php</location>

         </data>
        <data>
<category>gamedata</category>
       <id>123</id>
       <location>/example/games/data.php</location>

       </data>
  <data>
<category>gametitle</category>
       <id>888</id>
       <location>/example/games/title.php</location>

    </data>
   <data>
<category>gametitle</category>
        <id>777</id>
       <location>/example/games/title.php</location>

    </data>
   <data>
<category>gametitle</category>
        <id>999</id>
       <location>/example/score/title.php</location>

    </data>
</item></project>

Solution

  • For your nested grouping problem I think you want to use a second key:

    <xsl:stylesheet version="1.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output indent="yes"/>
    
        <xsl:key name="keyCategory" match="Row" use="category"/>
    
        <xsl:key name="location" match="Row" use="concat(category, '|', location)"/>
    
        <xsl:template match="/">
            <project>
                <item>
                    <name lang="en">Example</name>
                    <xsl:for-each select="//Row[generate-id(.) = generate-id(key('keyCategory', category)[1])]">
                        <data>
                            <xsl:copy-of
                              select="category"/>
                            <xsl:copy-of select="key('keyCategory', category)/id"/>
                            <xsl:copy-of 
                              select="key('keyCategory', category)[generate-id() = generate-id(key('location', concat(category, '|', location))[1])]/location"/>
                        </data>
                    </xsl:for-each>
                </item>
            </project>
        </xsl:template>
    </xsl:stylesheet>
    

    https://xsltfiddle.liberty-development.net/94Acsm4/0

    This only shows the grouping and ignores that your output uses a different namespace, to create the selected elements in the new namespace I would transform them:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0" 
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns="http://example.com/">
        <xsl:output indent="yes"/>
    
        <xsl:key name="keyCategory" match="Row" use="category"/>
    
        <xsl:key name="location" match="Row" use="concat(category, '|', location)"/>
    
        <xsl:template match="/">
            <project>
                <item>
                    <name lang="en">Example</name>
                    <xsl:for-each select="//Row[generate-id(.) = generate-id(key('keyCategory', category)[1])]">
                        <data>
                            <xsl:apply-templates
                              select="category"/>
                            <xsl:apply-templates select="key('keyCategory', category)/id"/>
                            <xsl:apply-templates 
                              select="key('keyCategory', category)[generate-id() = generate-id(key('location', concat(category, '|', location))[1])]/location"/>
                        </data>
                    </xsl:for-each>
                </item>
            </project>
        </xsl:template>
    
        <xsl:template match="*">
            <xsl:element name="{local-name()}">
                <xsl:apply-templates/>
            </xsl:element>
        </xsl:template>
    </xsl:stylesheet>
    

    https://xsltfiddle.liberty-development.net/94Acsm4/1