alex
alex

Reputation: 133

Advanced XPath query

I have an XML file that looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<PrivateSchool>

     <Teacher id="teacher1">
         <Name>
           teacher1Name
         </Name>
    </Teacher>

    <Teacher id="teacher2">
        <Name>
            teacher2Name
        </Name>
    </Teacher>

  <Student id="student1">
    <Name>
      student1Name
    </Name>
  </Student>

  <Student id="student2">
    <Name>
      student2Name
    </Name>
  </Student>

    <Lesson student="student1" teacher="teacher1"  />
    <Lesson student="student2" teacher="teacher2"  />
    <Lesson student="student3" teacher="teacher3"  />
    <Lesson student="student1" teacher="teacher2"  />
    <Lesson student="student3" teacher="teacher3"  />
    <Lesson student="student1" teacher="teacher1"  />
    <Lesson student="student2" teacher="teacher4"  />
    <Lesson student="student1" teacher="teacher1"  />

</PrivateSchool>

There's also a DTD associated with this XML, but I assume it's not much relevant to my question. Let's assume all needed teachers and students are well defined.

What is the XPath query that returns the teachers' NAMES, that have at least one student that took more than 10 lessons with them?

I was looking at many XPath sites/examples. Nothing seemed advanced enough for this kind of question.

Upvotes: 2

Views: 3172

Answers (4)

freesoft
freesoft

Reputation: 1144

The solution posted by Michael Kay for xpath 2.0 is correct, but aproximate. An exact solution for the xml posted at the question would be (without absolute paths):

//Teacher[
           for $t in . return 
             some $s in //Student satisfies 
               count(//Lesson[@teacher = $t/@id and @student = $s/@id]) gt 1
         ]/Name

(I used "gt 1" instead of "gt 10" in order to get some result)

Upvotes: 0

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243569

Use this XPath 2.0 expression:

for $limit in 2,
    $t in /*/Teacher,
    $id in $t/@id,
    $s in /*/Student/@id,
    $numLessons in
       count(/*/Lesson[@teacher eq $id
                     and @student eq $s])
 return
    if($numLessons gt $limit)
      then
        (string-join(($t/Name, $s, xs:string($numLessons)), ' '),
          '&#xA;'
         )
      else ()

here I have set $limit to 2, so that when this XPath expression is evaluated against the provided XML document:

<PrivateSchool>
    <Teacher id="teacher1">
        <Name>teacher1Name</Name>
    </Teacher>
    <Teacher id="teacher2">
        <Name>teacher2Name</Name>
    </Teacher>
    <Student id="student1">
        <Name>student1Name</Name>
    </Student>
    <Student id="student2">
        <Name>student2Name</Name>
    </Student>
    <Lesson student="student1" teacher="teacher1"  />
    <Lesson student="student2" teacher="teacher2"  />
    <Lesson student="student3" teacher="teacher3"  />
    <Lesson student="student1" teacher="teacher2"  />
    <Lesson student="student3" teacher="teacher3"  />
    <Lesson student="student1" teacher="teacher1"  />
    <Lesson student="student2" teacher="teacher4"  />
    <Lesson student="student1" teacher="teacher1"  />
</PrivateSchool>

it produces the correct result:

teacher1Name student1 3 

In your real expression you'll have $limit set to 10 and will only return the teachers' names:

for $limit in 10,
    $t in /*/Teacher,
    $id in $t/@id,
    $s in /*/Student/@id,
    $numLessons in
        count(/*/Lesson[@teacher eq $id
                      and @student eq $s])
 return
    if($numLessons gt $limit)
      then ($t/Name, '&#xA;')
      else ()

Upvotes: 1

user357812
user357812

Reputation:

This is an XPath 2.0 solution:

(/PrivateSchool
   /Lesson)
      [index-of(
          /PrivateSchool
            /Lesson
               /concat(@student, '|', @teacher),
          concat(@student, '|', @teacher)
       )[10]
      ]/(for $teacher in @teacher
         return /PrivateSchool
                   /Teacher[@id = $teacher]
                      /Name)

Upvotes: 1

Michael Kay
Michael Kay

Reputation: 163595

Doing a complex join in a single XPath may be possible, but you're banging your head against a brick wall. XQuery or XSLT are much more suited to this kind of thing. Here it is in XQuery:

declare variable $doc as doc('data.xml');

declare function local:numLessons($teacher, $student) {
  return count($doc//Lesson[@teacher = $teacher and @student = $student])
};

$doc//Teacher[some $s in //Lesson/@student satisfies local:numLessons(@id, $s) gt 10]/Name

Having done that, if you are really determined you can reduce it to XPath 2.0:

doc('data.xml')//Teacher[
   for $t in . return 
     some $s in //Lesson/@student satisfies 
       count(//Lesson[@teacher = $t and @student = $s]) gt 10] /Name

Not tested.

Upvotes: 1

Related Questions