Nested conditional if else statements in xpath - xpath

I have this XML:
<property id="1011">
<leasehold>No</leasehold>
<freehold>Yes</freehold>
<propertyTypes>
<propertyType>RESIDENTIAL</propertyType>
</propertyTypes>
</property>
and I want to create an xpath statement that is same as the following nested if-else pseudocode block.
if( propertyTypes/propertyType == 'RESIDENTIAL') {
if( leasehold == 'Yes' ){
return 'Rent'
} else
return 'Buy'
}
} else {
if( leasehold == 'Yes' ){
return 'Leasehold'
} else
return 'Freehold'
}
}
I've seen something about Becker's method but I couldn't really follow it. XPath isn't my strong point really.

I. In XPath 2.0 one simply translates this to:
if(/*/propertyTypes/propertyType = 'RESIDENTIAL')
then
(if(/*/leasehold='Yes')
then 'Rent'
else 'Buy'
)
else
if(/*/leasehold='Yes')
then 'Leasehold'
else 'Freehold'
XSLT 2.0 - based verification:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:sequence select=
"if(/*/propertyTypes/propertyType = 'RESIDENTIAL')
then
(if(/*/leasehold='Yes')
then 'Rent'
else 'Buy'
)
else
if(/*/leasehold='Yes')
then 'Leasehold'
else 'Freehold'
"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<property id="1011">
<leasehold>No</leasehold>
<freehold>Yes</freehold>
<propertyTypes>
<propertyType>RESIDENTIAL</propertyType>
</propertyTypes>
</property>
the XPath expression is evaluated and the result of this evaluation is copied to the output:
Buy
II. XPath 1.0 solution
In XPath 1.0 there isn't an if operator.
A conditional statement can still be implemented with a single XPath 1.0 expression, but this is more tricky and the expression may not be too readable and understandable.
Here is a generic way (first proposed by Jeni Tennison) to produce $stringA when a condition $cond is true() and otherwise produce $stringB:
concat(substring($stringA, 1 div $cond), substring($stringB, 1 div not($cond)))
One of the main achivements of this formula is that it works for strings of any length and no lengths need to be specified.
Explanation:
Here we use the fact that by definition:
number(true()) = 1
and
number(false()) = 0
and that
1 div 0 = Infinity
So, if $cond is false, the first argument of concat() above is:
substring($stringA, Infinity)
and this is the empty string, because $stringA has a finite length.
On the other side, if $cond is true() then the first argument of concat() above is:
sibstring($stringA, 1)
that is just $stringA.
So, depending on the value of $cond only one of the two arguments of concat() above is a nonempty string (respectively $stringA or $stringB).
Applying this generic formula to the specific question, we can translate the first half of the big conditional expression into:
concat(
substring('rent',
1 div boolean(/*[leasehold='Yes'
and
propertyTypes/propertyType = 'RESIDENTIAL'
]
)
),
substring('buy',
1 div not(/*[leasehold='Yes'
and
propertyTypes/propertyType = 'RESIDENTIAL'
]
)
)
)
This should give you an idea how to translate the whole conditional expression into a single XPath 1.0 expression.
XSLT 1.0 - based verification:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:copy-of select=
"concat(
substring('rent',
1 div boolean(/*[leasehold='Yes'
and
propertyTypes/propertyType = 'RESIDENTIAL'
]
)
),
substring('buy',
1 div not(/*[leasehold='Yes'
and
propertyTypes/propertyType = 'RESIDENTIAL'
]
)
)
)
"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document (above), the XPath expression is evaluated and the result of this evaluation is copied to the output:
buy
Do note:
If you decide to replace the specific strings with other strings that have different lengths than the original, you simply replace these strings in the above XPath 1.0 expression and you don't have to worry about specifying any lengths.

Becker's method for your data is the following:
concat(substring('Rent', 1 div boolean(propertyTypes/propertyType ="RESIDENTIAL" and leasehold="Yes")),
substring('Buy', 1 div boolean(propertyTypes/propertyType ="RESIDENTIAL" and leasehold="No")),
substring('Leasehold', 1 div boolean(propertyTypes/propertyType!="RESIDENTIAL" and leasehold="Yes")),
substring('Freehold', 1 div boolean(propertyTypes/propertyType!="RESIDENTIAL" and leasehold="No")))

Spent all day today, but works for me this is for Xpath 1.0:
concat(
substring(properties/property[#name="Headline"], 1, string-length(properties/property[#name="Headline"]) * 1),
substring(properties/property[#name="Name"], 1, not(number(string-length(properties/property[#name="Headline"]))) * string-length(properties/property[#name="Name"]))
)

Try this
if (condition)
then
if (condition) stmnt
else stmnt
else
if (condition) stmnt
else stmnt

Related

passing a result set from a user defined function into the max() function

first time poster long time browser to bear with me if I'm not clear. I'm quite new to xslt.
I'm trying to write a function which passes a list of cleansed date values to the max() function. Following is my input document:
<dates>
<date>1990-09-02Z</date>
<date>1990-09-03Z</date>
<date>1990-09-04Z</date>
<date>1990-09-05Z</date>
<date>1990-09-06Z</date>
</dates>
As you can see, the string values have a trailing 'Z'. If I try to pass these directly to max() using a nested substring() function
<xsl:template match="/dates">
<xsl:value-of select="max(xs:date(substring(//date,1,10)))"/>
</xsl:template>
I get this error:
A sequence of more than one item is not allowed as the first argument of fn:substring() ("1990-09-02Z", "1990-09-03Z")
so I've included an xsl:function declaration into my stylesheet which now looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:test="http://www.blah.blah/funct"
version="3.0">
<xsl:function name="test:funct" visibility="public">
<xsl:param name="input"/>
<xsl:sequence>
<xsl:for-each select="$input">
<xsl:value-of select="xs:date(substring(.,1,10))"/>
</xsl:for-each>
</xsl:sequence>
</xsl:function>
<xsl:template match="/dates">
<xsl:value-of select="max(test:funct(//date))"/>
</xsl:template>
</xsl:stylesheet>
However, now I'm getting the following error
Failure converting {1990-09-02} to a number
I thought max() could handle dates? I'm quite confused about what's being passed into the max() function and why it's not working. the output I'm looking for is 1990-09-06
I try to read the w3org specification docs but the terms are too technical for me so not making sense of it. Appreciate any help you can offer.
By the way, processing engine I'm using is Saxon-PE 9.8.0.12
edit: my ultimate goal is to have a stylesheet with a list of functions which I can include within other xsl stylesheets, so ultimately the solution has to be a function. In this specific case a function which produces a list of cleansed dates which can then be passed to max().
As you have tagged that as XSLT 3, I would suggest to start with basic XPath 2/3 where you can simply write
//date/xs:date(substring(., 1, 10))
i.e. you can use function calls in the last step of your path to extract the substring and construct an xs:date: https://xsltfiddle.liberty-development.net/6rexjii
So that expression //date/xs:date(substring(., 1, 10)) gives you a sequence of xs:date values, you can then use the max function on them:
max(//date/xs:date(substring(., 1, 10)))
https://xsltfiddle.liberty-development.net/6rexjii/1
As for writing a user-defined function to have that last step done, I would write a function where the input is an xs:string and which returns an xs:date:
<xsl:function name="mf:date" as="xs:date">
<xsl:param name="input" as="xs:string"/>
<xsl:sequence select="xs:date(substring($input, 1, 10))"/>
</xsl:function>
Then you can call it as max(//date/mf:date(.)): https://xsltfiddle.liberty-development.net/6rexjii/2
If you really wanted to write a function to process a sequence of input items to return a sequence of xs:dates then use
<xsl:function name="mf:dates" as="xs:date*">
<xsl:param name="input" as="xs:string*"/>
<xsl:sequence select="$input ! xs:date(substring(., 1, 10))"/>
</xsl:function>
and call it with
<xsl:value-of select="max(mf:dates(//date))"/>
https://xsltfiddle.liberty-development.net/6rexjii/3
As a syntax alternative, in XPath 3.1 you can use the arrow operator =>:
<xsl:value-of select="//date => mf:dates() => max()"/>
https://xsltfiddle.liberty-development.net/6rexjii/4

XPath: Get text that contains Obama but not Romney

I am quite new to XPath so bear with me. I have a XPath expression
'.//*[contains(.,"Obama")]/text()'
that gets me the text that contains "Obama". However, I haven't been able to figure out how to add
and [not(contains(., "Romney"))] to the expression without getting a syntax error. How is it done? Help much appriciated!
Use:
.//*[contains(.,"Obama") and not(contains(.,"Romney"))]/text()
XSLT - based verification:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:copy-of select=
'.//*[contains(.,"Obama") and not(contains(.,"Romney"))]/text()'/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following XML document:
<election>
<choice>Maybe Obama</choice>
<choice>Maybe Romney</choice>
</election>
the XPath expression is evaluated and the selected node is copied to the output:
Maybe Obama
Do note:
SomeExpression[x][y]
is not always equivalent to:
SomeExpression[x and y]
Therefore, it is recommended the latter -- not the former, as specified in the answer by #ChrisGerken.
Here is a concrete example:
Let's have this XML document:
<nums>
<num>01</num>
<num>02</num>
<num>03</num>
<num>04</num>
<num>05</num>
<num>06</num>
<num>07</num>
<num>08</num>
<num>09</num>
<num>10</num>
</nums>
and these two XPath expressions:
/*/*[. mod 3 = 0 and position() = 3]
and
/*/*[. mod 3 = 0][position() = 3]
The first expression selects:
<num>03</num>
However, the second expression selects:
<num>09</num>
And here is a complete XSLT - based verification:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:copy-of select=
"/*/*[. mod 3 = 0 and position() = 3]"/>
================
<xsl:copy-of select=
"/*/*[. mod 3 = 0][position() = 3]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the above XML document, the two XPath expressions are evaluated and the results of these evaluations are copied to the output:
<num>03</num>
================
<num>09</num>
Explanation:
position() is a *context-sensitive` function and typically produces different results when used in the k-th and in the m-th predicate, where k != m
try this:
'.//*[contains(.,"Obama")][not(contains(.,"Romney"))]/text()'
You can put as many predicates as you like one after another:
[a][b][c]

Xpath matches with single quotes?

How can I assert an xpath match that contains single quotes within the string to be asserted?
This is my string with value '40' to be asserted.
I assumed to escape the single quote characters with \' but that does not work.
matches( //faultstring[1]/text(), 'This is my string with value \'40\' to be asserted.' )
How is this done properly?
Try this
//faultstring[matches(text(),''')]
or
//faultstring[matches(text(),'&apos;')]
or
//faultstring[matches(text(),''')]
For a more elegant solution see this post
My understanding of this question is that the problem is caused by the need to use nested quotes if the XPath expression is within an XML document.
If this is the case, one can use this XPath expression:
$yourString = "This is my string with value &apos;40&apos; to be asserted."
XSLT - based verification:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:copy-of select=
"/* = "This is my string with value &apos;40&apos; to be asserted.""/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following XML document:
<t>This is my string with value '40' to be asserted.</t>
the XPath expression is evaluated and the result of this evaluation is copied to the output:
true

Return a string value based on XPATH condition

If I have the below XML, how to specify a xpath to return a string based on a condition. For example here if //b[#id=23] then "Profit" else "Loss"
<a>
<b id="23"/>
<c></c>
<d></d>
<e>
<f id="23">
<i>123</i>
<j>234</j>
<f>
<f id="24">
<i>345</i>
<j>456</j>
<f>
<f id="25">
<i>678</i>
<j>567</j>
<f>
</e>
</a>
I. XPath 2.0 solution (recommended if you have access to an XPath 2.0 engine)
(: XPath 2.0 has if ... then ... else ... :)
if(//b[#id=23])
then 'Profit'
else 'Loss'
II. XPath 1.0 solution:
Use:
concat(substring('Profit', 1 div boolean(//b[#id=23])),
substring('Loss', 1 div not(//b[#id=23]))
)
Verification using XSLT 1.0:
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:value-of select=
"concat(substring('Profit', 1 div boolean(//b[#id=23])),
substring('Loss', 1 div not(//b[#id=23]))
)"/>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document (corrected to make it well-formed):
<a>
<b id="23"/>
<c></c>
<d></d>
<e>
<f id="23">
<i>123</i>
<j>234</j>
</f>
<f id="24">
<i>345</i>
<j>456</j>
</f>
<f id="25">
<i>678</i>
<j>567</j>
</f>
</e>
</a>
produces the wanted, correct result:
Profit
When we replace in the XML document:
<b id="23"/>
with:
<b id="24"/>
again the correct result is produced:
Loss
Explanation:
We use the fact that:
substring($someString, $N)
is the empty string for all $N > string-length($someString).
Also, the number Infinity is the only number greater than the string-length of any string.
Finally:
number(true()) is 1 by definition,
number(false()) is 0 by definition.
Therefore:
1 div $someCondition
is 1 exactly when the $someCondition is true()
and is Infinity exactly when $someCondition is false()
Thus it follows from this that if we want to produce $stringX when $Cond is true() and to produce $stringY when $Cond is false(), one way to express this is by:
concat(substring($stringX, 1 div $cond),
substring($stringY, 1 div not($cond)),
)
In the above expression exactly one of the two arguments of the concat() function is non-empty.
You can't; you'd have to use XQuery for this. see e.g. XQuery Conditional Expressions
Or, if the resulting string is only used within Java, you can just process the value returned by XPath within your Java code:
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr = xpath.compile("//b[#id=23]");
boolean result = expr.evaluate(doc, XPathConstants.BOOLEAN);
if (result) return "Profit";
else return "Loss";

Is there an "if -then - else " statement in XPath?

It seems with all the rich amount of function in xpath that you could do an "if" . However , my engine keeps insisting "there is no such function" , and I hardly find any documentation on the web (I found some dubious sources , but the syntax they had didn't work)
I need to remove ':' from the end of a string (if exist), so I wanted to do this:
if (fn:ends-with(//div [#id='head']/text(),': '))
then (fn:substring-before(//div [#id='head']/text(),': ') )
else (//div [#id='head']/text())
Any advice?
Yes, there is a way to do it in XPath 1.0:
concat(
substring($s1, 1, number($condition) * string-length($s1)),
substring($s2, 1, number(not($condition)) * string-length($s2))
)
This relies on the concatenation of two mutually exclusive strings, the first one being empty if the condition is false (0 * string-length(...)), the second one being empty if the condition is true. This is called "Becker's method", attributed to Oliver Becker (original link is now dead, the web archive has a copy).
In your case:
concat(
substring(
substring-before(//div[#id='head']/text(), ': '),
1,
number(
ends-with(//div[#id='head']/text(), ': ')
)
* string-length(substring-before(//div [#id='head']/text(), ': '))
),
substring(
//div[#id='head']/text(),
1,
number(not(
ends-with(//div[#id='head']/text(), ': ')
))
* string-length(//div[#id='head']/text())
)
)
Though I would try to get rid of all the "//" before.
Also, there is the possibility that //div[#id='head'] returns more than one node.
Just be aware of that — using //div[#id='head'][1] is more defensive.
The official language specification for XPath 2.0 on W3.org details that the language does indeed support if statements. See Section 3.8 Conditional Expressions, in particular. Along with the syntax format and explanation, it gives the following example:
if ($widget1/unit-cost < $widget2/unit-cost)
then $widget1
else $widget2
This would suggest that you shouldn't have brackets surrounding your expressions (otherwise the syntax looks correct). I'm not wholly confident, but it's surely worth a try. So you'll want to change your query to look like this:
if (fn:ends-with(//div [#id='head']/text(),': '))
then fn:substring-before(//div [#id='head']/text(),': ')
else //div [#id='head']/text()
I do strongly suspect this may fix it however, as the fact that your XPath engine seems to be trying to interpret if as a function, where it is in fact a special construct of the language.
Finally, to point out the obvious, insure that your XPath engine does in fact support XPath 2.0 (as opposed to an earlier version)! I don't believe conditional expressions are part of previous versions of XPath.
How about using fn:replace(string,pattern,replace) instead?
XPATH is very often used in XSLTs and if you are in that situation and does not have XPATH 2.0 you could use:
<xsl:choose>
<xsl:when test="condition1">
condition1-statements
</xsl:when>
<xsl:when test="condition2">
condition2-statements
</xsl:when>
<xsl:otherwise>
otherwise-statements
</xsl:otherwise>
</xsl:choose>
according to pkarat's, law you can achieve conditional XPath in version 1.0.
For your case, follow the concept:
concat(substring-before(your-xpath[contains(.,':')],':'),your-xpath[not(contains(.,':'))])
This will definitely work. See how it works. Give two inputs
praba:
karan
For 1st input: it contains : so condition true, string before : will be the output, say praba is your output. 2nd condition will be false so no problems.
For 2nd input: it does not contain : so condition fails, coming to 2nd condition the string doesn't contain : so condition true... therefore output karan will be thrown.
Finally your output would be praba,karan.
Personally, I would use XSLT to transform the XML and remove the trailing colons. For example, suppose I have this input:
<?xml version="1.0" encoding="UTF-8"?>
<Document>
<Paragraph>This paragraph ends in a period.</Paragraph>
<Paragraph>This one ends in a colon:</Paragraph>
<Paragraph>This one has a : in the middle.</Paragraph>
</Document>
If I wanted to strip out trailing colons in my paragraphs, I would use this XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
version="2.0">
<!-- identity -->
<xsl:template match="/|#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- strip out colons at the end of paragraphs -->
<xsl:template match="Paragraph">
<xsl:choose>
<!-- if it ends with a : -->
<xsl:when test="fn:ends-with(.,':')">
<xsl:copy>
<!-- copy everything but the last character -->
<xsl:value-of select="substring(., 1, string-length(.)-1)"></xsl:value-of>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Unfortunately the previous answers were no option for me so i researched for a while and found this solution:
http://blog.alessio.marchetti.name/post/2011/02/12/the-Oliver-Becker-s-XPath-method
I use it to output text if a certain Node exists. 4 is the length of the text foo. So i guess a more elegant solution would be the use of a variable.
substring('foo',number(not(normalize-space(/elements/the/element/)))*4)
Somewhat simpler XPath 1.0 solution, adapted from Tomalek's (posted here) and Dimitre's (here):
concat(substring($s1, 1 div number($cond)), substring($s2, 1 div number(not($cond))))
Note: I found an explicit number() was required to convert the bool to an int otherwise some XPath evaluators threw a type mismatch error. Depending on how strict your XPath processor is type-matching you may not need it.

Resources