Reputation: 2807
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
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
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
Reputation: 163595
An XPath 1.0 solution would be
(preceding-sibling::* | ../preceding-sibling::*/*)[last()]
Upvotes: 1
Reputation: 163595
I think I would probably do
let $this := .
let $precedingCousins := (../../*/*)[. << $this]
return $precedingCousins[last()]
or its equivalent in XSLT.
Upvotes: 1
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
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)[. << $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