XPath 1.0 to get highest date from XML - xpath

In an XML file I have a number of date fields. I need to find out which one is the highest value. Unfortunately, I have to use XPath 1.0, so there's not an easy way to do it.
I started out with this XML:
<root>
<value>20120103</value>
<value>20130103</value>
<value>20120101</value>
<value>20140103</value>
<value>20100103</value>
</root>
From this, I can get the highest value with this xpath statement:
/root/value[not(text() <= preceding-sibling::value/text()) and not(text() <=following-sibling::value/text
())]
However, the real XML I'm working with got date fields like this:
<root>
<value>2012-01-03</value>
<value>2013-01-03</value>
<value>2012-01-01</value>
<value>2014-01-03</value>
<value>2010-01-03</value>
</root>
To execute the same XPath, I have to use substring and concat to remove the minus signs. However, the application I'm using the XPath in and the Oxygen XML editor tell me that concat and substring are no Xpath 1.0 functions (although several pages tell me that they are...)
When I tell Oxygen that it's Xpath 2.0, I can get the values with this statement:
/root/value[not(concat(concat(substring(text(),1,4),substring(text(),6,2)),substring(text(),9,2)) <= preceding-sibling::value/concat(concat(substring(text(),1,4),substring(text(),6,2)),substring(text(),9,2))) and not
(concat(concat(substring(text(),1,4),substring(text(),6,2)),substring(text(),9,2)) <=following-sibling::value/concat(concat(substring(text(),1,4),substring(text(),6,2)),substring(text(),9,2)))]
Any ideas why it isn't working with Xpath 1.0 and how I can get around this?
Edit:
I thought I was getting closer by using translate to remove the minus signs:
/root/value[not(translate(text(),'-','') <= translate(preceding-sibling::value/text(),'-','')) and not(translate(text(),'-','') <=translate(following-sibling::value/text(),'-','')
)]
but for one or another reasons this produces two values:
2013-01-03
2014-01-03
While I would only expect the second one

This cannot be determined with a single XPath 1.0 expression because:
In XPath 1.0 there isn't an < or > operator for strings.
Although one can use translate() to remove the hyphens from the context node, it isn't possible in a single XPath 1.0 expression to compare this with all other values, each of which has undergone the same translation.
XPath 2.0 solution:
max(/*/*/xs:date(.))

Actually, your first XPath
/root/value[not(text() <= preceding-sibling::value/text()) and not(text() <=following-sibling::value/text())]
should work fine in both cases.
As long as the format is in year, month, day order with leading zeros...
However, it will not work, if there are duplicated days, so better use:
(/root/value[not(text() < preceding-sibling::value/text()) and not(text() < following-sibling::value/text())])[1]

Related

xpath generate random number

Is there a way to generate a random number with xpath? My input is any well-formed xml, the output should be a random integer of a given length.
I usually achieve it with any coding or xslt but I'm struggling to find a working xpath expression.
XPath 3.1 has a function fn:random-number-generator().
In earlier XPath versions you'll need to improvise.
When asking XPath questions please say which version you are using - the ancient XPath 1.0 is still in widespread use so it's impossible to make guesses.
Since this question is the top google result, I'll say in XPath 3.1, Mr. Kay's answer can be achieved like this:
Say you want a random element. Count the elements:
<xsl:variable name="random-upper-limit" select="count(/myroot/mypath/myelement)"/>
Then get a random index number:
<xsl:variable name="my-random-number" select="random-number-generator()['next']?permute(1 to $random-upper-limit)[1]"/>

How to get multiple occurences of an element with XPath under usage of normalize-space and substring-before

I have an element with three occurences on the page. If i match it with Xpath expression //div[#class='col-md-9 col-xs-12'], i get all three occurences as expected.
Now i try to rework the matching element on the fly with
substring-before(//div[#class='col-md-9 col-xs-12'], 'Bewertungen'), to get the string before the word "Bewertungen",
normalize-space(//div[#class='col-md-9 col-xs-12']), to clean up redundant whitespaces,
normalize-space(substring-before(//div[#class='col-md-9 col-xs-12'] - both actions.
The problem with last three expressions is, that they extract only the first occurence of the element. It makes no difference, whether i add /text() after matching definition.
I don't understand, how an addition of normalize-space and/or substring-before influences the "main" expression in the way it stops to recognize multiple occurences of targeted element and gets only the first. Without an addition it matches everything as it should.
How is it possible to adjust the Xpath expression nr. 3 to get all occurences of an element?
Example url is https://www.provenexpert.com/de-de/jazzyshirt/
The problem is that both normalize-space() and substring-before() have a required cardinality of 1, meaning can only accept one occurrence of the element you are trying to normalize or find a substring of. Each of your expressions results in 3 sequences which these two functions cannot process. (I probably didn't express the problem properly, but I think this is the general idea).
In light of that, try:
//div[#class='col-md-9 col-xs-12']/substring-before(normalize-space(.), 'Bewertung')
Note that in XPath 1.0, functions like substring-after(), if given a set of three nodes as input, ignore all nodes except the first. XPath 2.0 changes this: it gives you an error.
In XPath 3.1 you can apply a function to each of the nodes using the apply operator, "!": //div[condition] ! substring-before(normalize-space(), 'Bewertung'). That returns a sequence of 3 strings. There's no equivalent in XPath 1.0, because there's no data type in XPath 1.0 that can represent a sequence of strings.
In XPath 2.0 you can often achieve the same effect using "/" instead of "!", but it has restrictions.
When asking questions on StackOverflow, please always mention which version of XPath you are using. We tend to assume that if people don't say, they're probably using 1.0, because 1.0 products don't generally advertise their version number.

XPath function on multiple text nodes

Using XPath 1.0 want to get list of text nodes applying XPath 'substring' function on every text node
substring(//p/text(), 10)
gives only one first text's sub-string, when
//p/text()
gives all of them, but want all sub-strings as set
EDIT:
Tried
//p/substring(text(), 10)
Says invalid XPath expression
How can I achieve this ?
Thanks in advance
If you want a set of strings as the result of an XPath 1.0 expression, then you're out of luck, because there is no such data type in XPath 1.0: the only collections available are collections of nodes, and you can only select nodes that already exist, you can't create new ones.
With XPath 2.0 this is a piece of cake:
//p/text()/substring(., 10)
So if you possibly can, find yourself an XPath 2.0 processor.

XPath expression

This question regards XPath expressions.
I want to find the average of the length of all URLs in a Web page, that point to a .pdf file.
So far I have constructed the following expression, but it does not work:
sum(string-length(string(//a/#href[contains(., ".pdf")]))) div
count(//a/#href[contains(., ".pdf")])
Any help will be appreciated!
You will need XPath 2.0.
For calculating the sum of the string lengths, you will need either
need a concatenated string of all #hrefs to apply to string-lenght($string as xs:string) (which only allows a single string as parameter), but concat(...) only takes an arbitrary number of atomar strings, not a sequence of those; or
apply string-length(...) on every #href as #Navin Rawat proposed - but using arbitrary functions in axis steps is a new feature of XPath 2.0.
If using XPath 2.0, there are functions avg(...) and ends-with(...) which help you in stripping down the expression to
avg(//a/#href[ends-with(., '.pdf')]/string-length())
If you have to stick with XPath 1.0, all you can do is using my expression below to fetch the URLs and calculate the average outside XPath.
Anyway, the subexpression you proposed will fail at URLs like http://example.net/myfile.pdf.txt. Only compare the end of the URL:
//a[#href[substring(., string-length(.) - 3) = '.pdf']]/#href
And you missed a path step for the attribute, so you've been trying to average the string length of the link names right now.
Please put something like:
sum(//a/#href[contains(.,'.pdf')]/string-length()) div count(//a/#href[contains(.,'.pdf')])

How to use the "translate" Xpath function on a node-set

I have an XML document that contains items with dashes I'd like to strip
e.g.
<xmlDoc>
<items>
<item>a-b-c</item>
<item>c-d-e</item>
<items>
</xmlDoc>
I know I can find-replace a single item using this xpath
/xmldoc/items/item[1]/translate(text(),'-','')
Which will return
"abc"
however, how do I do this for the entire set?
This doesn't work
/xmldoc/items/item/translate(text(),'-','')
Nor this
translate(/xmldoc/items/item/text(),'-','')
Is there a way at all to achieve that?
I know I can find-replace a single
item using this xpath
/xmldoc/items/item[1]/translate(text(),'-','')
Which will return
"abc"
however, how do I do this for the
entire set?
This cannot be done with a single XPath 1.0 expression.
Use the following XPath 2.0 expression to produce a sequence of strings, each being the result of the application of the translate() function on the string value of the corresponding node:
/xmlDoc/items/item/translate(.,'-', '')
The translate function accepts in input a string and not a node-set. This means that writing something like:
"translate(/xmlDoc/items/item/text(),'-','')"
or
"translate(/xmlDoc/items/item,'-','')"
will result in a function call on the first node only (item[1]).
In XPath 1.0 I think you have no other chances than doing something ugly like:
"concat(translate(/xmlDoc/items/item,'-',''),
translate(/xmlDoc/items/item[2],'-',''))"
Which is privative for a huge list of items and returns just a string.
In XPath 2.0 this can be solved nicely using for expressions:
"for $item in /xmlDoc/items/item
return replace($item,'-','')"
Which returns a sequence type:
abc cde
PS Do no confuse function calls with location paths. They are different kind of expressions, and in XPath 1.0 can not be mixed.
here is yet anther example, running it against chrome developer tools, in prepartion for a selenium test.
$x("//table[#id='sometable_table']//tr[1=1 and ./td[2=2 and position()=2 and .//*[translate(text(), ',', '') ='1001'] ] ]/td[position()=2]")
Essentially the the data sometable_table has a column containing numbers that appear localized. For example 1001 would appear as 1,001. With the above you have somewhat nasty xpath expression.
Where first you select all table rows. Then you focus on the data of the position 2 table data for the row. Then you go deeper into the contents of the position=2 table data expand the data on the cell until you find any node whose text after string replacement is 1001. Finally you ask for the table at position 2 to be returned.
But since all your main filters are at the table row level, you could be doing additional filters at table data columns at other positions as well, if you need to find the appropriate table row that has content (A) on a cell column and content (B) on a different column.
NOTE:
It was actually quite nasty to write this, because intuitively, we all google for XPATH replace string. So I was getting furstrated trying to use xpath replace until i realized chrome supports XPATH 1.0. In xpath 1.0 the string functions that exist are different from xpath 2.0, you need to use this translate function.
See reference:
http://www.edankert.com/xpathfunctions.html

Resources