Reputation:
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
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