56

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>
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
AB.
  • 849
  • 4
  • 14
  • 23
  • Might be worth looking at this related question: http://stackoverflow.com/questions/1813286/xslt-select-distinct-but-slightly-different-to-other-examples – lc. Feb 18 '10 at 19:33

6 Answers6

57

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>
Nick Grealy
  • 24,216
  • 9
  • 104
  • 119
  • 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: charger laptop ... I was not able to find distinct names for such layout, maybe reaching xslt1.0 limitations here... – R. Simac Jul 28 '14 at 16:44
  • 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? – Nick Grealy Jul 28 '14 at 23:55
  • @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): laptop charger laptop headphones charger – R. Simac Jul 29 '14 at 13:27
  • 3
    @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... – R. Simac Jul 30 '14 at 12:41
  • @NickG, it appears I finnally pinpointed the actual problem case that is/was bothering me. It has to do with what I refer to as non-continuous element lists (there may be better name for it). At any rate, the attached sample is definitely not working. Any comments appreciated. http://www.xsltcake.com/slices/lB8i4j/5 – R. Simac Jul 30 '14 at 19:08
  • 1
    +1. I needed to output also a sibling of the . This solution worked for me, and the one with `generate-id()` did not – Andrej Adamenko Sep 09 '14 at 06:01
  • Not a good solution - read here why: http://www.jenitennison.com/xslt/grouping/muenchian.html – michael.hor257k Apr 26 '19 at 13:39
  • @michael.hor257k - "Not a good solution" - that's your opinion, but you should probably elaborate. The extra developer effort (time and cost) to recall, understand and write the ` – Nick Grealy Apr 28 '19 at 12:31
  • Jeni Tennison already elaborated, many years ago. Muenchian grouping is a well-established design pattern in XSLT 1.0. – michael.hor257k Apr 28 '19 at 12:51
57

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>
Mads Hansen
  • 63,927
  • 12
  • 112
  • 147
  • This is working perfectly _only_ for finding distinct elements under entire / namespace. If the goal is to find distincts under cretain subtrees like in then the global key approach is no longer valid... – R. Simac Jul 28 '14 at 16:08
  • 1
    @R. Simac - you can adjust the match expression for the key to match a different set of items. – Mads Hansen Jul 29 '14 at 00:37
  • @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... – R. Simac Jul 29 '14 at 13:12
  • I needed to output also a sibling of the . This solution did not work for me, and the one with `preceding` did. – Andrej Adamenko Sep 09 '14 at 06:04
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>
Tomalak
  • 332,285
  • 67
  • 532
  • 628
16

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>
Felix Steiny
  • 191
  • 1
  • 2
  • 1
    This worked for me. It was nice to be able to keep things tightly encapsulated within the stylesheet. Adding in place of the value-of allowed me to apply the template to only the specific node. – kjl May 19 '14 at 17:45
10

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

Mads Hansen
  • 63,927
  • 12
  • 112
  • 147
Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
  • @Tomalak, "exponential"? No, only *linear* in the number of element nodes and any-type leaf nodes in the XML document. – Dimitre Novatchev Oct 11 '12 at 22:01
  • I cannot get this to work, my compiler(eclipse) is complaining that this is invalid XPath. – Nicholas Jun 06 '13 at 21:31
  • 2
    @Nicholas This is for XSLT 2.0 but you are working with an XSLT 1.0 processor. You have to use an ``, like the [accepted answer](http://stackoverflow.com/a/2293626/18771) does. – Tomalak Jun 07 '13 at 06:35
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.

hal
  • 1,705
  • 1
  • 22
  • 28