11

I've looked at this question: Select first instance only with XPath?

But if I have a node-set like this:


<container value="">
  <data id="1"></data>
  <data id="2">test</data>
  <container>
    <data id="1">test</data>
    <data id="3">test</data>
  </container>
</container>

Now my scenario is that this node-set is deep inside a document, and I have a pointer to the inner container. So I have to prefix my XPath with "/container/container" (the path is actually longer, but for this example this should do).

EDIT: What I want is a "data" node with an id of 1, which should come from the lowest node first or the closest ancestor. So, if I can't find it on the "current" (/container/container) I should look at the ancestors and get the nearest one (or in the end, not find anything). I have tried this:

/container/container/ancestor-or-self::container/data[@id="1"]

Which brings back a result set with two nodes. I thought I could use last() to get the deepest one, so I tacked that on the end, but to no avail.

Community
  • 1
  • 1
Nick Spacek
  • 4,717
  • 4
  • 39
  • 42
  • The specification could do with more clarity. You want the data node? The data node must have an id of 1? Form the original container if the data node does not have an id of 1 you want to move up the document to a parent container to see if that has a data node with an id of 1, if not keep going until data node with an id of 1 is found. Does that describe it? – AnthonyWJones May 21 '09 at 12:01
  • I tried to describe what I was looking for a bit clearer, though some guessed it. Sorry! – Nick Spacek May 21 '09 at 12:43

5 Answers5

12

Make sure the last() is scoped correctly. Try:

(/container/container/ancestor-or-self::container/data[@id="1"])[last()]

Similarly:

//container[last()]

and:

(//container)[last()]

are not the same thing. The first will return the last container node at each level of the hierarchy - multiple matches - and the second will return the last container node found - one match.

David Webb
  • 190,537
  • 57
  • 313
  • 299
6

How did you try tacking last() on? I think this should work:

/container/container/ancestor-or-self::container/data[@id="1"][last()]

EDIT:

Right, of course, I forgot the parentheses:

(/container/container/ancestor-or-self::container/data[@id="1"])[last()]

Which makes this the same as one of the other answers; however, as the original poster points out, this expression fails on:

<container value="">
  <container>
    <data id="1">a3</data>
    <data id="3">a4</data>
  </container>
  <data id="1">a1</data>
  <data id="2">a2</data>
</container>

True to the spirit of stackoverflow, I can however combine my answer with one of the other answers and get something that works in all cases:

(/container/container/ancestor-or-self::container[data[@id="1"]])[last()]/data[@id="1"]

By the way, if there are multiple @id-is-1 <data> children of one container, this will return all of them. To return only the first such <data> element:

(/container/container/ancestor-or-self::container[data[@id="1"]])[last()]/data[@id="1"][1]
Daniel Martin
  • 23,083
  • 6
  • 50
  • 70
  • If my reading here on the desired result is correct (big if) this won't work because the last() will match on both branches of ancestor-or-self - that's the real problem, so you need to clip the result set either early (as I have) or late (as Dave does). – annakata May 21 '09 at 11:55
  • This one does it. Thanks a lot (everyone too)! – Nick Spacek May 25 '09 at 11:31
1

try [position()=1]

ex.

/container/container/ancestor-or-self::container/data[position()=1]

1

I'm not 100% clear on what you're asking for, but if I read this right maybe you just want:

/container/container/ancestor-or-self::container[1]/data[@id='1']

note the "[1]"

Edit: thought about it a little more and the below works for me for all cases so far looking for @id=$N

/container/container/ancestor-or-self::container[data[@id=$N]][1]/data[@id=$N]

Pretty extreme xpathing right there. Basically what's happening is that the nodeset from ancestor-or-self returns the lowest order at position 1 - that's the one you want - but you need to put a second condition on it that node also has a data node you're interested in, once you have those conditions met you have the right node now you just want to tunnel further in to the actual data.

99% of the time xpath gets easier if you take it apart and think of it as an algorithm :)

annakata
  • 74,572
  • 17
  • 113
  • 180
  • I tried that, but that doesn't cover every case. What if I'm looking instead for @id='2'? I want to favor the results from the current node before the ancestors, but be able to fallback. – Nick Spacek May 21 '09 at 18:32
0

Instead of

@id="1"

have you tried:

position() == 1
skaffman
  • 398,947
  • 96
  • 818
  • 769
  • If position() == 1 would work then just dropping the ancestor-or-self:: all together. I think the requirement is not for just any first data node but a data node with an id of 1. The Question isn't as clear as it could be. – AnthonyWJones May 21 '09 at 12:04