16

Is there a way to query an XML document to return the maximum of a given attribute using Xpath 1.0 ?

For example is there a way to get the max id ?

<?xml version="1.0" encoding="utf-8"?>
<library>
        <book id="2" name="Dragon Tatoo"/>
        <book id="7" name="Ender's Game"/>
        <book id="3" name="Catch 22"/>
        <book id="1" name="Lord of the rings"/>
</library>
Alsatian
  • 3,086
  • 4
  • 25
  • 40
HerbSpiral
  • 183
  • 1
  • 2
  • 8
  • 1
    What is your host language for executing the XPath? If you're using XPath 1.0 (which does not have a `max` function) then it's probably faster to select all of the elements first and find the max in your PL. – Wayne Jan 03 '12 at 01:33

7 Answers7

10

In XPath 2.0, use the max function. To find the book with the highest id, do

/library/book[@id = max(/library/book/@id)]
Fred Foo
  • 355,277
  • 75
  • 744
  • 836
6

The following XPath selects the book with highest id:

/library/book[not(@id <= preceding-sibling::book/@id) and not(@id <=following-sibling::book/@id)]
dertkw
  • 7,798
  • 5
  • 37
  • 45
  • This indeed works, however the performance is not that good (when thousands of ids are present in the document) – HerbSpiral Jan 02 '12 at 15:19
  • +1 - I repeated the core of your answer, but I just wanted to provide additional information in my answer, including some of what's been spread out around the comments. – Wayne Jan 03 '12 at 02:09
  • Does not work if all the elements have the same value – Anonymoose Feb 27 '18 at 14:58
3

I've found that answers like the lwburk's or timbooo's work fine for attributes representing numbers having just one digit. However, if the attribute is a number having more than one digit, extrange things seem to happen when comparing between attributes' values. For example, try changing the original XML data with something like this:

<?xml version="1.0" encoding="utf-8"?>
<library>
        <book id="250" name="Dragon Tatoo"/>
        <book id="700123" name="Ender's Game"/>
        <book id="305" name="Catch 22"/>
        <book id="1070" name="Lord of the rings"/>
</library>

Running the suggested snippets won't work. I got a solution using the casting operator xs:int() applied on id attribute, like in:

/library/book[not(xs:int(@id) <= preceding-sibling::book/@id) and not(xs:int(@id) <=following-sibling::book/@id)]

That will give the correct answer!

Ricardo
  • 46
  • 1
2

If you're willing to use external tooling - which depends on your implementation featuring implementations of these tools - try the EXSLT:Math function highest().

The fact that EXSLT implements this implies that such a feature isn't directly available in plain xpath, of course. If you're not using Transforms, or want to stick purely with standards-compliant markup, other posters' suggestions would be a better choice.

Tom W
  • 5,108
  • 4
  • 30
  • 52
2

Note: The following information assumes use of XPath 1.0.

The following expression returns the element(s) with the largest id value:

/*/book[not(@id < preceding-sibling::book/@id) and 
        not(@id < following-sibling::book/@id)]

Note that this is slightly different than @timbooo's answer in that this will return more than one element when there are duplicates with the same max value (@timbooo's would return none). If you want only one element in this case, then you need a resolution strategy. To choose the first such element in document order, use this:

/*/book[not(@id < preceding-sibling::book/@id) and 
        not(@id < following-sibling::book/@id)][1]

To choose the last one, use this:

/*/book[not(@id < preceding-sibling::book/@id) and 
        not(@id < following-sibling::book/@id)][last()]

This approach is very inefficient (O(n^2)) because it requires you to compare each element to every other potential max. For this reason, it's probably best to use your host programming language to select the maximum element. Simply select all of the book elements first and then choose the max from that list. This is (most likely) a linear operation (O(n)), which would be noticeably faster on very large documents. For example, in Java (JAXP) you might do it like this:

XPath xpath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xpath.evaluate("/*/book", doc,
        XPathConstants.NODESET);
Node max = nodes.item(0);
for (int i = 0; i < nodes.getLength(); i++) {
    int maxval = Integer.parseInt(max.getAttributes()
            .getNamedItem("id").getNodeValue());
    int curval = Integer.parseInt(nodes.item(i).getAttributes()
            .getNamedItem("id").getNodeValue());
    if (curval >= maxval)
        max = nodes.item(i);
}
System.out.println(max.getAttributes().getNamedItem("name"));

Note that this is just a demonstration; be sure to include null-checks where appropriate.

Wayne
  • 59,728
  • 15
  • 131
  • 126
1

XPath 1.0

/library/book[not(@id < /library/book/@id)]

This query style is more generic and works even if books are grouped i.e.

<?xml version="1.0" encoding="utf-8"?>
<library>
    <genre id="1">
        <book id="2" name="Dragon Tatoo"/>
        <book id="7" name="Ender's Game"/>
    </genre>
    <genre id="2">
        <book id="3" name="Catch 22"/>
        <book id="1" name="Lord of the rings"/>
    </genre>
</library>

Same query still works (the path should be modified)

/library/genre/book[not(@id < /library/genre/book/@id)]

or even

//book[not(@id < //book/@id)]

To avoid performance troubles use XPath 2 max() instead

serge
  • 992
  • 5
  • 8
0

This example can be used to find the max.

XmlDocument doc = new XmlDocument();                    
doc.Load("../../Employees.xml");
XmlNode node = doc.SelectSingleNode("//Employees/Employee/@Id[not(. <=../preceding-sibling::Employee/@id) and not(. <=../following-sibling::Employee/@Id)]");
int maxId = Convert.ToInt32(node.Value);

For other similar topics on xpath and linq check out http://rmanimaran.wordpress.com/2011/03/20/xml-find-max-and-min-value-in-a-attribute-using-xpath-and-linq/

Jpepper
  • 437
  • 3
  • 15