concat(…) / string-join(…)Strings — build one string out of several values
Use `concat()` when you have a fixed number of parts, and `string-join()` when you have a sequence of unknown length. `concat()` accepts only single items, so passing a sequence of more than one node raises an error — wrap it in `string-join()` instead.
Example: concat($r/name, ' (', $r/cuisine, ')')
substring / contains / starts-withStrings — test and slice text
`contains()`, `starts-with()` and `ends-with()` return booleans and are the everyday way to filter on text. `substring()` is 1-based, and `substring-before()` / `substring-after()` split around a delimiter — handy for pulling an ID out of a compound attribute value.
Example: //recipe[contains(name, 'Curry')]/name
upper-case / lower-case / normalize-spaceStrings — normalise before comparing
XPath and XQuery string comparison is case-sensitive and whitespace-sensitive, so most 'my query returns nothing' bugs are fixed by wrapping both sides in `lower-case()` and `normalize-space()`. `normalize-space()` collapses internal runs of whitespace and trims the ends — essential when matching text that was pretty-printed across lines.
Example: //recipe[lower-case(cuisine) = 'thai']
tokenize / replace / matchesStrings — regular expressions
`matches()` tests, `replace()` rewrites, and `tokenize()` splits — all three take XPath regex syntax, which is close to but not identical to Perl or JavaScript. Escape backslashes carefully inside string literals, and remember that `replace()` uses `$1` for capture groups.
Example: tokenize(//album[1]/@genre, ',\s*')
count / sum / avg / min / maxNumbers — aggregate a sequence
These atomise their argument, so they work directly on attribute or element nodes without an explicit conversion in most cases. `count()` is the only one that never errors on non-numeric input, so use it when you just need cardinality. On an empty sequence, `count()` returns 0 while `sum()` returns 0 and `avg()` returns the empty sequence.
Example: sum(//album/tracks/track/@number)
round / floor / ceiling / round-half-to-evenNumbers — control rounding
`round()` rounds halves upward, which quietly biases financial totals; `round-half-to-even()` is the banker's-rounding alternative and takes an optional precision argument. `format-number()` is the one to use when the output is for display rather than further arithmetic.
Example: round-half-to-even(avg(//album/@year), 0)
distinct-values / reverse / subsequenceSequences — reorder and trim
`subsequence($seq, $start, $length)` is the 1-based way to page through a sequence and reads better than a positional predicate for anything past the first item. `reverse()` flips order without sorting, and `distinct-values()` removes duplicates after atomisation — so it returns values, never nodes.
Example: subsequence(reverse(//recipe/name), 1, 2)
empty / exists / head / tailSequences — guard before you compute
`empty()` and `exists()` are the readable alternatives to comparing `count(...)` against zero, and they let the engine stop early. `head()` and `tail()` split the first item off a sequence, which is the idiomatic base case for a recursive `declare function`.
Example: if (exists(//recipe[cuisine = 'Thai'])) then 'yes' else 'no'
for-each / filter / fold-leftSequences — higher-order functions
These take function items as arguments and are shared with XPath 3.1. In XQuery they are often the shorter form of a whole FLWOR: `filter()` replaces a `where`, `for-each()` replaces a simple `for ... return`, and `fold-left()` expresses accumulations that would otherwise need recursion.
Example: for-each(//recipe/name, function($n) { upper-case($n) })
current-date / xs:date castsDates and numbers — cast untyped text before comparing
Untyped XML text is not a date or a number until you cast it: `xs:date($v)`, `xs:integer($v)` or `$v cast as xs:date`. Once typed, dates support comparison and subtraction (yielding a duration), and `current-date()` gives you today for relative tests. Note that this engine does not implement `format-date()` — build display strings with `concat()` and `substring()` instead.
Example: for $a in //album return xs:integer($a/@year) + 1
name / local-name / namespace-uriNodes — inspect names and namespaces
`local-name()` ignores prefixes entirely, which is why it is the standard escape hatch for documents in a default namespace. `name()` returns the prefixed name as written, and `namespace-uri()` gives the URI a prefix resolves to — useful for confirming a document really is namespaced before you fight with prefixes.
Example: //*[local-name() = 'recipe']/name
data / string / node-nameNodes — convert between nodes and values
`string()` gives you the string value of a node, `data()` gives its typed value (a string for untyped XML), and both are how you stop a result from being a node when you want text. In a constructed attribute the conversion happens implicitly, which is why `id="{$r/@id}"` works without a wrapper.
Example: for $r in //recipe return data($r/@difficulty)