xpathxquery

XPath/XQuery procedural grouping


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>

Which I would like to transform as:

<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>

The logic is to create a new book every time we encounter a title and put every following "non‑title" nodes into that book.

Here's what I tried so far:

let $books := (
  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 $book in $books
    return <book>{$book}</book>
  }</books>

But I get

<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>

ASIDE: group by doesn't seem to be useful to solve the current problem, so I tried to group the books in an array, but I have no idea if that's the correct way to do it; any tip is welcome.


Solution

  • 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.