order groups returned by XQuery FLWOR group by clause in document order

244 Views Asked by At

Coming from an XSLT background I sometimes struggle to find the XQuery equivalent of some XSLT approach I am used to. This question is about how to order groups in document order, which XSLT 2/3 seems to do automatically while with XQuery the order seems to be undefined.

In more detail: with grouping in XSLT 3 (or 2 as well) I am used to (https://www.w3.org/TR/xslt-30/#order-of-groups) getting the groups returned in "order of first appearance", that is, for instance, when I group some child elements of an element, the ordering of groups is determined by the document order of the first item in each group. This doesn't seem to be the case in XQuery, where https://www.w3.org/TR/xquery-31/#id-group-by says: "The order in which tuples appear in the post-grouping tuple stream is implementation-dependent.".

The question in short is: if

              for $city in city
              group by $country := $city/@country
              return ...

doesn't guarantee me any order in XQuery, is using

              for $city at $pos in city
              group by $country := $city/@country
              order by $pos[1]
              return ...

instead the right way to have the "order of first appearance" in XQuery, like XSLT does it by default?

The background: in a simple example (taken from the XSLT 3 spec https://www.w3.org/TR/xslt-30/#grouping-examples) the city elements in

<cities>
  <city name="Milano"  country="Italia"      pop="5"/>
  <city name="Paris"   country="France"      pop="7"/>
  <city name="München" country="Deutschland" pop="4"/>
  <city name="Lyon"    country="France"      pop="2"/>
  <city name="Venezia" country="Italia"      pop="1"/>
</cities>

have to be grouped by the @country attribute value to sum up the @pop population value for each country.

A compact XSLT 3 way to do that is

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    expand-text="yes"
    version="3.0">

  <xsl:output method="html" indent="yes" html-version="5"/>

  <xsl:template match="cities">
      <table>
          <thead>
              <tr>
                  <th>Country</th>
                  <th>Cities</th>
                  <th>Population</th>
              </tr>
          </thead>
          <tbody>
              <xsl:for-each-group select="city" group-by="@country">
                  <tr>
                      <td>{ current-grouping-key() }</td>
                      <td>
                          <xsl:value-of select="sort(current-group()/@name)" separator=", "/>
                      </td>
                      <td>{ sum(current-group()/@pop) }</td>
                  </tr>
              </xsl:for-each-group>
          </tbody>
      </table>
  </xsl:template>

</xsl:stylesheet>

and I think with a compliant XSLT 3 implementation I am guaranteed to get the result in the order (Italia, France, Deutschland) from the input:

<table>
   <thead>
      <tr>
         <th>Country</th>
         <th>Cities</th>
         <th>Population</th>
      </tr>
   </thead>
   <tbody>
      <tr>
         <td>Italia</td>
         <td>Milano, Venezia</td>
         <td>6</td>
      </tr>
      <tr>
         <td>France</td>
         <td>Lyon, Paris</td>
         <td>9</td>
      </tr>
      <tr>
         <td>Deutschland</td>
         <td>München</td>
         <td>4</td>
      </tr>
   </tbody>
</table>

With XQuery however I think I am not guaranteed any specific order, with

declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";

declare option output:method 'html';
declare option output:html-version '5';

    cities/<table>
              <thead>
                  <tr>
                      <th>Country</th>
                      <th>Cities</th>
                      <th>Population</th>
                  </tr>
              </thead>
              <tbody>
                  {
                      for $city in city
                      group by $country := $city/@country
                      return
                          <tr>
                              <td>{ $country }</td>
                              <td>{ string-join( sort($city/@name/string()), ', ') }</td>
                              <td>{ sum($city/@pop) }</td>
                          </tr>
                  }
              </tbody>
          </table>

Saxon at https://xqueryfiddle.liberty-development.net/b4GWV7 gives me the result as

<table>
   <thead>
      <tr>
         <th>Country</th>
         <th>Cities</th>
         <th>Population</th>
      </tr>
   </thead>
   <tbody>
      <tr>
         <td>Deutschland</td>
         <td>München</td>
         <td>4</td>
      </tr>
      <tr>
         <td>Italia</td>
         <td>Milano, Venezia</td>
         <td>6</td>
      </tr>
      <tr>
         <td>France</td>
         <td>Lyon, Paris</td>
         <td>9</td>
      </tr>
   </tbody>
</table>

What is the right way to ensure the order is taken from the "order of first appearance" in XQuery, is using

              for $city at $pos in city
              group by $country := $city/@country
              order by $pos[1]
              return
                  <tr>
                      <td>{ $country }</td>
                      <td>{ string-join( sort($city/@name/string()), ', ') }</td>
                      <td>{ sum($city/@pop) }</td>
                  </tr>

the right or easiest, most compact way?

Seems to do the job at https://xqueryfiddle.liberty-development.net/b4GWV7/1 but I would like opinions whether there is a different way, more idiomatic way for XQuery.

0

There are 0 best solutions below