57

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>
1

6 Answers 6

61

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>
Sign up to request clarification or add additional context in comments.

9 Comments

While appreciating the answer above which is completely applicable for the original case, just wanted to note the above approach is not applicable for slightly more complex schema, where each product has it's own elements, for example: <products> <product><name>charger</name></product> <product><name>laptop</name></product> ... I was not able to find distinct names for such layout, maybe reaching xslt1.0 limitations here...
Really @R.Simac? The following xpath should give you the products, with the first instance of a name (if this is what you want?)... //product[not(./name=preceding::*/name)]. I believe that it may not work for all scenarios, perhaps you can you provide an example where it doesn't work?
@NickG ... it was one of these 'it does not work for me (tm)' situations... For example, your suggestion does not produce any output for following xml (sorry for ugly formatting, time constrained): <items> <item> <products> <product><name>laptop</name></product> <product><name>charger</name></product> </products> </item> <item> <products> <product><name>laptop</name></product> <product><name>headphones</name></product> <product><name>charger</name></product> </products> </item> </items>
@NickG I stand corrected. It does work. It was my xsl processor setup (eclipse) that is to blame along with me doing heavy multitasking. Also thanks for the online xslt processor site link, did not know about it...
+1. I needed to output also a sibling of the <product>. This solution worked for me, and the one with generate-id() did not
|
59

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>

4 Comments

This is working perfectly only for finding distinct elements under entire / namespace. If the goal is to find distincts under cretain subtrees like in <warehouse name="a"><items...><warehouse name="b"> then the global key approach is no longer valid...
@R. Simac - you can adjust the match expression for the key to match a different set of items.
@Mads, I was thinking of being unable to set the key match 'dynamically'. I don't know how to use/instruct key to match only the distinct items under warehouse X...
I needed to output also a sibling of the <product>. This solution did not work for me, and the one with preceding did.
21

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>

2 Comments

Tomalak xslt 2.0 is not supported by browser...just came to knw...while testing ...any way to do it without xslt 2.0
@AB - I've added an XSLT 1.0 solution
17

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>

1 Comment

This worked for me. It was nice to be able to keep things tightly encapsulated within the stylesheet. Adding <xsl:apply-templates select = ".." mode="summary"/> in place of the value-of allowed me to apply the template to only the specific node.
10

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

3 Comments

@Tomalak, "exponential"? No, only linear in the number of element nodes and any-type leaf nodes in the XML document.
I cannot get this to work, my compiler(eclipse) is complaining that this is invalid XPath.
@Nicholas This is for XSLT 2.0 but you are working with an XSLT 1.0 processor. You have to use an <xsl:key>, like the accepted answer does.
0

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.

1 Comment

Not a good solution - read here why: jenitennison.com/xslt/grouping/muenchian.html

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.