Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Maps, Arrays and Records

Documents are trees, but a lot of data is not: a lookup table, a list of rows, the response of a web service in JSON. XPath 4.0 has two structures for such data. A map associates keys with values, and an array is a list of values by position. Both are items, so they can be in sequences, be passed to functions and be put inside each other.

Maps

A map is written with curly braces, with a key and a value for each entry:

$ sxml xpath '{ "anchor": "req-7", "keyword": "MUST" }'
{"anchor":"req-7","keyword":"MUST"}

(You can also write map { ... }, as XPath 3.1 required.) The ? operator looks up a key:

$ sxml xpath 'let $requirement := { "anchor": "req-7", "keyword": "MUST" } return $requirement?keyword'
"MUST"

A key that is not in the map gives the empty sequence, and ?* gives all the values:

$ sxml xpath 'let $requirement := { "anchor": "req-7", "keyword": "MUST" } return ($requirement?anchor, $requirement?section, "and", $requirement?*)'
"req-7"
"and"
"req-7"
"MUST"

The keys are atomic values: strings, numbers, dates and so on. A map is also a function from its keys to its values, so $requirement("keyword") is the same lookup; that is how the month map of Values and Types was used. Maps are values, so they never change. Functions in the map namespace make new maps from old ones:

$ sxml xpath '{ "anchor": "req-7" } => map:put("keyword", "MUST") => map:remove("anchor")'
{"keyword":"MUST"}
$ sxml xpath 'map:keys({ "MUST": 5, "MAY": 2 }), map:size({ "MUST": 5, "MAY": 2 })'
"MUST"
"MAY"
2

Building maps from documents

Maps are a good way to index a document. An xref without text, such as <xref target="RFC2119"/>, gets its text from what it points to: a renderer writes "[RFC2119]" for a reference and "Section 4" for a section. A map from anchor to that text is the lookup table such a renderer needs. map:merge combines many maps into one:

$ sxml xpath --context-item=vane.xml 'map:merge(//section ! { @anchor: string(name) })'
{"intro":"Introduction","terminology":"Terminology","states":"Flag States","messages":"Messages","rollout":"Rollout","status":"Status","pause":"Pause","security":"Security Considerations","iana":"IANA Considerations"}

The key @anchor is an attribute, which becomes its value when it is used as a key. Now the whole table: sections numbered as in Paths, Steps and Predicates, references in brackets, and terms by their text:

$ sxml xpath --context-item=vane.xml 'let $text := map:merge((//section ! { @anchor: "Section " || string-join(ancestor-or-self::section ! (count(preceding-sibling::section) + 1), ".") }, //reference ! { @anchor: `[{@anchor}]` }, //dt ! { @anchor: string() })) return ($text?pause, $text?RFC2119, $text?term-percentage, map:size($text))'
"Section 4"
"[RFC2119]"
"Percentage"
17

Every element with an anchor can go into one map, here from anchor to the element's name. Its keys are the targets an xref may name, so an xref whose target is not a key points at nothing:

$ sxml xpath --context-item=vane.xml 'map:merge(//*[@anchor] ! { @anchor: name() }) => map:size()'
33
$ sxml xpath --context-item=vane.xml 'let $kinds := map:merge(//*[@anchor] ! { @anchor: name() }) return count(//xref[empty($kinds(@target))])'
0

Thirty-three anchors, and no dangling reference. map:merge keeps the first value when two maps have the same key; with the option { "duplicates": "reject" } it raises an error instead, which makes it a check that the anchors are unique. Key words are not unique, so this fails:

$ sxml xpath --context-item=vane.xml 'map:merge(//t[@anchor] ! { string(bcp14): string(@anchor) }, { "duplicates": "reject" })'
expression:1:1: err:FOJS0003: Duplicate keys are rejected by the duplicates option

When several items share a key, map:build groups them. Here, the requirements by key word:

$ sxml xpath --context-item=vane.xml 'map:build(//t[@anchor], fn { string(bcp14) }, fn { string(@anchor) })'
{"MUST":("req-1","req-2","req-4","req-6","req-7"),"SHOULD":("req-3","req-8"),"MUST NOT":"req-5","MAY":("req-9","req-11"),"SHOULD NOT":"req-10"}

The value of an entry can be any sequence, as here. fn { ... } is a small function; the next section explains them.

Arrays

An array is written with square brackets. Unlike a sequence, an array keeps its members apart, even when a member is itself a sequence:

$ sxml xpath '[1, 2, (3, 4)]'
[1,2,(3,4)]
$ sxml xpath '[1, 2, (3, 4)]?3'
3
4

?3 looks up the third member. array { ... } makes an array with one member for each item of a sequence, and ?* turns an array back into a sequence. A reference has one or more authors, and an array of arrays keeps each reference's authors together, which a flat sequence could not:

$ sxml xpath --context-item=vane.xml 'array { //reference ! array { .//author/@surname ! string() } }'
[["Bradner"],["Crocker","Overell"],["Leiba"],["M'Raihi","Machani","Pei","Rydell"]]
$ sxml xpath --context-item=vane.xml 'let $authors := array { //reference[@anchor = "RFC6238"]//author/@surname ! string() } return ($authors?1, array:size($authors), $authors?*[last()])'
"M'Raihi"
4
"Rydell"

for member goes through the members of an array:

$ sxml xpath --context-item=vane.xml 'for member $authors in array { //reference ! array { .//author/@surname ! string() } } return string-join($authors?*, " and ")'
"Bradner"
"Crocker and Overell"
"Leiba"
"M'Raihi and Machani and Pei and Rydell"

JSON

Maps and arrays are exactly what JSON is made of, which makes JSON easy to read and write. The json output method writes maps and arrays as JSON. Tools that check an implementation against a specification often want its requirements as JSON; here is the list, an array with a map for each requirement, written to a file with --output:

$ sxml xpath --context-item=vane.xml --method=json --output=xpath-requirements.json 'array { //t[@anchor] ! { "anchor": string(@anchor), "keyword": string(bcp14), "section": string(ancestor::section[1]/name) } }'
$ cat xpath-requirements.json
[{"anchor":"req-1","keyword":"MUST","section":"Flag States"},{"anchor":"req-2","keyword":"MUST","section":"Flag States"},{"anchor":"req-3","keyword":"SHOULD","section":"Flag States"},{"anchor":"req-4","keyword":"MUST","section":"Rollout"},{"anchor":"req-5","keyword":"MUST NOT","section":"Rollout"},{"anchor":"req-9","keyword":"MAY","section":"Rollout"},{"anchor":"req-6","keyword":"MUST","section":"Status"},{"anchor":"req-10","keyword":"SHOULD NOT","section":"Status"},{"anchor":"req-7","keyword":"MUST","section":"Pause"},{"anchor":"req-8","keyword":"SHOULD","section":"Pause"},{"anchor":"req-11","keyword":"MAY","section":"Pause"}]

That turned part of an XML document into JSON in one line. (--output does not replace a file that exists; add --force to run it again.) Reading it back gives maps and arrays again. json-doc reads a JSON file, and parse-json reads JSON text:

$ sxml xpath 'json-doc("xpath-requirements.json")?*[?keyword = "MAY"]?anchor'
"req-9"
"req-11"
$ sxml xpath 'parse-json(unparsed-text("xpath-requirements.json"))?3'
{"anchor":"req-3","keyword":"SHOULD","section":"Flag States"}
$ sxml xpath 'parse-json('"'"'{"anchor": "req-7", "states": ["checking", "paused"]}'"'"')?states?2'
"paused"

?* gives the members of the array, and the predicate [?keyword = "MAY"] looks up keyword in each. (The '"'"' in the last command is how a shell puts a single quote inside single quotes. In a query file, you don't need it.) JSON, CSV and HTML has much more on JSON.

Records

A record type describes maps with known keys, like a struct in other languages. Use it where a sequence type is allowed, to check that a map has the shape you expect:

$ sxml xpath 'let $requirement as record(anchor as xs:string, keyword as xs:string, section as xs:string) := { "anchor": "req-7", "keyword": "MUST", "section": "Pause" } return $requirement?section'
"Pause"
$ sxml xpath 'let $requirement as record(anchor as xs:string, keyword as xs:string, section as xs:string) := { "anchor": "req-7", "keyword": "MUST" } return $requirement?section'
expression:1:5: err:XPTY0004: The value does not match the required type (XPath 4.0, section 3.4, coercion rules)

The second map has no section, so it is not a value of the record type. A record type also converts its fields, as a function signature converts its arguments: a field declared xs:string turns a node into its string. So the requirement can be made from the paragraph's nodes:

$ sxml xpath --context-item=vane.xml 'let $requirement as record(anchor as xs:string, keyword as xs:string, section as xs:string) := //t[@anchor = "req-7"] ! { "anchor": @anchor, "keyword": bcp14, "section": ancestor::section[1]/name } return $requirement'
{"anchor":"req-7","keyword":"MUST","section":"Pause"}

And the JSON file we wrote has exactly that shape, so its members can be checked as records when they are read:

$ sxml xpath 'let $requirements as record(anchor as xs:string, keyword as xs:string, section as xs:string)* := json-doc("xpath-requirements.json")?* return $requirements[?section = "Rollout"]?anchor'
"req-4"
"req-5"
"req-9"

Records shine in XQuery and XSLT, where function signatures can use them.