Advanced XPath 3 expressions¶
The XPath section covers 1.0 — location paths, axes,
predicates, the four-type model. XPath 2.0 and 3.1 (the version XSLT 3.0
carries) add a layer of expression syntax on top of paths: ways to map, bind,
quantify, branch, pipe, and test types inline, without an xsl:for-each or an
xsl:variable for every intermediate step.
This page is a tour of the expressions you will actually reach for, built on the familiar catalog:
<catalog>
<cd genre="rock"><title>Empire Burlesque</title><artist>Bob Dylan</artist><price>10.90</price><year>1985</year></cd>
<cd genre="pop"><title>Hide your heart</title><artist>Bonnie Tyler</artist><price>9.90</price></cd>
<cd genre="country"><title>Greatest Hits</title><artist>Dolly Parton</artist><price>9.90</price></cd>
</catalog>
The ! simple map operator¶
! is the simple map operator, added in XPath 3.0. A ! B evaluates the
expression B once for every item in A, with that item as the context
item, and concatenates the results into one sequence:
- For each
cd, evaluatetitle— the three title elements. - For each
cd, computeupper-case(title)— three strings. .is the current item; this yields1, 4, 9. The left side is atomics, not nodes — something/cannot do.
(1, 2, 3) ! (. * .) → 1 4 9
! versus /¶
They look similar — both "do something for each item on the left" — but they are
not interchangeable. The path operator / does three things that ! does
not:
/ (path step) |
! (simple map) |
|
|---|---|---|
| Right side must yield | nodes | anything (atomics, maps, nodes…) |
| Result order | document order | iteration order (left-to-right) |
| Duplicate nodes | removed | kept |
Three consequences fall out of that table:
(1, 2, 3) ! (. * 2) (1)
(1, 2, 3) / (. * 2) (2)
catalog//cd ! title (3)
(catalog/cd | catalog/cd) (4)
2, 4, 6— the left side is atomic, and!is happy with that.- Error. The left operand of
/must be a sequence of nodes; an atomic value on the left is not allowed. This is the cleanest reason!exists. - Order matters: a path like
a//breturns nodes in document order even when the navigation reached them out of order.!instead keeps the left-to-right iteration order of its input. - Union sorts into document order and removes duplicates → 3 nodes. The
same nodes mapped with
!(e.g.catalog/cd ! .twice over) keep every occurrence —!never deduplicates.
Rule of thumb
Use / when you are navigating to nodes and want document order with no
duplicates — the normal case. Reach for ! when the per-item result is a
computed value (a number, a string, a map) or when you specifically want
to keep order and duplicates. catalog/cd ! string-length(title) is the
idiomatic "title length of each CD."
Don't confuse it with != or the annotation (1)!
!= is the not-equal comparison — a different token entirely. And the
<!-- (1)! --> markers in this book's code samples are documentation
annotations, not XPath. The simple-map ! is always binary: something on
each side.
The => arrow operator¶
=> (3.0) feeds the value on its left in as the first argument of the
function call on its right. It turns nested calls inside-out so they read in the
order data flows:
<!-- nested, read inside-out: -->
string-join(sort(distinct-values(catalog/cd/@genre)), ', ')
<!-- piped, read left-to-right: -->
catalog/cd/@genre => distinct-values() => sort() => string-join(', ')
country, pop, rock
Both forms are identical to the processor; the arrow is purely about
readability. It pairs naturally with !: map to values, then pipe them onward —
catalog/cd ! xs:decimal(price) => sum(). See
higher-order functions for => with function
values.
for — mapping with a bound variable¶
! maps with .; for $x in … return … maps with a named variable, which
you need when expressions nest and . would be ambiguous. The result is again a
flattened sequence:
Empire Burlesque (rock), Hide your heart (pop), Greatest Hits (country)
Multiple in clauses make a cross product, evaluated in nested-loop order —
handy for pairing every item with every other:
1x, 1y, 2x, 2y— the second variable varies fastest.
let — naming a subexpression inline¶
let $v := … return … binds a value without an xsl:variable, so you can name
a repeated subexpression right inside one XPath:
let $avg := avg(catalog/cd/xs:decimal(price))
return catalog/cd[xs:decimal(price) gt $avg]/title (1)
- Compute the average once, then select the titles priced above it →
Empire Burlesque. Withoutlet, you would repeat the wholeavg(…)inside the predicate.
let and for compose freely: for $cd in catalog/cd let $p := xs:decimal($cd/price) return ….
some / every — quantified tests¶
These answer "any?" and "all?" over a sequence, returning a single boolean:
some $p in catalog/cd/price satisfies xs:decimal($p) gt 10 (1)
every $p in catalog/cd/price satisfies xs:decimal($p) gt 0 (2)
true— at least one CD costs more than 10.true— all prices are positive.
They make the 1.0 node-set = trap explicit: instead of relying on the
existential quirk of price = '9.90', you say which quantifier you mean.
if/then/else — a conditional that is an expression¶
XPath's own conditional returns a value, so it slots inside a larger expression —
no xsl:choose needed for a simple branch. The else is mandatory:
premium, standard, standard
Type expressions: instance of, cast, treat¶
The sequence type system (see sequences and types) shows up inline as four operators:
$x instance of xs:integer+ (1)
$s castable as xs:date (2)
xs:decimal(price) (3)
$node treat as element(cd) (4)
- Boolean: is
$xone-or-more integers? A safe guard before using a value. - Boolean: would casting
$sto a date succeed? Test before youcastto avoid a runtime error. - An actual cast —
xs:decimal(price)turns the price text into a typed decimal so arithmetic and comparison behave numerically. - An assertion to the type checker that
$nodeis acd— changes the static type, not the value; errors at runtime if untrue.
The ? lookup operator¶
For maps and arrays (and JSON), ? reaches into a
structure by key or position, and unary ?* takes everything:
- The value under key
titlein a map. - The first member of an array (arrays are 1-based).
- The
priceof every map in an array —?*fans out, then?pricemaps over the results. This is how you walk parsed JSON.
Putting it together¶
A single expression that combines several of these — average price per genre, formatted, sorted, as one string — shows why the operators earn their keep:
distinct-values(catalog/cd/@genre)
=> sort()
! (let $g := .
return concat($g, ': ',
avg(catalog/cd[@genre = $g] ! xs:decimal(price))))
=> string-join('; ')
country: 9.9; pop: 9.9; rock: 10.9
Read it as a pipeline: the genres, deduplicated and sorted, then mapped
(!) — binding each to $g with let so the inner predicate can reuse it — to
a "genre: average" string, then joined. The same result in 1.0 would take a
Muenchian key, a sort, and several templates.
Where to go next¶
- Sequences and types — the model these operators compute over.
- Higher-order functions —
=>with function values,fold-left,filter. - Maps and arrays — the
?lookup operator in full. - Regular expressions and strings —
matches,replace,tokenize.