Sorting a tree with xquery by local-name - xquery

I want the below XML
<a>
<z>
<e>
<b>
<c>
Sorted as such
<a>
<c>
<z>
<b>
<e>
I am using local-name as order by.
I believe I will need to call function recursively.
I believe I will need to recreate the element using element constructor.
I do not care about attributes at this time.
snippet:
let $child-elements := $elements/*
return
if ($child-elements) then
myfunctx:sort($child-elements, $current-element)
else
element { xs:Qname ($local-name)} , {$current-element, $result) ???

declare function local:sort($container){
element {$container/name()} {
for $child in $container/*
order by $child/local-name()
return local:sort($child)
}
};
let $doc := <doc>
<a/>
<z>
<e/>
<b/>
</z>
<c/>
</doc>
return local:sort($doc)

Related

XQuery: How to add a comma after sequence except for the last element

I have the following xml:
<form>
<bibl>
<biblScope>1</biblScope>
<biblScope>a</biblScope>
</bibl>
<bibl>
<biblScope>2</biblScope>
<biblScope>b</biblScope>
</bibl>
<bibl>
<biblScope>38</biblScope>
<biblScope>c</biblScope>
</bibl>
</form>
Using this XQuery
for $bibl in form/bibl
return
<div>
{
for $biblScope in $bibl/biblScope/text()
return
$biblScope
}
{if ($bibl[position()] ne $bibl[last()]) then ',' else '#'}
</div>
The <biblScope> contents are printed one after another. After each biblelement, I would like to add a separator (comma / ",") except for the last one, but with the code above, all I get is
<div>
1a
#
</div>
<div>
2b
#
</div>
<div>
38c
#
</div>
and this is definitely wrong, because what I would like to have is
<div>1a,</div>
<div>2b,</div>
<div>38c#</div>
(add a comma after every bibl element content; the last one is supposed to be followed by # instead of a comma.)
Having tried different things for some time now, I could need some help. What is the proper way to do this?
The problem is that position() and last() work on the current context, which is not set by flwor expressions. If you want to use similar semantics, use the at $position syntax to get a position counter, and define $last as the number of results:
let $last := count(form/bibl)
for $bibl at $position in form/bibl
return
<div>
{
for $biblScope in $bibl/biblScope/text()
return
$biblScope
}
{if ($position ne $last) then ',' else '#'}
</div>
If you're able to use XQuery 3.0, the application operator ! might be of use here. Replacing the unnecessary loops by axis steps and element constructors in those, you can rely on the positional predicates, as the context is set to to <bibl/> elements:
form/bibl ! <div>
{
biblScope/text(),
if (position() ne last()) then ',' else '#'
}
</div>
First note that this:
for $biblScope in $bibl/biblScope/text()
return
$biblScope
can be replaced by this:
$bibl/biblScope/text()
(This bit of verbosity is a surprisingly common mistake. It suggests you're thinking procedurally - processing items one at a time, rather than processing sets.)
Then this:
<div>
{$bibl/biblScope/text()}
{if ($bibl[position()] ne $bibl[last()]) then ',' else '#'}
</div>
should be replaced by this:
<div>{string-join($bibl/biblScope, ',')}#</div>
Note that I've also got rid of the unnecessary (and arguably incorrect) use of /text(), which is another XQuery anti-pattern.

calling a function to process its own result a certain number of times

I have a query which attaches unique references to elements in #xml:ids. It works as it should, but how can I avoid the kludgy repetition of the function local:add-references()?
xquery version "3.0";
declare namespace tei="http://www.tei-c.org/ns/1.0";
declare function local:change-attributes($node as node(), $new-name as xs:string, $new-content as item(), $action as xs:string, $target-element-names as xs:string+, $target-attribute-names as xs:string+) as node()+ {
if ($node instance of element())
then
element {node-name($node)}
{
if ($action = 'attach-attribute-to-element' and name($node) = $target-element-names)
then ($node/#*, attribute {$new-name} {$new-content})
else
$node/#*
,
for $child in $node/node()
return $child
}
else $node
};
declare function local:add-references($element as element()) as element() {
element {node-name($element)}
{$element/#*,
for $child in $element/node()
return
if ($child instance of element() and $child/../#xml:id)
then
if (not($child/#xml:id))
then
let $local-name := local-name($child)
let $preceding-siblings := $child/preceding-sibling::element()
let $preceding-siblings := count($preceding-siblings[local-name(.) eq $local-name])
let $following-siblings := $child/following-sibling::element()
let $following-siblings := count($following-siblings[local-name(.) eq $local-name])
let $seq-no :=
if ($preceding-siblings = 0 and $following-siblings = 0)
then ''
else $preceding-siblings + 1
let $id-value := concat($child/../#xml:id, '-', $local-name, if ($seq-no) then '-' else '', $seq-no)
return
local:change-attributes($child, 'xml:id', $id-value, 'attach-attribute-to-element', ('body', 'quote', 'titlePage','text','p','div','front','head','titlePart'), '')
else local:add-references($child)
else
if ($child instance of element())
then local:add-references($child)
else $child
}
};
let $doc :=
<TEI xmlns="http://www.tei-c.org/ns/1.0">
<text xml:id="hamlet">
<front>
<div>
<p>…</p>
</div>
<titlePage>
<titlePart>…</titlePart>
</titlePage>
<div>
<p>…</p>
</div>
</front>
<body>
<p>…</p>
<quote>…</quote>
<p>…</p>
<div>
<head>…</head>
<p>…</p>
<quote>…</quote>
<p>…</p>
<p>…</p>
</div>
<div>
<lg>
<l>…</l>
<l>…</l>
</lg>
</div>
</body>
</text>
</TEI>
return local:add-references(local:add-references(local:add-references(local:add-references(local:add-references($doc)))))
There must be some way to call a function recursively, wrapping it up in itself a certain number of times, using the depth of the document.
The problem is that a certain depth of elements has to wait until their parent elements have been given #xml:ids before they can get their own (the top level has to be seeded with an #xml:id).
This is probably not the best solution, but you might just test if applying the function still changes anything, and cancel otherwise:
declare function local:add-references-recursively($now as element()) as element() {
let $next := local:add-references($now)
return
if (deep-equal($now, $next))
then $now
else local:add-references-recursively($next)
};
And call using
local:add-references-recursively($doc)

transforming tree to sequence of elements

I have something like this
<a>
<b>
<c>1</c>
<d>2<e>3</e></d>
</b>
</a>
and I want to obtain a sequence like this
<a/>
<b/>
<c>1</c>
<d>2<e>3</e></d>
that is, when recursing, every time an element occurs which does not have a text node, I want to output the element as an empty element, whereas every time an element with a text node occurs, I want to output it as it is. Of course, the text nodes in the above input have to be space-normalized.
If I put it through a standard identity transform,
declare function local:copy($element as element()) as element() {
element {node-name($element)}
{$element/#*,
for $child in $element/*
return
if ($child/text())
then $child
else (element {node-name($child)}{$child/#*}, local:copy($child))
}
};
<b> gets reconstructed as a full element (containing <c> and <d>), but if I remove the element constructor, it does not get output at all.
I don't quite get the fourth line in your example output, I'm just guessing what you want is actually this:
<a/>
<b/>
<c>1</c>
<d>2</d>
<e>3</e>
You don't need any functions. Just list all elements, reconstruct one with same name and include it's text node children.
for $element in //*
return element { local-name($element) } { $element/text() }
This version is even shorter, but I think it requires XQuery 3.0, because earlier versions did not allow element constructors in step expressions:
//*/element { local-name(.) } { text() }

Returning a computed element with computed attribute

I am trying to return a computed element with a computed attribute. I have Google'd and RTFM'd this.
Assume the element name and attribute name are in variables:
let $elname := "book"
let $attrname := "title"
I know I can create an element with:
element {$elname} {'content'}
and an attribute:
attribute {$attrname} {'value'}
But how do I create:
<book title="something"/>
I've tried every permutation I can imagine. Thanks very much.
Try:
element {$elname} {attribute {$attrname} {'something'}}
Here's an example of <book> also containing content:
element {$elname} {attribute {$attrname} {'something'},'foo'}
This would produce:
<book title="something">foo</book>
Figured this out:
return element {'joe'} { attribute {'x'} {'e'}, 'z' }

Recursive iteration over an object in Jade template?

I have an object of mixed type properties - some strings, some arrays of strings, some objects containing arrays of strings - that can potentially go many levels deep.
I would like to iterate over all properties so that an object creates a div, an array creates a div, and a string property creates a span to contain the text.
{ "string" : "some text", "object" : { "array" : [ "text" ] } }
The above object would render as:
<span>some text</span>
<div>
<div>
<span>text</span>
</div>
</div>
But usually much more complex structures. How should I go about accomplishing this is Jade?
It's been a while since you asked, but mixin is your friend, I think. I haven't tried it out, but if mixins support recursion, this should work:
mixin parseObject(obj)
div
- each val, key in obj
- if (typeof val === 'string')
span #{val}
- else if (typeof val === 'object')
mixin parseObject(val)
Then in the body of your .jade file, call mixin parseObject(rootObject).
Recursion seems to be suppported now. I have successfully used the function with a minor tweak; you need to use the mixin keyword when calling the function.
mixin parseObject(obj)
div
each val, key in obj
if typeof val === 'string'
span #{val}
else if typeof val === 'object'
mixin parseObject(val)
In the modern version of Jade it's look like
mixin parseObject( obj )
div
each val in obj
if typeof val === 'string'
span= val
else if typeof val === 'object'
+parseObject( val )
Then in the body of your .jade file, call
+parseObject( rootObject )

Resources