Get the reverse order of ancestor nodes with XQuery/XPath

753 Views Asked by At

I am searching for a good solution (short expression, best performance) to get the reverse order of all ancestor nodes with XQuery 3.1 on BaseX.

Now i'm using this code, to get dirA/dirA3/dirA31 for the given XML example:

xquery version "3.1" encoding "utf-8";

declare variable $files := 
  <dir name="dirA">
    <file name="fileA_F1"/>
    <file name="fileA_F2"/>
    <dir name="dirA1">
      <file name="fileA1_F1"/>
      <file name="fileA1_F2"/>
    </dir>
    <dir name="dirA2">
      <file name="fileA2_F1"/>
      <file name="fileA2_F2"/>
    </dir>
    <dir name="dirA3">
      <file name="fileA3_F1"/>
      <file name="fileA3_F2"/>
      <file name="fileA3_F3"/>
       <dir name="dirA31">
        <file name="fileA31_F1"/>
        <file name="fileA31_F2"/>
        <file name="fileA31_F3"/>
      </dir>
    </dir>
  </dir>;


let $path := trace(string-join($files//file[@name='fileA31_F2']/ancestor::dir/@name,'/'))

return()

and this code to get the reverse order dirA31/dirA3/dirA :

let $reversepath := trace(string-join(reverse(tokenize(string-join($files//file[@name='fileA31_F2']/ancestor::dir/@name,'/'),'/')),'/'))

I mean, is there any other XPath or XQuery expression that traverses the ancestors in the reverse order?

Annotation: name attribute values of file nodes are unique

3

There are 3 best solutions below

2
On BEST ANSWER

Another suggestion:

declare function f:reverse-path($n as node()) {
  if (exists($n/../@name))
  then $n || '/' || f:reverse-path($n/..)
  else @name
};
f:reverse-path($files//file[@name='fileA31_F2'])

or more generally, you asked for a function that traverses ancestors in reverse order (innermost ancestor first):

declare function f:reverse-ancestors($n as node()) {
  $n ! (., ..!f:reverse-ancestors(.))
};
1
On

In order to sum up the preceding answers either in whole or in part. (Many thanks to the authors!):

xquery version "3.1" encoding "utf-8";

declare namespace f="myfunc";

declare default element namespace "f";

declare variable $files := 
  <dir name="dirA">
    <file name="fileA_F1"/>
    <file name="fileA_F2"/>
    <dir name="dirA1">
      <file name="fileA1_F1"/>
      <file name="fileA1_F2"/>
    </dir>
    <dir name="dirA2">
      <file name="fileA2_F1"/>
      <file name="fileA2_F2"/>
    </dir>
    <dir name="dirA3">
      <file name="fileA3_F1"/>
      <file name="fileA3_F2"/>
      <file name="fileA3_F3"/>
      <dir name="dirA31">
        <file name="fileA31_F1"/>
        <file name="fileA31_F2"/>
        <dir name="dirA311"/>
        <dir name="dirA312">
          <file name="fileA312_F1"/>
          <file name="fileA312_F2"/>
          <file name="fileA312_F3"/>
        </dir>
      </dir>
    </dir>
  </dir>;


declare function f:traverse-ancestors($n as node()) {
    (: do it from the farthest to the nearest ancestor node :)
    f:doItF2N($n),
    (: Using 'Simply map operator' (XQuery 3.0) :)
    $n ! (., ..!f:traverse-ancestors(.)),
    (: do it from the nearest to the farthest ancestor node :)
    f:doItN2F($n)
};

declare function f:doItF2N($n as node()) {
  trace($n!@name)
};

declare function f:doItN2F($n as node()) {
  trace($n!@name)
};

(: Examples :)

let $childNode := $files//dir[file!@name='fileA312_F3']

(: Ex.1 :)
(: traverse ancestors 'normally' or in reverse order  :)

let $x := f:traverse-ancestors($childNode)

(: Ex.2 - Concatenation of the ancestors name attribute in reverse order, using '/' as separator :)

(: Solution with "=>" - "Arrow operator" (XPath 3.1) :)

let $x := trace($childNode/ancestor::dir/@name => reverse() => string-join('/'))

(: Solution with nested function calls (XPath 3.0) :)

let $x := trace(string-join(reverse($childNode/ancestor::dir/@name),'/'))

let $res := ()

return($res)

Evaluates in BaseX v9.2.4 to:

name="dirA312"
name="dirA31"
name="dirA3"
name="dirA"
name="dirA"
name="dirA3"
name="dirA31"
name="dirA312"
"dirA31/dirA3/dirA"
"dirA31/dirA3/dirA"
4
On

Using XPath 3.1 the new syntax with the => operator (https://www.w3.org/TR/xpath-31/#id-arrow-operator)

let $file := $files//file[@name='fileA31_F2']
return 
    $file/ancestor::dir/@name => reverse() => string-join('/')

is supposed to be more compact and readable but it takes a while getting used to it.