tags:

views:

5446

answers:

4

I found this page describing the Muenchian method, but I think I'm applying it wrong.

Consider that this would return a set of ages:

/doc/class/person/descriptive[(@name='age')]/value

1..2..2..2..3..3..4..7

But I would like a nodeset only one node for each age.

1..2..3..4..7

Each of these seem to return all of the values, instead of unique values:

/doc/class/person/descriptive[(@name='age')][not(value=preceding-sibling::value)]/value
/doc/class/person/descriptive[(@name='age')]/value[not(value=preceding-sibling::value)]

What am I missing?

A: 

Aren't you missing a reference to 'descriptive' right after the preceding-value? Some thing like the following:

/doc/class/person/descriptive[(@name='age')][not(value=preceding-sibling::descriptive[@name='age']/value)]/value

(Haven't tested it)

JacobE
+3  A: 

Here's an example:

<root>
    <item type='test'>A</item>
    <item type='test'>B</item>
    <item type='test'>C</item>
    <item type='test'>A</item>
    <item type='other'>A</item>
    <item type='test'>B</item>
    <item type='other'>D</item>
    <item type=''>A</item>
</root>

And the XPath:

//preceding::item/preceding::item[not(.=preceding-sibling::item)]/text()

Results: A B C D

BQ
Obviously, you can use additional XPath to restrict based on the type attribute or other data in your actual file. I just had it in there during my quick test.
BQ
Also note that "item" in the XPath isn't a keyword, it's the name of the element in the XML document that the preceding:: and preceding-sibling:: axes are working on.
BQ
+3  A: 

Here is the Muenchian version of BQ's answer using his data:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

  <xsl:output indent="yes" method="text"/>
  <xsl:key name="item-by-value" match="item" use="."/>

  <xsl:template match="/">
    <xsl:apply-templates select="/root/item"/>
  </xsl:template>

  <xsl:template match="item">
    <xsl:if test="generate-id() = generate-id(key('item-by-value', normalize-space(.)))">
      <xsl:value-of select="."/>
      <xsl:text>
</xsl:text>
    </xsl:if>
  </xsl:template>

  <xsl:template match="text()">
    <xsl:apply-templates/>
  </xsl:template>
</xsl:stylesheet>

This transform gives

A
B
C
D

  1. The key() lookup above in the template for item returns a nodeset containing all the item elements with the same string value as the context node.
  2. If you apply a function that expects a single node to a nodeset, it will operate on the first node in that nodeset.
  3. All calls to generate-id() are guaranteed to generate the same ID for a given node during a single pass through a document.
  4. Therefore, the test will be true if the context node is the same node as the first one returned by the key() call.
ChuckB
with a little tweaking of the match and use parameters this worked like a charm inside a for-each element; thanks!
David Alpert
A: 

The Muenchian method uses keys to create a unique list of items from the node set. For your data, the key would look like this:

<!-- Set the name to whatever you want -->
<xsl:key name="PeopleAges" match="/doc/class/person/descriptive[@name = 'age']/value" use="." />

From there, I would personally use xsl:apply-templates but you can use the following select attribute in other places:

<!-- you can change `apply-templates` to: `copy-of` or `for-each`. -->
<xsl:apply-templates select="/doc/class/person/descriptive[@name = 'age']/value[count(. | key('PeopleAges', .)[1]) = 1]" />

The accompanying match for the above is much simpler:

<xsl:template match="person/descriptive[@name = 'age']/value">
    <strong>Age: </strong><xsl:value-of select="." />
</xsl:template>
sirlancelot