92

Anyone know how to get the position of a node using XPath?

Say I have the following xml:

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

I can use the following xpath query to select the third <b> node (<b>tsr</b>):

a/b[.='tsr']

Which is all well and good but I want to return the ordinal position of that node, something like:

a/b[.='tsr']/position()

(but a bit more working!)

Is it even possible?

edit: Forgot to mention am using .net 2 so it's xpath 1.0!


Update: Ended up using James Sulak's excellent answer. For those that are interested here's my implementation in C#:

int position = doc.SelectNodes("a/b[.='tsr']/preceding-sibling::b").Count + 1;

// Check the node actually exists
if (position > 1 || doc.SelectSingleNode("a/b[.='tsr']") != null)
{
    Console.WriteLine("Found at position = {0}", position);
}
Syscall
  • 19,327
  • 10
  • 37
  • 52
Wilfred Knievel
  • 3,225
  • 1
  • 26
  • 33
  • Please try not to post answers in the question -> it would be better to have posted this as an answer, then *possibly* linked to it from the question. – theMayer Jan 16 '19 at 16:08

8 Answers8

105

Try:

count(a/b[.='tsr']/preceding-sibling::*)+1.
Wayne
  • 59,728
  • 15
  • 131
  • 126
James Sulak
  • 31,389
  • 11
  • 53
  • 57
  • 1
    'Coz I'm using .net & either it or I can't handle the power I went with: int position = doc.SelectNodes("a/b[.='tsr']/preceding-Sibling::b").Count + 1; if (position > 1 || doc.SelectSingleNode("a/b[.='tsr']") != null) // Check the node actually exists { // Do magic here } – Wilfred Knievel Oct 24 '08 at 09:52
  • 1
    in zero indexed languages you don't need the +1 – JonnyRaa Nov 23 '17 at 14:50
  • If there is no match then `count` returns 0, but the +1 means that this will always equate to 1. So no matter what string you use regardless of whether it matches or not, 1 will always be returned - this answer solves that issue https://stackoverflow.com/a/27173072/1798234 – james Jul 15 '22 at 10:02
9

You can do this with XSLT but I'm not sure about straight XPath.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" encoding="utf-8" indent="yes" 
              omit-xml-declaration="yes"/>
  <xsl:template match="a/*[text()='tsr']">
    <xsl:number value-of="position()"/>
  </xsl:template>
  <xsl:template match="text()"/>
</xsl:stylesheet>
Steven Huwig
  • 20,015
  • 9
  • 55
  • 79
9

I realize that the post is ancient.. but..

replace'ing the asterisk with the nodename would give you better results

count(a/b[.='tsr']/preceding::a)+1.

instead of

count(a/b[.='tsr']/preceding::*)+1.
user414661
  • 1,042
  • 9
  • 4
6

If you ever upgrade to XPath 2.0, note that it provides function index-of, it solves problem this way:

index-of(//b, //b[.='tsr'])

Where:

  • 1st parameter is sequence for searching
  • 2nd is what to search
CroWell
  • 606
  • 8
  • 15
  • It should be noted that this will only work with XPath 2+. Anything below that will have to use the 'weird' count function. – Dan Atkinson Dec 16 '15 at 11:36
  • 1
    @Dan, it was noted in the link to original docs, added explicit notice, thanks! – CroWell Dec 16 '15 at 15:03
  • A little example in-situ (get the position of the element France in a XML doc) : index-of(//country_name/common_name, //country_name/common_name[text()="France"]) – Onyr Mar 01 '21 at 14:36
3

Just a note to the answer done by James Sulak.

If you want to take into consideration that the node may not exist and want to keep it purely XPATH, then try the following that will return 0 if the node does not exist.

count(a/b[.='tsr']/preceding-sibling::*)+number(boolean(a/b[.='tsr']))
3

Unlike stated previously 'preceding-sibling' is really the axis to use, not 'preceding' which does something completely different, it selects everything in the document that is before the start tag of the current node. (see http://www.w3schools.com/xpath/xpath_axes.asp)

Damien
  • 2,254
  • 1
  • 22
  • 30
  • 4
    Not including ancestor nodes. Don't trust w3schools on the details! But I agree... although preceding:: works in this case, because there are no elements before the relevant b elements other than the a ancestor, it's more fragile than preceding-sibling. OTOH, the OP didn't tell us what context he wanted to know the position within, so potentially preceding:: could be right. – LarsH Aug 18 '10 at 18:44
0

The problem is that the position of the node doesn't mean much without a context.

The following code will give you the location of the node in its parent child nodes

using System;
using System.Xml;

public class XpathFinder
{
    public static void Main(string[] args)
    {
        XmlDocument xmldoc = new XmlDocument();
        xmldoc.Load(args[0]);
        foreach ( XmlNode xn in xmldoc.SelectNodes(args[1]) )
        {
            for (int i = 0; i < xn.ParentNode.ChildNodes.Count; i++)
            {
                if ( xn.ParentNode.ChildNodes[i].Equals( xn ) )
                {
                    Console.Out.WriteLine( i );
                    break;
                }
            }
        }
    }
}
Andrew Cox
  • 10,672
  • 3
  • 33
  • 38
0

I do a lot of Novell Identity Manager stuff, and XPATH in that context looks a little different.

Assume the value you are looking for is in a string variable, called TARGET, then the XPATH would be:

count(attr/value[.='$TARGET']/preceding-sibling::*)+1

Additionally it was pointed out that to save a few characters of space, the following would work as well:

count(attr/value[.='$TARGET']/preceding::*) + 1

I also posted a prettier version of this at Novell's Cool Solutions: Using XPATH to get the position node

geoffc
  • 4,030
  • 7
  • 44
  • 51