How to use XSLT to create distinct values

Xslt

Xslt Problem Overview


I have XML like this:

<items>
  <item>
    <products>
      <product>laptop</product>
      <product>charger</product>
    </products>
  </item>
  <item>
    <products>
      <product>laptop</product>
      <product>headphones</product>  
    </products>  
  </item>
</items>

I want it to output like

laptop
charger
headphones

I was trying to use distinct-values() but I guess i m doing something wrong. Can anyone tell me how to achieve this using distinct-values()? Thanks.

<xsl:template match="/">			
  <xsl:for-each select="//products/product/text()">
    <li>
      <xsl:value-of select="distinct-values(.)"/>
    </li>				
  </xsl:for-each>
</xsl:template>

but its giving me output like this:

<li>laptop</li>
<li>charger</li>
<li>laptop></li>
<li>headphones</li>

Xslt Solutions


Solution 1 - Xslt

An XSLT 1.0 solution that uses key and the generate-id() function to get distinct values:

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

<xsl:key name="product" match="/items/item/products/product/text()" use="." />

<xsl:template match="/">

  <xsl:for-each select="/items/item/products/product/text()[generate-id()
                                       = generate-id(key('product',.)[1])]">
    <li>
      <xsl:value-of select="."/>
    </li>
  </xsl:for-each>

</xsl:template>

</xsl:stylesheet>

Solution 2 - Xslt

Here's an XSLT 1.0 solution that I've used in the past, I think it's more succinct (and readable) than using the generate-id() function.

  <xsl:template match="/">           
    <ul> 
      <xsl:for-each select="//products/product[not(.=preceding::*)]">
        <li>
          <xsl:value-of select="."/>
        </li>   
      </xsl:for-each>            
    </ul>
  </xsl:template>

Returns:

<ul xmlns="http://www.w3.org/1999/xhtml">
  <li>laptop</li>
  <li>charger</li>
  <li>headphones</li>
</ul>

Solution 3 - Xslt

You don't want "output (distinct-values)", but rather "for-each (distinct-values)":

<xsl:template match="/">              
  <xsl:for-each select="distinct-values(/items/item/products/product/text())">
    <li>
      <xsl:value-of select="."/>
    </li>
  </xsl:for-each>
</xsl:template>

Solution 4 - Xslt

I came to this problem while working with a Sitecore XSL rendering. Both the approach that used key() and the approach that used the preceding axis performed very slowly. I ended up using a method similar to key() but that did not require using key(). It performs very quickly.

<xsl:variable name="prods" select="items/item/products/product" />
<xsl:for-each select="$prods">
  <xsl:if test="generate-id() = generate-id($prods[. = current()][1])">
    <xsl:value-of select="." />
    <br />
  </xsl:if>
</xsl:for-each>

Solution 5 - Xslt

distinct-values(//product/text())

Solution 6 - Xslt

I found that you can do what you want with XSLT 1.0 without generate-id() and key() functions.

Here is Microsoft-specific solution (.NET's XslCompiledTransform class, or MSXSLT.exe or Microsoft platfocm COM-objects).

It is based on this answer. You can copy sorted node set to variable ($sorted-products in the stylesheet below), then convert it to node-set using ms:node-set function. Then you able for-each second time upon sorted node-set:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:ms="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="ms">

  <xsl:output method="html" indent="yes" />

  <xsl:template match="/">
    <xsl:variable name="sorted-products">
        <xsl:for-each select="//products/product">
            <xsl:sort select="text()" />
            
            <xsl:copy-of select=".|@*" />
        </xsl:for-each>
    </xsl:variable>
    
    <xsl:variable name="products" select="ms:node-set($sorted-products)/product" />

    <xsl:for-each select="$products">
      <xsl:variable name='previous-position' select="position()-1" />
      
      <xsl:if test="normalize-space($products[$previous-position]) != normalize-space(./text())">
        <li>
          <xsl:value-of select="./text()" />
        </li>
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
  
</xsl:stylesheet>

output:

<li>charger</li>
<li>headphones</li>
<li>laptop</li>

You can try it out in online playground.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAB.View Question on Stackoverflow
Solution 1 - XsltMads HansenView Answer on Stackoverflow
Solution 2 - XsltNick GrealyView Answer on Stackoverflow
Solution 3 - XsltTomalakView Answer on Stackoverflow
Solution 4 - XsltFelix SteinyView Answer on Stackoverflow
Solution 5 - XsltJim GarrisonView Answer on Stackoverflow
Solution 6 - XslthalView Answer on Stackoverflow