" /> " /> "/>

Counting distinct items in XSLT and listing only once

6.3k Views Asked by At

I have the following XML:

<assessment>
    <section>
        <item>
            <attributes>
                <variables>
                    <variable>
                        <variable_name value="MORTIMER"/>
                    </variable>
                </variables>
            </attributes>
        </item>
        <item>
            <attributes>
                <variables>
                    <variable>
                        <variable_name value="FRED"/>
                    </variable>
                </variables>
            </attributes>
        </item>
        <item>
            <attributes>
                <variables>
                    <variable>
                        <variable_name value="MORTIMER"/>
                    </variable>
                </variables>
            </attributes>
        </item>
    </section>
</assessment>

I have the following XSLT to process that XML:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>

 <xsl:key name="kValueByVal" match="item//variables//variable_name" 
          use="@value"/>

 <xsl:template match="assessment">
     <xsl:for-each select="
      .//item//variables//variable_name/@value
      ">
        <xsl:value-of select=
        "concat(., ' ', count(key('kValueByVal', .)), '&#xA;')"/>
         <br/>
     </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

It outputs the following, which is almost what I want:

MORTIMER 2
FRED 1
MORTIMER 2

It lists each of the variable_names and how many times each occurs. The only problem is that it gives this count once for each time the variable_name occurs instead of only once.

This is what I want it to output:

MORTIMER 2
FRED 1

How do I modify the XSLT code to give me that? Note that we're using XSLT 1.0.

The following solution, which seems like it should work, outputs nothing:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>

 <xsl:key name="kValueByVal" match="item//variables//variable_name"
          use="@value"/>

 <xsl:template match="assessment">
     <xsl:for-each select=".//item//variables//variable_name/@value[generate-id()
                                                                    =
                                                                    generate-id(key('kValueByVal',.)[1])]">
        <xsl:value-of select=
        "concat(., ' ', count(key('kValueByVal', .)), '&#xA;')"/>
         <br/>
     </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>
5

There are 5 best solutions below

1
Dimitre Novatchev On BEST ANSWER

You really need to understand how the Muenchian grouping works, otherwise you'd be asking variations of the same questions forever.

Do read Jeni Tennison's tutorial.

Here is a solution for your latest question:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>

 <xsl:key name="kVarNameByVal" match="variable_name"
          use="@value"/>

 <xsl:template match=
  "variable_name[generate-id()
                =
                 generate-id(key('kVarNameByVal', @value)[1])
                ]
  ">
        <xsl:value-of select=
        "concat(@value, ' ', count(key('kVarNameByVal', @value)), '&#xA;')"/>
         <br/>
 </xsl:template>
</xsl:stylesheet>

When this transformation is performed on the provided XML document, the wanted result is produced:

MORTIMER 2
FRED 1
3
Matt Gibson On

Seeing as you're using XSLT 2.0, I'd use the built-in grouping features. (For earlier versions, you'd probably want to look into Muenchian grouping.)

<?xml version="1.0" ?>
<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>

 <xsl:key name="kValueByVal" match="item//variables//variable_name" 
          use="@value"/>

 <xsl:template match="assessment">
     <xsl:for-each-group select=".//item//variables//variable_name" group-by="@value">
                <xsl:value-of select="concat(current-grouping-key(), ' ', count(current-group()), '&#xA;')"/>
        <br/>
     </xsl:for-each-group>
 </xsl:template>
</xsl:stylesheet>
1
AudioBubble On

You get what you are asking for:

<xsl:for-each select=".//item//variables//variable_name/@value"> 

Wich means: for each one of these attributes

When grouping, you must say: for each one of these one of a kind

And, how do you know wich are one of a kind? With Muenchian method:

<xsl:for-each select=".//item//variables//variable_name/@value[generate-id()
                                                               =
                                                               generate-id(key('kValueByVal',.)[1])]">

That means: the ones been the first with that key.

EDIT: Also, avoid // when you know input schema.

EDIT: Now I can see that you change the key... So, for you new key, who is first of a kind? Yes! variable_name element:

<xsl:for-each select=".//item//variables//variable_name[generate-id()
                                                        =
                                                        generate-id(key('kValueByVal',@value)[1])]">
0
Paul Reiners On
<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>

 <xsl:key name="kValueByVal" match="item//variables//variable_name" 
          use="@value"/>

 <xsl:template match="assessment">
     <xsl:for-each 
              select="//item//variables//variable_name[
                          generate-id() = 
                              generate-id(key('kValueByVal', @value)[1])]">
        <xsl:value-of select=
        "concat(./@value, ' ', count(key('kValueByVal', ./@value)), '&#xA;')"/>
         <br/>
     </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>
0
harpo On

This will do it in XSLT 1.0.

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>

 <xsl:key name="kValueByVal" match="item//variables//variable_name" 
          use="@value"/>

 <xsl:template match="assessment">
     <xsl:for-each select="
      //item//variables//variable_name[not(@value=ancestor::item/preceding-sibling::item//variables//variable_name/@value)]
      ">
        <xsl:value-of select=
        "concat(@value, ' ', count(key('kValueByVal', @value)), '&#xA;')"/>
         <br/>
     </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

The output I get is

MORTIMER 2
<br />FRED 1
<br />

Note that it assumes a bit more about the document structure (the ancestor::item bit), but you should be able to take it from there.