Find the total number of child elements? - xquery

<?xml version="1.0" encoding="UTF-8"?>
<root>
<author>
<name>A</name>
<book>Book1</book>
<book>Book2</book>
</author>
<author>
<name>B</name>
<age>45</age>
<book>Book3</book>
</author>
</root>
How do I write a XQuery to display the total number of books by an author?

One approach is:
let $max-books = max(/root/author/count(book))
return /root/author[count(book) = $max-books]
which will return all authors who have authored a maximum number of books.
As a one liner, this can be simplified to:
/root/author[count(book) = max(/root/author/count(book))]
Another way to do this is:
(for $author in /root/author
order by count($author/book) descending
return $author/name)[1]
which will return an author with the maximum number of books.

#Fox: should you not tag this question with "homework"? ;-)
#Oliver Hallam: IMHO, #Fox wants to list each author together with the amount of books by that author and not the author with the highest amount of books.
Your first query
let $max-books = max(/root/author/count(book))
return /root/author[count(book) = $max-books]
Contains a syntax error. You should use ":=" instead of only "=". Furthermore
#Fox: to find the solution, have a look at FLWOR expressions. You can use the "for" part to select each of the book nodes with an XPath expression and bind each node to a $book variable. Then use the "let" part to define two variables ($authorName and $amountOfBooks) using the $book as "starting point". Last, use the "return" part to define the output format you need for the resulting XML.

Related

XQuery – counting parent’s preceding siblings

I wish I could be able to count preceding siblings of the highest div in ePub (for a footnote). I need to pass the value to the attribute before passing notes through XSLT.
for $note in doc('/db/custom_jh/bukwor.xml')//tei:note[#place='bottom']
let $parent := count($note[preceding-sibling::tei:div[#n='1']])
let $update := update insert attribute att2 {$parent} into $note
return $note
Attempts with $note[preceding-sibling::tei:div[#n='1']] or $note[ancestor-or-self::tei:div[#n='1']] returns just 0 or the total sum of all the divs.
Something like <xsl:number level="any" select="tei:div[#n='1']/>" from XSLT, if possible.
UPDATE
The very minimal code for counting (still not working, returns only 6 × 1, should at least one 2:
for $note at $count in doc('/db/custom_jh/bukwor.xml')//tei:note[#place='bottom']
let $parent := count($note[ancestor-or-self::*/tei:div[#n='1']])
return $parent
I don't know about ePub format of XML and there is no sample XML provided so the requirement isn't clear, at least for me. But according to the title, you might want something like this :
let $parent := count($note/parent::*/preceding-sibling::tei:div[#n='1'])
basically counting preceding sibling tei:div from parent element of current $note, where the tei:div have n attribute value equals 1.
The whole example was slightly bad. Finally, I restructured the whole thing. At the moment, I do it this way:
let $chaps :=
(
let $countAll := count($doc//tei:note)
for $chapter at $count in $doc//tei:div[#n='1']
let $countPreceding := count($chapter/preceding::tei:div[#n='1']//tei:note[#place='bottom'])
let $params :=
<parameters>
<param name="footnoteNo" value="{$countPreceding}"/>
</parameters>
return
<entry name="OEBPS/chapter-{$count}.xhtml" type="xml">
{
transform:transform($chapter, doc("/db/custom_jh/xslt/style-web.xsl"), $params)
}
</entry>
)
The count($chapter/preceding::tei:div[#n='1']//tei:note[#place='bottom']) does the trick for me. (I need to collect all footnotes in one file and make backlinks to locations of their indexes in different files).

How to find the lowest common ancestor of two nodes in XQuery?

Suppose the input XML is
<root>
<entry>
<title>Test</title>
<author>Me</author>
</entry>
</root>
I would like to find the lowest common ancestor of title and author.
I tried the following code in BaseX:
let $p := doc('t.xq')//title,
$q := doc('t.xq')//author,
$cla := ($p/ancestor-or-self::node() intersect $q/ancestor-or-self::node())
return
$cla
But it returns nothing (blank output).
Your code works totally fine for me, apart from returning all common ancestors.
The Last Common Ancestor
Since they're returned in document order and the last common ancestor must also be the last node, simply extend with a [last()] predicate.
declare context item := document {
<root>
<entry>
<title>Test</title>
<author>Me</author>
</entry>
</root>
};
let $p := //title,
$q := //author,
$cla := ($p/ancestor-or-self::node() intersect $q/ancestor-or-self::node())[last()]
return
$cla
Files and Databases
If the query you posted does not return anything, you might be working on a file t.xq. intersect requires all nodes to be compared in the same database, each invocation of doc(...) on a file creates a new in-memory database. Either create a database in BaseX with the contents, or do something like
declare variable $doc := doc('t.xq');
and replace subsequent doc(...) calls by $doc (which now references a single in-memory database created for the file).
This is one possible way :
let $db := doc('t.xq'),
$q := $db//*[.//title and .//author][not(.//*[.//title and .//author])]
return
$q
brief explanation :
[.//title and .//author] : The first predicate take into account elements having descendant of both title and author.
[not(.//*[.//title and .//author])] : Then the 2nd predicate applies the opposite criteria to the descendant elements, meaning that overall we only accept the inner-most elements matching the first predicate criteria.
output :
<entry>
<title>Test</title>
<author>Me</author>
</entry>
I changed doc('t.xq') in front of the variables $p and $q with the variable $db as follows. Now it works (plus, I used the last() to have the last (lowest) common ancestor).
let
$db := doc('t.xq'),
$p := $db//title,
$q := $db//author,
$cla := ($p/ancestor-or-self::node() intersect $q/ancestor-or-self::node())[last()]
return $cla

Xquery - what is wrong here

I would like the node replace to act on the $person variable. What do I need to change?
The following code should change the name of the people in the sequence to X.
declare function local:ChangeName($person)
{
xdmp:node-replace($person//Name/text, text { "X" } )
<p>{$person}</p>
};
let $b := <Person>
<Name>B</Name>
<IsAnnoying>No</IsAnnoying>
</Person>
let $j := <Person>
<Name>J</Name>
<IsAnnoying>No</IsAnnoying>
</Person>
let $people := ($b, $j)
return $people ! local:ChangeName(.)
xdmp:node-replace() only operates on persisted documents, not on in-memory documents.
Your local:ChangeName() function could construct the Person and Name elements but copying the IsAnnoying element, as in:
declare function local:ChangeName($person)
{
<p>
<Person>
<Name>X</Name>
{$person//IsAnnoying}
</Person>
</p>
};
For more complex transformations, consider a recursive typeswitch or XSLT transform.
As Erik noted above, you can't apply the xdmp update functions to in-memory nodes. If you have a strong need to to do in-memory node updates, Ryan Grimm's memupdate library can come in handy. It basically does the grunt work of the recursive traversal for you. But beware, in-memory updates do not scale to large documents because it requires making a copy to do an update.
https://github.com/marklogic/commons/tree/master/memupdate
I don't have a ML instance running, so I couldn't test it (maybe there are more issues), but it has to be
$person//Name/text()
instead of
$person//Name/text
The Xpath:
$person//Name/text
should be:
$person//Name/text()
The XPath is looking to replace the text node, which doesn't exist.
Also note that the results of xdmp:node-replace will not be visible in the same transaction. To see the results you need to start a new transaction.

XQuery fn:deep-equal - comparing text from XPath with string literal

I'm trying to use XQuery function fn:deep-equal to compare sections of XML documents and I'm getting unexpected behaviour. When comparing XPath value with string literal, function returns false.
For example following code
let $doc :=
<root>
<child><message>Hello</message></child>
</root>
let $message := <message>Hello</message>
let $value := $doc/child/message/text()
let $compareDirectly := fn:deep-equal($value, "Hello") (: -> false :)
let $compareAsString := fn:deep-equal(fn:concat($value, ""), "Hello") (: -> true :)
let $comparePath := fn:deep-equal($value, $message/text()) (: -> true :)
return
<results>
<value>{$value}</value>
<directly>{$compareDirectly}</directly>
<asString>{$compareAsString}</asString>
<path>{$comparePath}</path>
</results>
Executed using Saxon, XQuery program generates following XML
<?xml version="1.0" encoding="UTF-8"?>
<results>
<value>Hello</value>
<directly>false</directly>
<asString>true</asString>
<path>true</path>
</results>
I'd expect $compareDirectly to be true (same as two other examples), but fn:deep-equal does not seem to work as I would intuitively expect. I'm wondering whether this is correct behaviour.
Is there any better wah how to compare two XML nodes?
I'm lookig for some generic solution which could be used for both XML snippets (like values of $doc or $message in example) and also for this special case with string literal.
From the spec:
To be deep-equal, they must contain items that are pairwise deep-equal; and for two items to be deep-equal, they must either be atomic values that compare equal, or nodes of the same kind, with the same name, whose children are deep-equal.
So this is why it doesn't return true when comparing a text node to an atomic type. In your other two examples you are comparing 2 string atomic types. It looks as if you don't need deep-equal, which compares nodes recursively. If that's the case, then you can just compare the strings:
$doc/child/message/string() eq $message/string()
=> true()
If there are other requirements, then you may need to update your example to demonstrate those more clearly.

In XQuery how can I list files that contain 1 or more child elements in a second list?

Using the book example seen everywhere, if I had many documents each containing a list of books, how could I list just the documents which contained one or more ISBN numbers stored in a variable?
i.e.
<Doc id="1">
<ISBN code="734554656570317"
<ISBN code="234363495740647"
<ISBN code="833563495780345"
<Doc id="2">
<ISBN code="467467546756747"
<ISBN code="890473590555875"
<ISBN code="234557857667412"
etc
How can I achieve something like:
declare variable $isbnList := ("833563495780345", "234557857667412");
for $doc in /Doc
where $doc/ISBN[#code in $isbnList]
If $isbnList contains a sequence of atomic items, each of which is a string ISBN code, this should work:
/Doc[ISBN/#code = $isbnList]
( = performs a general comparison and will return true if at least one item on the left matches at least one item on the right, for example (1, 2) = (2, 3) returns true).

Resources