Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Find position of a node within a nodeset using xpath

After playing around with position() in vain I was googling around for a solution and arrived at this older stackoverflow question which almost describes my problem.

The difference is that the nodeset I want the position within is dynamic, rather than a contiguous section of the document.

To illustrate I'll modify the example from the linked question to match my requirements. Note that each <b> element is within a different <a> element. This is the critical bit.

<root>
    <a>
        <b>zyx</b>
    </a>
    <a>
        <b>wvu</b>
    </a>
    <a>
        <b>tsr</b>
    </a>
    <a>
        <b>qpo</b>
    </a>
</root>

Now if I queried, using XPath for a/b I'd get a nodeset of the four <b> nodes. I want to then find the position within that nodeset of the node that contains the string 'tsr'. The solution in the other post breaks down here: count(a/b[.='tsr']/preceding-sibling::*)+1 returns 1 because preceding-sibling is navigating the document rather than the context node-set.

Is it possible to work within the context nodeset?

like image 746
philsquared Avatar asked Apr 09 '10 09:04

philsquared


People also ask

What is node test in XPath?

XPath defines several node tests that can be used to select nodes from the source tree. Strictly speaking, any XPath expression can be considered a node test; the expression para , for example, selects all <para> elements from the context node.

What does descendant mean in XPath?

The descendant axis indicates all of the children of the context node, and all of their children, and so forth. Attribute and namespace nodes are not included - the parent of an attribute node is an element node, but attribute nodes are not the children of their parents.


1 Answers

Here is a general solution that works on any node that belongs in any node-set of nodes in the same document:

I am using XSLT to implement the solution, but finally obtain a single XPath expression that may be used with any other hosting language.

Let $vNodeSet be the node-set and $vNode be the node in this node-set whose position we want to find.

Then, let $vPrecNodes contains all nodes in the XML document preceding $vNode.

Then, let $vAncNodes contains all nodes in the XML document that are ancestors of $vNode.

The set of nodes in $vNodeSet that precede $vNode in document order consists of all nodes in the nodeset that belong also to $vPrecNodes and all nodes in the node-set that also belong to $vAncNodes.

I will use the well-known Kaysian formula for intersection of two nodesets:

$ns1[count(.|$ns2) = count($ns2)]

contains exactly the nodes in the intersection of $ns1 with $ns2.

Based on all this, let $vPrecInNodeSet is the set of nodes in $vNodeSet that precede $vNode in document order. The following XPath expression defines $vPrecInNodeSet:

$vNodeSet
      [count(.|$vPrecNodes) = count($vPrecNodes)
      or
       count(.|$vAncNodes) = count($vAncNodes)
      ]

Finally, the wanted position is: count($vPrecInNodeSet) +1

Here's how this all works together:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

 <xsl:variable name="vNodeSet" select="/*/a/b"/>

 <xsl:variable name="vNode" select="$vNodeSet[. = 'tsr'][1]"/>

 <xsl:variable name="vPrecNodes" select="$vNode/preceding::node()"/>

 <xsl:variable name="vAncNodes" select="$vNode/ancestor::node()"/>

 <xsl:variable name="vPrecInNodeSet" select=
  "$vNodeSet
      [count(.|$vPrecNodes) = count($vPrecNodes)
      or
       count(.|$vAncNodes) = count($vAncNodes)
      ]
  "/>

 <xsl:template match="/">
   <xsl:value-of select="count($vPrecInNodeSet) +1"/>
 </xsl:template>
</xsl:stylesheet>

When the above transformation is applied on the provided XML document:

<root>
    <a>
        <b>zyx</b>
    </a>
    <a>
        <b>wvu</b>
    </a>
    <a>
        <b>tsr</b>
    </a>
    <a>
        <b>qpo</b>
    </a>
</root>

the correct result is produced:

3

Do note: This solution does not depend on XSLT (used only for illustrative purposes). You may assemble a single XPath expression, substituting the variables with their definition, until there are no more variables to substitute.

like image 151
Dimitre Novatchev Avatar answered Nov 15 '22 00:11

Dimitre Novatchev