Predicates and functions

Filtering node-sets with predicates, positional versus value tests, and the string, number and boolean functions you will use most.

Predicates filter a node-set

A predicate in square brackets keeps only the nodes for which the expression is true. Inside a predicate, the context is the node being tested — so a bare path is relative to it, not to the document.

//book[price > 20]                      value test
//book[@id]                             has the attribute
//book[not(@id)]                        lacks the attribute
//book[position() = 1]                  same as //book[1], per parent
//book[last()]                          the last book child of each parent
//book[position() <= 3]                 the first three
//book[title = 'Alpha']                 exact text comparison
//book[contains(title, 'lph')]          substring test
//book[starts-with(@id, 'bk-10')]       prefix test
//book[price > 20 and @available='true']  boolean combination
ComparisonMeaning in XPath 1.0
=True if any pair of values is equal — not an identity test
!=True unless every pair is equal, which surprises almost everyone
< > <= >=Numeric comparison after conversion to number
and or not()Boolean logic
|Union of two node-sets
⚠️
The != operator is not the negation of = when node-sets are involved. Use not(price = 20) when you mean 'no price equals 20'.

The functions you will actually use

CategoryFunctionsPurpose
Stringstring() concat() substring() substring-before() substring-after() string-length() normalize-space() translate()Cutting and cleaning text
Numbernumber() sum() count() floor() ceiling() round()Arithmetic over node-sets
Booleanboolean() true() false() not() lang()Conditions and type coercion
Node-setlast() position() local-name() name() namespace-uri()Filters that need context
count(//book)                                  how many books
sum(//book/price)                              total price as a number
round(sum(//book/price) * 100) / 100           two-decimal total
normalize-space(//book[1]/title)               trim and collapse spaces
concat(//book[1]/title, ' - ', //book[1]/price)  join into one string
substring-before(//book[1]/title, ' ')         text before the first space
translate(//book[1]/title, 'abc', 'ABC')       character-by-character map
string-length(//book[1]/title) > 4             boolean result
  • substring() in XPath 1.0 is one-based, so substring('abc', 1, 2) is ab.
  • translate() maps characters, not sequences — it cannot replace a word.
  • There is no regular expression support in XPath 1.0; 2.0 adds matches(), replace() and tokenize().
  • Comparing a number to a string converts the string to a number, and an unparsable string becomes NaN, which compares false against everything — including itself.

Debugging an empty result

  1. Check well-formedness first — a parse error looks like an empty node-set in some tools.
  2. Test the path one step at a time: /catalog, then /catalog/book, then the predicate.
  3. Confirm the namespace mapping: an unprefixed name never matches a default namespace.
  4. Verify the predicate is not applied per parent when you expected document-wide selection.
  5. Print count() rather than the value, so an empty result is unambiguous.

FAQ

What does <code>//book[1]</code> versus <code>(//book)[1]</code> really change?
The first filters per parent, the second takes the document-wide first item. This single pair of characters causes a large share of XPath bugs.
Can XPath do case-insensitive matching?
Not directly in 1.0. Fold both sides with translate(), or use XPath 2.0's matches() with the case-insensitive flag.

Location paths and axes Advanced matching and sorting

Last refreshed 2026-09-18.