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

Grouping

Reports are mostly about putting things together: the requirements of each section, the uses of each term, the keywords of each chapter. group by puts together the bindings that share a key, wherever they are in the document. This section shows how far that goes.

Grouping by a computed key

The key of group by can be any expression whose value is one atomic value (or nothing). The draft's sections nest, and a reader of the table of contents thinks in top-level sections. Here the requirements are grouped by the number of the top-level section they are in:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    group by $number := count($requirement/ancestor::section[last()]/preceding-sibling::section) + 1
    return `{$number}. {$requirement[1]/ancestor::section[last()]/name}: {count($requirement)}`'
"2. Flag States: 3"
"3. Messages: 5"
"4. Pause: 3"

ancestor::section[last()] is the outermost section around a requirement, and the sections before it make its number. Section 1, the Introduction, has no requirements, so it has no group.

Remember the rule from the previous section: after group by, the grouping variable $number is one value, and every other variable, here $requirement, holds all the values of its group, in the order they had before. That's why the name is taken from $requirement[1], the first requirement of the group.

A grouping variable can also be one that a let bound earlier. Then group by $keyword alone is enough:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    let $keyword := string($requirement/bcp14)
    group by $keyword
    order by $keyword
    return { "keyword": $keyword, "count": count($requirement), "anchors": array { $requirement/@anchor ! string() } }'
{"keyword":"MAY","count":2,"anchors":["req-9","req-11"]}
{"keyword":"MUST","count":5,"anchors":["req-1","req-2","req-4","req-6","req-7"]}
{"keyword":"MUST NOT","count":1,"anchors":["req-5"]}
{"keyword":"SHOULD","count":2,"anchors":["req-3","req-8"]}
{"keyword":"SHOULD NOT","count":1,"anchors":["req-10"]}

Each group became a map, which is often the most convenient form for a summary, and which the json output method writes as JSON.

Several keys

With more than one key, a group is made for each combination of keys that occurs. These are the keywords of each section:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    group by $section := string($requirement/ancestor::section[1]/name),
             $keyword := string($requirement/bcp14)
    order by $section, $keyword
    return `{$section}, {$keyword}: {count($requirement)}`'
"Flag States, MUST: 2"
"Flag States, SHOULD: 1"
"Pause, MAY: 1"
"Pause, MUST: 1"
"Pause, SHOULD: 1"
"Rollout, MAY: 1"
"Rollout, MUST: 1"
"Rollout, MUST NOT: 1"
"Status, MUST: 1"
"Status, SHOULD NOT: 1"

Combinations that don't occur, such as a MAY in Status, make no group.

After the grouping

Clauses after group by work on the groups. let computes something for each group, where keeps some groups and drops others, and order by sorts them. Which anchors are referred to more than once, and from where?

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $xref in //xref
    group by $target := string($xref/@target)
    where count($xref) > 1
    order by $target
    return $target || ": " || string-join($xref/ancestor::*[@anchor][1]/@anchor, ", ")'
"term-flag: terminology, req-1"
"term-pause: req-5, req-7"

How many requirements, and how many different keywords, does each section have?

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    group by $section := string($requirement/ancestor::section[1]/name)
    let $keywords := distinct-values($requirement/bcp14)
    order by count($keywords) descending, $section
    return `{$section}: {count($requirement)} requirements, {count($keywords)} keywords`'
"Pause: 3 requirements, 3 keywords"
"Rollout: 3 requirements, 3 keywords"
"Flag States: 3 requirements, 2 keywords"
"Status: 2 requirements, 2 keywords"

Keys are compared as distinct-values compares them: strings by their characters, numbers as numbers. A collation after the key compares strings by other rules, for example without regard to case. A term is defined as "Flag" and used as "flag"; with this collation, the definition and its uses fall into one group:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $word in (//dt, //xref[starts-with(@target, "term-")])
    group by $key := string($word)
        collation "http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive"
    return $key || ": " || count($word)'
"Flag: 3"
"SDK: 2"
"Percentage: 2"
"Pause: 3"

The key of a group is the key of its first member, here the dt, which comes first in the sequence. Working with Text has more on collations.

Choosing a key well

A key is atomized, as a function argument is. A node used as a key becomes its string value, not the node itself:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    group by $chapter := $requirement/ancestor::section[last()]
    return $chapter/name'
expression:4:12: err:XPTY0004: An operand on the left of "/" is not a node (XPath 4.0, section 4.7.3: XPTY0004, formerly XPTY0019)

$chapter is the whole text of a section as an untyped atomic value, which has no name child. That's why the first example of this section grouped by a number and took the name from the group's first member. Text makes a poor key anyway: two sections with the same text would be one group. Group by something that names the thing, such as a number or an anchor.

A key must be one value or nothing. Nothing is a key of its own: every paragraph without a bcp14 lands in one group, whose key is the empty sequence:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $paragraph in //t
    group by $keyword := $paragraph/bcp14/string()
    return `{ $keyword otherwise "(none)" }: { count($paragraph) }`'
"(none): 5"
"MUST: 5"
"SHOULD: 3"
"MUST NOT: 1"
"MAY: 2"
"SHOULD NOT: 1"

There are three SHOULDs here, not two: the paragraph in Security Considerations has a keyword but no anchor, so the earlier queries, which looked only at //t[@anchor], did not see it. The draft's own editorial note says it needs an anchor; Updating Documents gives it one.

Two values are an error. A paragraph with two keywords can't be put in one keyword's group:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $paragraph in (//t, <t><bcp14>MUST</bcp14> and <bcp14>MAY</bcp14></t>)
    group by $keyword := $paragraph/bcp14
    return count($paragraph)'
expression:3:14: err:XPTY0004: The value does not match the required type (XPath 4.0, section 3.4, coercion rules)

The draft's convention is one keyword per requirement, and this error is one way to find a paragraph that breaks it. To group such a paragraph under each of its keywords, iterate over the keywords instead: for $keyword in //bcp14 group by $name := string($keyword), and reach the paragraph as $keyword/...

Runs of neighbours

group by doesn't care where the members of a group are. Sometimes position is the whole point: the queries a service sends to its database one after another, the events of one session. XQuery's window clauses group items that are next to each other, in tumbling windows that don't overlap and sliding windows that do. They are taught on trace data, where they find a run of repeated calls, in Who Calls Whom, and How Often.