2

I need to fetch a node set containing elements that refer to exactly two other elements. I can't figure out how to do it in a single XPath expression.

My (simplified) source XML looks like this:

<group>
   <id>1337</id>
</group>
<group>
   <id>1338</id>
</group>
<member>
   <id>31415</id>
   <groupId>1337</groupId>
</member>
<member>
   <id>31416</id>
   <groupId>1337</groupId>
</member>
<member>
   <id>31417</id
   <groupId>1338</groupId>
</member>

Now, I want to select all <group> nodes that refer to exactly two <member>s, which should result in returning the group with id=1337 only. I tried the following...

<xsl:variable name="groupsWithTwoMembers" select="group[count(../member[id=??]) = 2]"/>

...and obviously at the '??' I need to insert the groupId of the group I selected at the start of the XPath expression, but I can't think of a way to get to this groupId. Can anyone out there think of one? Thanks!

Hanno
  • 563
  • 5
  • 15
  • Good question, +1. In addition to @Alejandro 's good answer, see my answer for an XPath2.0 one-liner solution. :) – Dimitre Novatchev Nov 12 '10 at 18:56
  • Hi Dimitre, thanks for the thumbs-up. Your XPath2.0 solution looks good too, but unfortunately I'm stuck with XPath1.0 due to the software architecture of our project. Thanks anyway for your input! – Hanno Nov 15 '10 at 10:28

4 Answers4

2

The problem is that predicates filters previous selected nodes with each of those nodes as context. So, you need to pull in the out of context reference.

This can be done with variable/parameter reference or current() function (results in context node for the whole XSLT instruction).

Also, in this case having cross references, it's better to use keys:

<xsl:key name="kMemberByGroupId" match="member" use="groupId"/>  
...
<xsl:variable name="groupsWithTwoMembers" 
              select="group[count(key('kMemberByGroupId',id)) = 2]"/>
  • Thanks, Alejandro! I implemented your suggestion (to use keys) and it works like a charm! You've really helped me out here, so thanks a lot! – Hanno Nov 15 '10 at 10:27
1

XPath2.0:

$p/group[count(index-of($p/member/groupId, id)) eq 2]

where $p is the parent of the provided XML fragment.

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • @Dimitre: +1 Good answer. Do you realize that your descovery about `fn:index-of` usage for "getting not reapeated" it's usable for grouping without the need of `for ... in distinct-values()`? –  Nov 12 '10 at 19:06
  • @Alejandro: Yes, I am not using `distinct-values()` in this XPath expression. – Dimitre Novatchev Nov 12 '10 at 20:21
  • @Dimitre: I meant that `$sequence[index-of($sequence,.)[2]]` gives the repeated values, but `$sequence[index-of($sequence,.)[1]]` gives the "first of a kind". –  Nov 12 '10 at 21:58
  • @Alejandro: Yes, but I am not sure which is more efficient. – Dimitre Novatchev Nov 12 '10 at 22:01
  • @Dimitre: You are right about that. The difference is semantic: `fn:distinct-values()` should be more efficient but the result is a sequence of atomic values. –  Nov 12 '10 at 22:13
  • @LarsH: THen you definitely havent seen this: http://stackoverflow.com/questions/133092/how-do-you-identify-duplicate-elements-in-an-xpath-2-0-sequence/287360#287360 :) – Dimitre Novatchev Nov 12 '10 at 23:03
  • @Dimitre: whoa. Yeah, thanks for pointing out that gem. :-) (Too bad about all the spam on your blog entry!) – LarsH Nov 12 '10 at 23:23
  • Really interesting approach! I'll keep the index-of function in mind; it might really help me out some day! – Hanno Nov 15 '10 at 10:30
0

In XPath 1.0 this is not possible. In XPath 2.0 you can do the following (untested):

for $a in fn:distinct-values(//group/id) 
  if count(../../member[@id = $a]) = 2 then return $a
ordnungswidrig
  • 3,140
  • 1
  • 18
  • 29
  • Besides missing right brackets, it doesn't look quite right... the `//group[...]` doesn't make sense, because the predicate doesn't use the context group except for `..`, which is the same for all groups. Instead maybe you mean `for $a in fn:distinct-values(//group/id) if count(../../member[@id = $a]) = 2 then return $a`? – LarsH Nov 12 '10 at 21:15
  • Thanks for the catch. Your proposal looks correct. I update my answer. – ordnungswidrig Nov 16 '10 at 15:27
0

You should look into the difference between the current node and context node. They are usually the same but when dealing with predicates, the context node (often represented with a .) matches the node you are predicating against. In this situation you should use current() to access the current node rather than the context node.

Chris Cameron-Mills
  • 4,587
  • 1
  • 27
  • 28
  • As Mike Kay expresses it, `current()` returns the context node as it is "outside" the whole XPath expression. But in this situation, where the OP has `select="group[count(../member[id=??]) = 2]"`, it's not the context node outside the XPath expression that we want; it's the context node of each group (i.e. outside the outer predicate). Unfortunately XPath 1.0 has no standard way to access this "intermediate" context. – LarsH Nov 12 '10 at 21:18