MrD at KookerellaLtd
MrD at KookerellaLtd

Reputation: 2807

selecting preceding cousins (including siblings)

I often have the problem of wanting to select the previous 'cousin' to a node.

For example:

<root>
    <level1>
        <level2 id="1"/>  
        <level2 id="2"/>  
        <level2 id="3"/>  
        <level2 id="4"/>  
    </level1>
    <level1>
        <level2 id="5">
            <level2 id="9"/>
        </level2>  
        <level2 id="6"/>  
        <level2 id="7"/>  
        <level2 id="8"/>  
    </level1>
</root>

If the context node is

/root/level1/level2[@id='6']

I don't want "id=9"; I want "id=5".

Similarly if the context node is

/root/level1/level2[@id='5']

I want "id=4".

So....preceding-sibling does work in the former case but not the latter. preceding selects in the latter case but not the former. (And there are probably a multitude of other cases.)

So my current solution is this (using id=6 as the context node...I could put it in a function, but this should be clear enough)

(/root/level1/level2[@id='6']/preceding::level2 intersect /root/level1/level2)[last()]

this does work, but seems..convoluted, and possibly inefficient...am I missing a simpler solution?

(I'm current using XPath 3.1 in XSLT 3, but I think its a general question.)

(I've limited this to XPath 2.0+, but I'll probably open up the question to XPath 1.0, as that seems even worse because intersect doesn't exist...but I'll do that in another post.)

For XSLT 1.0 this works:

/root/level1/level2[@id='6']/preceding::level2[parent::level1[parent::root]][1]

and is debatably a better solution.

What I'd like to do is

/root/level1/level2[@id='6']/preceding::(root/level/level2)[1]

and have that be interpreted as a match on the path root/level/level2, but I don't think that's valid?


there are some really good answer here, I want completely clear what the question was, apologies.

I'm looking for all 'nth cousins' where 0th cousins are siblings.

irritatingly the elegant solutions just get 0th and 1st, and the more complex solutions are 'correct' but quite clunky (no offense).

For the moment the most elegant solutions I have is to (as a programmer) take the absolute simple path to your current node, in this case

let $this = ....

(root/level1/level2)[. << $this][last()]

the clever solutions do this programatically (one using depth, the other the path), and are a bit mind boggling to me.

the elegant solutions, use the '<<' operator, but only match 0th and 1st siblings (which was a reasonable interpretation).

the XSLT 1.0 solution similarly only does 0th and 1st, and I think I'd actually use the parent:: solution above, and match the full path to the root to ensure all cousins are matched.

(this of course presumes you know the full path, though martin honnens solution works in general).

Upvotes: 3

Views: 154

Answers (6)

MrD at KookerellaLtd
MrD at KookerellaLtd

Reputation: 2807

tbh, Michael Kay's answer is basically there except it tests the level directly, I'd want a variation on that, that ignored the level and just returned all cousins.

<xsl:function name="kooks:cousins" as="node()*">
    <xsl:param name="node" as="node()"/>
    <xsl:sequence select="$node/../*"/>         
    <xsl:if test="$node/..">
        <xsl:sequence select="kooks:cousins($node/..)/*"/>
    </xsl:if>
</xsl:function> 

note, like another answer, there is no matching predicate, this just matches all element cousins, and also doesnt handle the previous/next, but this can be done with '<<'

If you know the full path then

(/foo/bar/wibble)[. << $this]  

is simple and comprehendable

where $this is a member of "/foo/bar/wibble"

this MAY work in xslt 1.0, I've not used it, and the order worries me because of the use of node-set (for following you may need to change the order of the union operarands), and as per M Kay's answer the order is document order.

<xsl:template mode="preceding-cousin" match="/"/>

<xsl:template mode="preceding-cousin" match="*">
    <xsl:variable name="precedingUnclesRTF">
        <xsl:apply-templates mode="preceding-cousin" select=".."/>
    </xsl:variable>
    <xsl:variable name="precedingUncles" select="msxsl:node-set($precedingUnclesRTF)/*"/>
    <xsl:copy-of select="$precedingUncles/* | preceding-sibling::*"/>
</xsl:template>

and the depth answer also works in XSLT 1.0 is simpler than the above, and wont suffer from an exponential copying of nodes as the above does, but does process all the previous nodes in the document, it works in all versions of XSLT and isnt recursive (so wont kill the stack), though i think the recursive function answer can be rewritten as a fold or xsl iteration.

<xsl:template mode="preceding-cousin2" match="*">
    <xsl:variable name="depth" select="count(ancestor::*)"/>
    <xsl:copy-of select="preceding::*[count(ancestor::*) = $depth]"/>
</xsl:template>

Upvotes: 0

Michael Kay
Michael Kay

Reputation: 163595

To do Nth cousins, where N is a parameter supplied dynamically, I would write a recursive function, which necessarily makes it XPath 2.0+.

In XQuery notation

declare function my:cousins($node as node(), 
                            $degree as xs:integer) as node()* {
   if ($degree eq 0) 
   then $node/../*
   else my:cousins($node/.., $degree - 1)/*
} 
  

Upvotes: 1

Michael Kay
Michael Kay

Reputation: 163595

An XPath 1.0 solution would be

(preceding-sibling::* | ../preceding-sibling::*/*)[last()]

Upvotes: 1

Michael Kay
Michael Kay

Reputation: 163595

I think I would probably do

let $this := .
let $precedingCousins := (../../*/*)[. << $this]
return $precedingCousins[last()]

or its equivalent in XSLT.

Upvotes: 1

kjhughes
kjhughes

Reputation: 111726

If I understand your requirements correctly, XPath 1.0 is all you need.

Given your input XML, this XSLT 2.0 (for the test program – the key XPath is 1.0),

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="2.0">
  <xsl:output indent="yes"/>
  <xsl:template match="/">
    <results>
      <xsl:for-each select="//level2">
        <case id="{@id}">
          <xsl:variable name="depth" select="count(ancestor::*)"/>
          <xsl:sequence select="preceding::level2[count(ancestor::*) = $depth][1]"/>
        </case>
      </xsl:for-each>
    </results>
  </xsl:template>
</xsl:stylesheet>

produces these test results,

<?xml version="1.0" encoding="UTF-8"?>
<results>
   <case num="1"/>
   <case num="2">
      <level2 id="1"/>
   </case>
   <case num="3">
      <level2 id="2"/>
   </case>
   <case num="4">
      <level2 id="3"/>
   </case>
   <case num="5">
      <level2 id="4"/>
   </case>
   <case num="9"/>
   <case num="6">
      <level2 id="5">
         <level2 id="9"/>
      </level2>
   </case>
   <case num="7">
      <level2 id="6"/>
   </case>
   <case num="8">
      <level2 id="7"/>
   </case>
</results>

which covers the two cases of concern and produces reasonable results for all starting level2 elements as well.

Upvotes: 3

Martin Honnen
Martin Honnen

Reputation: 167716

I can't think of an easy expression, I wonder whether the following code using path and xsl:evaluate implements the requirement:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  xmlns:mf="http://example.com/mf"
  expand-text="yes">
  
  <xsl:function name="mf:cousins" as="node()*">
    <xsl:param name="node" as="node()"/>
    <xsl:variable name="path" select="path($node)"/>
    <xsl:variable name="path-without-predicates" select="replace($path, '\[[0-9]+\]', '')"/>
    <xsl:variable name="cousins" as="node()*">
      <xsl:evaluate context-item="$node" xpath="$path-without-predicates"/>
    </xsl:variable>
    <xsl:sequence select="$cousins except $node"/>
  </xsl:function>
  
  <xsl:function name="mf:preceding-cousins" as="node()*">
    <xsl:param name="node" as="node()"/>
    <xsl:sequence select="mf:cousins($node)[. &lt;&lt; $node]"/>
  </xsl:function>
  
  <xsl:template match="level2[@id = 6] | level2[@id = 5]">
    <xsl:comment>preceding cousin: {mf:preceding-cousins(.)[last()]=>serialize(map{ 'method' : 'xml' })}</xsl:comment>
    <xsl:next-match/>
  </xsl:template>

  <xsl:output method="xml" indent="no"/>

  <xsl:mode on-no-match="shallow-copy"/>

</xsl:stylesheet>

It looks a bit like a "hack" to use the path function, then string replacement and xsl:evaluate but I don't see a simple, direct XPath.

Upvotes: 1

Related Questions