Re: [xsl] XSLT3 generic grouping functions

Subject: Re: [xsl] XSLT3 generic grouping functions
From: "Michael Kay mike@xxxxxxxxxxxx" <xsl-list-service@xxxxxxxxxxxxxxxxxxxxxx>
Date: Wed, 8 Jul 2020 10:20:52 -0000
It might be useful to think of xsl:for-each-group as providing a solution for
"stereotype" grouping use cases that are commonly encountered, but looking to
other functional primitives for solutions to the more general problem: for
example you can use fn:fold-left to build up the set of groups e.g. as an
array of arrays. For the common use-case tackled by group-by, creating an
index in the form of a map is often a convenient alternative to
xsl:for-each-group.

If you're starting with the requirement "create a sequence of groups, starting
a new group at item x when $test(x) returns true" then rather than use
xsl:for-each-group with a group-starting pattern, another approach (producing
a sequence of arrays) would be

fold-left($population, (), function($groups, $x) {if ($test($x)) then ([$x],
$groups) else array:append(head($groups), $x))}) => reverse()

(It builds the list of groups in reverse order for convenience and then
reverses it at the end).

At what point you stop using the "stereotype" xsl:for-each-group instruction
and start rolling-your-own is an open question, but with maps, arrays, and
higher-order-functions, rolling your own is much more viable than it was.

Michael Kay
Saxonica

> On 8 Jul 2020, at 10:24, Matthieu RICAUD-DUSSARGET
m.ricaud-dussarget@xxxxxxxxxxxxxxxxxx
<xsl-list-service@xxxxxxxxxxxxxxxxxxxxxx> wrote:
>
> Hi all,
>
> While teaching XSLT, someone ask if it was possible to have a kind of
generic function to group XML elements.
> I first answer the only way to do it was to use (eventually nested)
<xsl:for-each-group> instruction with specific conditions according to the
data.
> Later, as I was discovering XSLT3, I realize the grouping condition might be
sent as an higher-order-function parameter, which lead me to write some
generic grouping functions to wrap XML elements (see example below).
>
> The original code can be found on github at
https://github.com/ELSGestion/els-sie-xsl-lib/blob/master/src/main/xsl/els-co
mmon_xml.xsl
<https://github.com/ELSGestion/els-sie-xsl-lib/blob/master/src/main/xsl/els-c
ommon_xml.xsl>
> Therebs also some generic xsl to nest titles
(https://github.com/ELSGestion/els-sie-xsl-lib/blob/master/src/main/xsl/nest-
titles.xsl
<https://github.com/ELSGestion/els-sie-xsl-lib/blob/master/src/main/xsl/nest-
titles.xsl>) and a specific implementation of it to nest HTML titles
(https://github.com/ELSGestion/els-sie-xsl-lib/blob/master/src/main/xsl/nest-
html-titles.xsl
<https://github.com/ELSGestion/els-sie-xsl-lib/blob/master/src/main/xsl/nest-
html-titles.xsl>)
> Working examples can be found in the test folder
https://github.com/ELSGestion/els-sie-xsl-lib/tree/master/src/test
<https://github.com/ELSGestion/els-sie-xsl-lib/tree/master/src/test>, as xspec
unit test or xspec driven integration tests.
>
> I actually have a bug with Saxon 9.9 (https://saxonica.plan.io/issues/4636
<https://saxonica.plan.io/issues/4636>) on the generic starting-with grouping
function, but this is an occasion to share theses grouping libraries J
>
> Any comments, feedbacks or similar code implementation are welcome !
>
> Cheers
> Matthieu Ricaud-Dussarget
>
> Example of generic adjacent grouping function used to define a specific
bby-name groupingb implementation (therebs a similar set of functions
for starting-with) :
>
> <xd:doc>
>   <xd:desc>
>     <xd:p>Wrap adjacent elements into a new "wrapper" element</xd:p>
>     <xd:p>CAUTION : any text, pi, comments within context will be
loose</xd:p>
>   </xd:desc>
>   <xd:param name="context">Parent of the adjacent elements to
wrap</xd:param>
>   <xd:param name="adjacent.function">Xpath function to set the adjacency
condition</xd:param>
>   <xd:param name="wrapper">Element wrapper</xd:param>
>   <xd:param name="keep-context">Say if the context should be kept or not in
the result</xd:param>
>   <xd:return>context (or its content) with wrapped adjacent
element</xd:return>
> </xd:doc>
> <xsl:function name="els:wrap-elements-adjacent" as="node()*">
>   <xsl:param name="context" as="element()"/>
>   <xsl:param name="adjacent.function"/> <!--as="xs:string"-->
>   <xsl:param name="wrapper" as="element()"/>
>   <xsl:param name="keep-context" as="xs:boolean"/>
>   <xsl:variable name="content" as="item()*">
>     <xsl:for-each-group select="$context/*"
group-adjacent="$adjacent.function(.)">
>       <xsl:choose>
>         <xsl:when test="current-grouping-key()">
>           <xsl:for-each select="$wrapper">
>             <xsl:copy>
>               <xsl:copy-of select="@*"/>
>               <xsl:sequence select="current-group()"/>
>             </xsl:copy>
>           </xsl:for-each>
>         </xsl:when>
>         <xsl:otherwise>
>           <xsl:copy-of select="current-group()"/>
>         </xsl:otherwise>
>       </xsl:choose>
>     </xsl:for-each-group>
>   </xsl:variable>
>   <xsl:choose>
>     <xsl:when test="$keep-context">
>       <xsl:for-each select="$context">
>         <xsl:copy>
>           <xsl:copy-of select="@*"/>
>           <xsl:sequence select="$content"/>
>         </xsl:copy>
>       </xsl:for-each>
>     </xsl:when>
>     <xsl:otherwise>
>       <xsl:sequence select="$content"/>
>     </xsl:otherwise>
>   </xsl:choose>
> </xsl:function>
>
> <xd:doc>
>   <xd:desc>
>     <xd:p>Wrap "adjacent by name" elements into a new  "wrapper"
element</xd:p>
>     <xd:p>CAUTION : any text, pi, comment within context will be
loose</xd:p>
>   </xd:desc>
>   <xd:param name="context">Parent of the adjacent elements to
wrap</xd:param>
>   <xd:param name="adjacent.names">sequence of qualified names to set
adjacent elements</xd:param>
>   <xd:param name="wrapper">element wrapper</xd:param>
>   <xd:param name="keep-context">Say if the context should be kept or not in
the result</xd:param>
>   <xd:return>context (or its content) with wrapped adjacent
element</xd:return>
> </xd:doc>
> <xsl:function name="els:wrap-elements-adjacent-by-names" as="node()*">
>   <xsl:param name="context" as="element()"/>
>   <xsl:param name="adjacent.names" as="xs:string+"/>
>   <xsl:param name="wrapper" as="element()"/>
>   <xsl:param name="keep-context" as="xs:boolean"/>
>   <xsl:sequence select="els:wrap-elements-adjacent(
>     $context,
>     function($e) as xs:boolean {name($e) = $adjacent.names},
>     $wrapper,
>     $keep-context)"/>
> </xsl:function>
>
> XSL-List info and archive <http://www.mulberrytech.com/xsl/xsl-list>
> EasyUnsubscribe <http://lists.mulberrytech.com/unsub/xsl-list/293509> (by
email <>)

Current Thread