I have a XML file:
<books>
<title>Moby-Dick</title>
<author>Herman Melville</author>
<title>Sunrise Nights</title>
<author>Jeff Zentner</author>
<author>Brittany Cavallaro</author>
<price>14.52€</price>
<title>My Salty Mary</title>
<author>Cynthia Hand</author>
<author>Brodi Ashton</author>
<author>Jodi Meadows</author>
</books>
For which I would like to create a new book
node every time I encounter a title
node, putting every following non‑title
node into that book. My expected output is:
<books>
<book>
<title>Moby-Dick</title>
<author>Herman Melville</author>
</book>
<book>
<title>Sunrise Nights</title>
<author>Jeff Zentner</author>
<author>Brittany Cavallaro</author>
<price>14.52€</price>
</book>
<book>
<title>My Salty Mary</title>
<author>Cynthia Hand</author>
<author>Brodi Ashton</author>
<author>Jodi Meadows</author>
</book>
</books>
As XQuery's group by
doesn't seem to be useful here, I'm trying to do the grouping in an array using fold-left
. Here's what I wrote so far:
aside: There might be a simpler method so feel free to point it out to me.
let $groups := (
doc("books.xml")/books/* =>
fold-left((array{}, 0), function($acc, $node) {
let
$arr := $acc[1],
$idx := $acc[2]
return
if (name($node) = "title")
then ($arr => array:append($node), $idx+1)
else ($arr => array:put($idx, ($arr($idx), $node)), $idx)
})
)[1]
return
<books>{
for $nodes in $groups
return <book>{$nodes}</book>
}</books>
Which wrongly outputs:
<books>
<book>
<title>Moby-Dick</title>
<author>Herman Melville</author>
<title>Sunrise Nights</title>
<author>Jeff Zentner</author>
<author>Brittany Cavallaro</author>
<price>14.52€</price>
<title>My Salty Mary</title>
<author>Cynthia Hand</author>
<author>Brodi Ashton</author>
<author>Jodi Meadows</author>
</book>
</books>
I found the error in my code: I was iterating over the $groups
array with a for $nodes in $groups return ...
which should be $groups => array:for-each(function($nodes){...})
instead.
For a simpler method than fold-left
check the accepted answer.
If you have the option of using XSLT 2.0+, use:
<xsl:template match="booke">
<books>
<xsl:for-each-group select="*"
group-starting-with="title">
<book>
<xsl:copy-of select="current-group()"/>
</book>
</xsl:for-each-group>
</books>
</xsl:template>
In XQuery 3.0+ it can be done using the FLWOR window
clause.
for tumbling window $w in books/*
start $s when $s[self::title]
return <book>{$w}</book>
Not tested.