user283145
user283145

Reputation:

XPath/XSLT: how to select all elements that satisfy a condition involving another set of elements

I have an XML document similar to the following:

<tt>
  <a text="1"/>
  <a text="2"/>
  ...
  <a text="n"/>

  <b text="14">data</b>
  <b text="2">data</b>
  ...
</tt>

How can I select all <b> elements that have text attribute not equal to the text attribute of any of the <a> elements? I'm using XPath 1.0.

I've thinking about something like tt/b[not (tt/a[@text = xxx::@text])], where xxx should refer to the tt/b element being examined. I don't know how exactly it may be done.

Upvotes: 2

Views: 4739

Answers (1)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243599

An answer such as /tt/b[@text != ../a/@text] is wrong and selects the wrong set of nodes:

<b text="14">data</b>
<b text="2">data</b>

As we see, the second selected node's text attribute is 2 and there is an a element whose text attribute is 2.

Here is a correct XPath expression:

/tt/b[not(@text = ../a/@text)]

When evaluated against the provided XML document:

<tt>
  <a text="1"/>
  <a text="2"/>
  ...
  <a text="n"/>

  <b text="14">data</b>
  <b text="2">data</b>
  ...
</tt>

it correctly selects only one node:

<b text="14">data</b>

Explanation:

By definition the XPath != operator has a very unintuitive behavior whenever at least one of its arguments is a node-set:

From the W3C XPath 1.0 Recommendation:

"If one object to be compared is a node-set and the other is a number, then the comparison will be true if and only if there is a node in the node-set such that the result of performing the comparison on the number to be compared and on the result of converting the string-value of that node to a number using the number function is true. If one object to be compared is a node-set and the other is a string, then the comparison will be true if and only if there is a node in the node-set such that the result of performing the comparison on the string-value of the node and the other string is true"

In this particular case for the element:

<b text="2">data</b>

The comparison:

@text != ../a/@text

is true() even though there exists:

<a text="2"/> 

because there exist at least one ../a element (and actually more than one), the string (or numeric) value of whose text attribute isn't equal to "2".

This is a well-known fact and a FAQ: Always avoid using the != operator unless you absolutely know what you are doing!

The correct solution of this problem is to use the not() function like this:

not(@text = ../a/@text)

This expression evaluates to true() only if @text = ../a/@text is false() -- that is only if there isn't even a single ../a/@text whose string value is equal to the string value of the text attribute of the context node.

XSLT-based verification:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/">
  <xsl:copy-of select="/tt/b[not(@text = ../a/@text)]"/>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the provided XML document (above), the correct result is produced:

<b text="14">data</b>

Upvotes: 7

Related Questions