Jim Maivald
Jim Maivald

Reputation: 520

Grouping data using XSLT is not working completely

I have an XSLT that I have created to group data for a catalog of shoes. The shoes need to be grouped by "line" and by "brand". The Line and Brand titles need to appear at the beginning of each section. Each line has more than one brand. Each brand usually has more than one shoe in it.

Here is some sample XML data:

<?xml version="1.0" encoding="UTF-8"?>
<catalog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<shoe>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
    <brand>WALKING</brand>
    <style-name>NEW BALANCE-NB 475V2 (WIDE)</style-name>
    <color>Black</color>
    <price>67.99</price>
</shoe>
<shoe>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
    <brand>WALKING</brand>
    <style-name>NEW BALANCE-496 (WIDE)</style-name>
    <color>Grey/Pink</color>
    <price>64.99</price>
</shoe>
<shoe>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
    <brand>CROSS TRANING</brand>
    <style-name>FILA-MEMORY PANACHE</style-name>
    <color>Black/Pink</color>
    <price>69.99</price>
</shoe>
<shoe>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
    <brand>RUNNING</brand>
    <style-name>FILA-VITALITY 2 TRAIL</style-name>
    <color>Grey/Prpl/Turq</color>
    <price>59.99</price>
</shoe>
<shoe>
    <line>LINE 87: MENS BRANDED ATHLETIC</line>
    <brand>CASUAL</brand>
    <style-name>LEVI'S-HAMILTON BUCK HI</style-name>
    <color>Black/Black</color>
    <price>34.99</price>
</shoe>
<shoe>
    <line>LINE 87: MENS BRANDED ATHLETIC</line>
    <brand>CASUAL</brand>
    <style-name>EVERLAST-EVAN SKATE</style-name>
    <color>Black</color>
    <price>29.99</price>
</shoe>
<shoe>
    <line>LINE 87: MENS BRANDED ATHLETIC</line>
    <brand>RUNNING</brand>
    <style-name>SKECHERS-POWER SWITCH (WIDE)</style-name>
    <color>Black/White</color>
    <price>69.99</price>
</shoe>
<shoe>
    <line>LINE 87: MENS BRANDED ATHLETIC</line>
    <brand>RUNNING</brand>
    <style-name>SKECHERS-EQUALIZER GEL TOP </style-name>
    <color>Black</color>
    <price>69.99</price>
</shoe>
</catalog>

Here is my XSLT:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" encoding="UTF-8"/>

    <xsl:key name="shoes-by-line" match="shoe" use="line" />
    <xsl:key name="shoes-by-brand" match="shoe" use="concat(line,'#',brand )" />

    <xsl:template match="catalog">
        <catalog>
            <shoes>
                <xsl:for-each select="shoe[count(. | key('shoes-by-line', line)[1]) = 1]">
                    <line>
                        <xsl:value-of select="line"/>
                    </line>
                    <brands>
                        <brand>
                            <xsl:value-of select="brand"/>
                        </brand>
                        <xsl:for-each select="key( 'shoes-by-brand', concat(line,'#',brand ))">
                            <shoe>
                                <style-name>
                                    <xsl:value-of select="style-name"/>
                                </style-name>
                                <color>
                                    <xsl:value-of select="color"/>
                                </color>
                                <price>
                                    <xsl:value-of select="price"/>
                                </price>
                            </shoe>
                        </xsl:for-each>
                    </brands>
                </xsl:for-each>
            </shoes>
        </catalog>
    </xsl:template>

</xsl:stylesheet>

I am getting 1 "line" and 1 "brand" but I'm not getting the additional brands in each line. There should be at least two to three brands in each line. In this XML there are 2 lines.

Here is the XML structure that I want:

<?xml version="1.0" encoding="UTF-8"?>
<catalog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<lines>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
<brands>
    <brand>WALKING</brand>
<shoes>
<shoe>
    <style-name>NEW BALANCE-NB 475V2 (WIDE)</style-name>
    <color>Black</color>
    <price>67.99</price>
</shoe>
<shoe>
    <style-name>NEW BALANCE-496 (WIDE)</style-name>
    <color>Grey/Pink</color>
    <price>64.99</price>
</shoe>
</shoes>
</brands>
</lines>
<lines>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
<brands>
    <brand>CROSS TRANING</brand>
<shoes>
<shoe>
    <style-name>FILA-MEMORY PANACHE</style-name>
    <color>Black/Pink</color>
    <price>69.99</price>
</shoe>
<shoe>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
    <brand>RUNNING</brand>
    <style-name>FILA-VITALITY 2 TRAIL</style-name>
    <color>Grey/Prpl/Turq</color>
    <price>59.99</price>
</shoe>
</shoes>
</brands>
</lines>
<lines>
    <line>LINE 87: MENS BRANDED ATHLETIC</line>
<brands>
    <brand>CASUAL</brand>
<shoes>
<shoe>
    <style-name>LEVI'S-HAMILTON BUCK HI</style-name>
    <color>Black/Black</color>
    <price>34.99</price>
</shoe>
<shoe>
    <style-name>EVERLAST-EVAN SKATE</style-name>
    <color>Black</color>
    <price>29.99</price>
</shoe>
</shoes>
</brands>
</lines>
<lines>
  <line>LINE 87: MENS BRANDED ATHLETIC</line>
<brands>
  <brand>RUNNING</brand>
<shoes>
<shoe>
    <line>LINE 87: MENS BRANDED ATHLETIC</line>
<brands>
    <brand>RUNNING</brand>
<shoes>
<shoe>
    <style-name>SKECHERS-POWER SWITCH (WIDE)</style-name>
    <color>Black/White</color>
    <price>69.99</price>
</shoe>
<shoe>
    <style-name>SKECHERS-EQUALIZER GEL TOP </style-name>
    <color>Black</color>
    <price>69.99</price>
</shoe>
</shoes>
</brands>
</lines>
</catalog>

Upvotes: 1

Views: 109

Answers (2)

matthias_h
matthias_h

Reputation: 11416

With some adjustments to your XSLT the wanted result is generated:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8"/>
  <xsl:key name="shoes-by-line" match="shoe" use="line" />
  <xsl:key name="shoes-by-brand" match="shoe" use="concat(line,'#',brand )" />
  <xsl:template match="catalog">
    <catalog>
        <xsl:for-each select="shoe[count(. | key('shoes-by-line', line)[1]) = 1]">
            <lines>
                <line>
                    <xsl:value-of select="line"/>
                </line>
                <brands>
                    <brand>
                        <xsl:value-of select="brand"/>
                    </brand>
                    <shoes>
                        <xsl:for-each select="key( 'shoes-by-brand', concat(line,'#',brand ))">
                            <shoe>
                                <style-name>
                                    <xsl:value-of select="style-name"/>
                                </style-name>
                                <color>
                                    <xsl:value-of select="color"/>
                                </color>
                                <price>
                                    <xsl:value-of select="price"/>
                                </price>
                            </shoe>
                        </xsl:for-each>
                    </shoes>
                </brands>
            </lines>
        </xsl:for-each>
    </catalog>
  </xsl:template>
</xsl:stylesheet>

Your XSLT already generated the desired output, it was only necessary to move some grouping elements to a different position:

Before:

<catalog>
    <shoes>
       <xsl:for-each select="shoe[count(. | key('shoes-by-line', line)[1]) = 1]">
          <line>
             <xsl:value-of select="line"/>
          </line>
          <brands>
             <brand>
                <xsl:value-of select="brand"/>
             </brand>
                <xsl:for-each select="key('shoes-by-brand', 
                                      concat(line,'#',brand ))">
                   <shoe>
                        ...

Adjusted:

<catalog>
    <xsl:for-each select="shoe[count(. | key('shoes-by-line', line)[1]) = 1]">
        <lines>
            <line>
                <xsl:value-of select="line"/>
            </line>
            <brands>
                <brand>
                    <xsl:value-of select="brand"/>
                </brand>
                <shoes>
                    <xsl:for-each select="key('shoes-by-brand', 
                                          concat(line,'#',brand ))">
                        <shoe>
                           ...

Update: As noticed in the comment, above XSLT doesn't provide the correct output. Just as a different approach: following XSLT using xsl:for-each-group instead of keys:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8"/>
  <xsl:template match="catalog">
    <catalog>
        <xsl:for-each-group select="shoe" group-by="line">
            <xsl:for-each-group select="current-group()" group-by="brand">
                <lines>
                    <line>
                        <xsl:value-of select="line"></xsl:value-of>
                    </line>
                    <brands>
                        <xsl:for-each-group select="current-group()" group-by="brand">
                            <brand>
                                <xsl:value-of select="current-grouping-key()"></xsl:value-of>
                            </brand>
                            <shoes>
                                <xsl:for-each select="current-group()">
                                    <shoe>
                                        <style-name>
                                            <xsl:value-of select="style-name"/>
                                        </style-name>
                                        <color>
                                            <xsl:value-of select="color"/>
                                        </color>
                                        <price>
                                            <xsl:value-of select="price"/>
                                        </price>
                                    </shoe>
                                </xsl:for-each>
                            </shoes>
                        </xsl:for-each-group>
                    </brands>
                </lines>
            </xsl:for-each-group>
        </xsl:for-each-group>
    </catalog>
  </xsl:template>
</xsl:stylesheet>

when applied to your input XML produces the following output:

<?xml version="1.0" encoding="UTF-8"?>
<catalog>
  <lines>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
    <brands>
        <brand>WALKING</brand>
        <shoes>
            <shoe>
                <style-name>NEW BALANCE-NB 475V2 (WIDE)</style-name>
                <color>Black</color>
                <price>67.99</price>
            </shoe>
            <shoe>
                <style-name>NEW BALANCE-496 (WIDE)</style-name>
                <color>Grey/Pink</color>
                <price>64.99</price>
            </shoe>
        </shoes>
    </brands>
  </lines>
  <lines>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
    <brands>
        <brand>CROSS TRANING</brand>
        <shoes>
            <shoe>
                <style-name>FILA-MEMORY PANACHE</style-name>
                <color>Black/Pink</color>
                <price>69.99</price>
            </shoe>
        </shoes>
    </brands>
  </lines>
  <lines>
    <line>LINE 43: WOMENS BRANDED ATHLETIC</line>
    <brands>
        <brand>RUNNING</brand>
        <shoes>
            <shoe>
                <style-name>FILA-VITALITY 2 TRAIL</style-name>
                <color>Grey/Prpl/Turq</color>
                <price>59.99</price>
            </shoe>
        </shoes>
    </brands>
  </lines>
  <lines>
    <line>LINE 87: MENS BRANDED ATHLETIC</line>
    <brands>
        <brand>CASUAL</brand>
        <shoes>
            <shoe>
                <style-name>LEVI'S-HAMILTON BUCK HI</style-name>
                <color>Black/Black</color>
                <price>34.99</price>
            </shoe>
            <shoe>
                <style-name>EVERLAST-EVAN SKATE</style-name>
                <color>Black</color>
                <price>29.99</price>
            </shoe>
        </shoes>
    </brands>
  </lines>
  <lines>
    <line>LINE 87: MENS BRANDED ATHLETIC</line>
    <brands>
        <brand>RUNNING</brand>
        <shoes>
            <shoe>
                <style-name>SKECHERS-POWER SWITCH (WIDE)</style-name>
                <color>Black/White</color>
                <price>69.99</price>
            </shoe>
            <shoe>
                <style-name>SKECHERS-EQUALIZER GEL TOP </style-name>
                <color>Black</color>
                <price>69.99</price>
            </shoe>
        </shoes>
    </brands>
  </lines>
</catalog>

Note that there's one difference from the requested output as given in the OP - the brand RUNNING for the line LINE 43: WOMENS BRANDED ATHLETIC is a separate brand instead of being in the CROSS TRAINING brand, but maybe this is a little mistake in the wanted output. Otherwise it would be necessary to find a solution for getting RUNNING and CROSS TRAINING listed in the same brand.

As the shoes should be listed for each brand of each line, <xsl:for-each-group select="shoe" group-by="line"> selects first all shoes grouped by line. Then all brands in this group are selected by brand using <xsl:for-each-group select="current-group()" group-by="brand"> and all shoes of the brand processed in the loop <xsl:for-each select="current-group()">.

Update: As mentioned in the comments, this is an XSLT 2.0 stylesheet. I've just corrected the version in above template. Though I've tested the template with an online XSLT Test Tool having it declared as version 1.0, it didn't throw any error (though it should have).

Upvotes: 1

Jim Maivald
Jim Maivald

Reputation: 520

I revisited the XSLT I believe I found the solution using XSLT 1.0

<?xml version="1.0" encoding="UTF-8"?><!-- DWXMLSource="D76-shoebook.xml" -->
 <!DOCTYPE xsl:stylesheet>
 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="xml" encoding="UTF-8"/>

 <xsl:key name="shoes-by-line" match="shoe" use="line"/>
 <xsl:key name="shoes-by-brand" match="shoe" use="concat(line, '|', brand)"/>
 <xsl:template match="catalog">

 <catalog>
 <xsl:for-each select="shoe[generate-id() = generate-id(key('shoes-by-line', line)[1])]">
 <lines>
 <xsl:for-each select="key('shoes-by-line', line)[generate-id() = generate-id(key('shoes-by-brand', concat(line, '|', brand))[1])]">
 <line><xsl:value-of select="line"/></line><xsl:text>
 </xsl:text><brand><xsl:value-of select="brand"/></brand><xsl:text>
 </xsl:text><shoes>
 <xsl:for-each select="key('shoes-by-brand', concat(line, '|', brand))">
 <shoe>
 <style-name><xsl:value-of select="style-name" /></style-name>
 <color><xsl:value-of select="color" /></color><xsl:text>
 <price><xsl:value-of select="price" /></price><xsl:text>
 </xsl:text>                
 </shoe>        
            </xsl:for-each>
         </shoes>   
      </xsl:for-each>
</lines>
</xsl:for-each>
</catalog>
</xsl:template>
</xsl:stylesheet>

This produces the proper grouping and nesting from the source XML keying off LINE and then on BRAND.

Upvotes: 0

Related Questions