Use XPath:Replace on first instance of a single character [duplicate] - xpath

This question already has an answer here:
Match first only instance of a character [duplicate]
(1 answer)
Closed 3 years ago.
I'm trying to replace only the first instance of a character in a string like fred-3-1-2 with XPath::replace and replace it with a / so that the resulting string is fred/3-1-2. I cannot guarantee anything else about the original string other than that it will have one or more dashes in it. I'm having a ton of difficulty finding a regex pattern that works with XPath::replace and consistently matches only that particular first instance of -.
I feel like I came close with:
(?:.+?)(-)(?:.+)
But this also matches the full string as well, so it is no good.
Please do not offer solutions using anything but plain regular expressions that would work on https://regex101.com. The "flavor" of regex should abide by XPath/XQuery semantics (https://www.w3.org/TR/xmlschema-2/#regexs).

You can use:
replace('fred-3-1-2', '^([^-]*)-','$1/')
Check it. Result:
fred/3-1-2
Meaning:
From the start, match any non - character followed by one -
character, replace the match by the first captured group plus /
character
Do note: XPath/XQuery follows Perl regexp. The description in XML Schema you cite is extended in XPath/Xquery with the followings: "Matching the Start and End of the String", "Reluctant Quantifiers", "Captured Sub-Expressions", "Back-References"

Pure XPath 1.0 solution -- no regular expressions:
concat(substring-before('fred-3-1-2', '-'), '/', substring-after('fred-3-1-2', '-'))
In case the string is contained in a variable $s and it isn't known that it contains a -, then use:
concat(
substring(concat(substring-before($s, '-'), '/', substring-after($s, '-')),
1 div contains($s, '-')
),
substring($s, 1 div not(contains($s, '-')))
)
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:variable name="s" select="'fred-3-1-2'"/>
<xsl:variable name="t" select="'fred+3+1+2'"/>
<xsl:template match="/">
<xsl:value-of select="concat(substring-before('fred-3-1-2', '-'), '/', substring-after('fred-3-1-2', '-'))"/>
=================
<xsl:value-of select=
"concat(
substring(concat(substring-before($s, '-'), '/', substring-after($s, '-')),
1 div contains($s, '-')
),
substring($s, 1 div not(contains($s, '-')))
)"/>
===============
<xsl:value-of select=
"concat(
substring(concat(substring-before($t, '-'), '/', substring-after($t, '-')),
1 div contains($t, '-')
),
substring($t, 1 div not(contains($t, '-')))
)"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on any XML file (not used), it evaluates the XPath expressions provided on this answer and produces the wanted, correct results: if the string contains - then its first (only) occurence in the string is replaced in the result of the evaluation; if the string doesn't contain any hyphens, then the result is the same string unchanged:
fred/3-1-2
=================
fred/3-1-2
===============
fred+3+1+2

Related

XPath: OR in the middle or in the end of xpath not working [duplicate]

I've got an XSLT template which counts topics at all levels, for use in labeling those topics with numbering in DITA projects I've got.
<xsl:template match="*[contains(#class, ' bookmap/chapter ')] | *[contains(#class, ' map/topicref ')] [not(ancestor-or-self::*[contains(#class,' bookmap/frontmatter ')])]" mode="topicTitleNumber">
<xsl:number format="1 " count="*[contains(#class, ' map/topicref ')] [not(ancestor-or-self::*[contains(#class,' bookmap/frontmatter ')])] | *[contains(#class, ' bookmap/chapter ')]" level="multiple"/>
</xsl:template>
I'm trying to add an additional exclusion to what gets counted though, for when a topicref class has a title element with an outputclass of noNum.
<xsl:template match="*[contains(#class, ' bookmap/chapter ')] | *[contains(#class, ' map/topicref ')] [not(ancestor-or-self::*[contains(#class,' bookmap/frontmatter ')])]" mode="topicTitleNumber">
<xsl:number format="1 " count="*[contains(#class, ' map/topicref ')] [not(ancestor-or-self::*[contains(#class,' bookmap/frontmatter ')] | *[contains(title/#outputclass, 'noNum')])] | *[contains(#class, ' bookmap/chapter ')]" level="multiple"/>
</xsl:template>
Shown above, I added | *[contains(title/#outputclass, 'noNum')] after the first not statement, thinking that would function as an additional condition in which the count call would skip over when the template was called (i.e. ...not an ancestor-or-self with [criteria] or a topic with title outputclass attribute of 'noNum'...). However it appears that the criteria I added is treated as something the template does match on and count.
Assuming that I am correct on the last point, I believe I need to put that condition inside of its own 'not' statement, but I'm not sure how to do that with the conditions already present in the XPath.
In XPath | is a set union operator, not a logical OR.
Union operator, |
XPath 1.0
The | operator computes the union of its operands, which must be node-sets.
XPath 2.0+
The union and | operators are equivalent. They take two node sequences as operands and return a sequence containing all the nodes that occur in either of the operands.
Logical OR, or
Use or instead for logical OR .
Complex XPath still not working?
Break it down to smaller parts:
Does //*[contains(#class, ' bookmap/chapter ')] select what you expect? Repeat for each of the most basic parts of your logical expression individually.
Combine those individually verified terms or predicates, one at a time, and observe that each step along the way, there are not surprises.
Repair any discrepancies between expected and actual results before combining additional terms.

Extract last word using Xpath 1.0

I need to select only the last word using xpath 1.0. I have something like this:
<Example>
<Ctry> Portugal PT </Ctry>
</Example>
I want to select only the PT word but the order is not exact, i.e: <Ctry> Portugal - Lisbon - PT </Ctry>, but the word i want to extract is always the last one.
I've already tried:
//*[name()='Example'][substring(., string-length(.) - string-length('PT')+1) = 'PT']/text() but extracts always the whole string.
Can anyone help me please?
You're selecting a node using the substring as a predicate to filter out other nodes. If you want the substring to be your output, it shouldn't go inside brackets.
substring(//*[name()='Example'], string-length(//*[name()='Example']) - string-length('PT')+1)
note that /text() can be ommited when working with string functions

How to join the results of two XPath expressions using the concat function?

I have following XML:
<root>
<chp id='1'>
<sent id='1'>hello</sent>
<sent id='2'>world</sent>
</chp>
<chp id='2'>
<sent id='1'>the</sent>
<sent id='2'>best</sent>
<sent id='3'>world</sent>
</chp>
</root>
Using the XPath expression
root/chp/sent[contains(.,'world')]/#id
I have the result 2 3 (which is exactly what I want), but when I run
concat('sentence ', /root/chp/sent[contains(.,'world')]/#id, ' - chap ' , /root/chp/sent[contains(.,'world')]/../#id )
the result breaks at the first result:
sentence 2 - chap 1
The last argument does not contain a single value, but a sequence. You cannot use XPath 1.0 to join this sequence to a single string. If you're able to use XPath 2.0, use string-join($sequence, $divisor):
concat(
'sentence ',
string-join(/root/chp/sent[contains(.,'world')]/#id, ' '),
' - chap ',
string-join(/root/chp/sent[contains(.,'world')]/../#id, ' ')
)
which will return
sentence 2 3 - chap 1 2
Most probably you want to loop over the result set yourself (also requires XPath 2.0):
for $sentence in /root/chp/sent[contains(.,'world')]
return concat('sentence ', $sentence/#id, ' - chap ', $sentence/../#id)
which will return
sentence 2 - chap 1
sentence 3 - chap 2
If you cannot use XPath 2.0, but are able to further process results in some outside programming language (like Java, PHP, ...) using DOM: Use /root/chp/sent[contains(.,'world')] to find the sentence nodes, loop over them, then query the #id and parent (chapter) #id using DOM and construct the result.

How to convert the int number into money format in X-path expression?

I want to convert the number (ie.1000) directly into into the money format like(ie.1,000).how should i do that?
In XSLT there is format-number(), but in pure XPath you'll have to do it the hard way. Perhaps you should do the formatting in the host language that you call XPath from?
You can use
<xsl:value-of select="format-number($wartosc, "###,###,###,###,##0.00")" />
You can use regex.
If $number is a string containing your integer number, then you can use:
replace(replace(
if (string-length($number) mod 3 eq 2) then concat("0", $number)
else if (string-length($number) mod 3 eq 1) then concat("00", $number)
else $number,
"([0-9]{3})", ",$1"),
"^[,0]+", "")

XPath test if node value is number

How can I check if a node value is a number using XPath?
Any ideas?
Test the value against NaN:
<xsl:if test="string(number(myNode)) != 'NaN'">
<!-- myNode is a number -->
</xsl:if>
This is a shorter version (thanks #Alejandro):
<xsl:if test="number(myNode) = myNode">
<!-- myNode is a number -->
</xsl:if>
The shortest possible way to test if the value contained in a variable $v can be used as a number is:
number($v) = number($v)
You only need to substitute the $v above with the expression whose value you want to test.
Explanation:
number($v) = number($v) is obviously true, if $v is a number, or a string that represents a number.
It is true also for a boolean value, because a number(true()) is 1 and number(false) is 0.
Whenever $v cannot be used as a number, then number($v) is NaN
and NaN is not equal to any other value, even to itself.
Thus, the above expression is true only for $v whose value can be used as a number, and false otherwise.
There's an amazing type test operator in XPath 2.0 you can use:
<xsl:if test="$number castable as xs:double">
<!-- implementation -->
</xsl:if>
I'm not trying to provide a yet another alternative solution, but a "meta view" to this problem.
Answers already provided by Oded and Dimitre Novatchev are correct but what people really might mean with phrase "value is a number" is, how would I say it, open to interpretation.
In a way it all comes to this bizarre sounding question: "how do you want to express your numeric values?"
XPath function number() processes numbers that have
possible leading or trailing whitespace
preceding sign character only on negative values
dot as an decimal separator (optional for integers)
all other characters from range [0-9]
Note that this doesn't include expressions for numerical values that
are expressed in exponential form (e.g. 12.3E45)
may contain sign character for positive values
have a distinction between positive and negative zero
include value for positive or negative infinity
These are not just made up criteria. An element with content that is according to schema a valid xs:float value might contain any of the above mentioned characteristics. Yet number() would return value NaN.
So answer to your question "How i can check with XPath if a node value is number?" is either "Use already mentioned solutions using number()" or "with a single XPath 1.0 expression, you can't". Think about the possible number formats you might encounter, and if needed, write some kind of logic for validation/number parsing. Within XSLT processing, this can be done with few suitable extra templates, for example.
PS. If you only care about non-zero numbers, the shortest test is
<xsl:if test="number(myNode)">
<!-- myNode is a non-zero number -->
</xsl:if>
The one I found very useful is the following:
<xsl:choose>
<xsl:when test="not(number(myNode))">
<!-- myNode is a not a number or empty(NaN) or zero -->
</xsl:when>
<xsl:otherwise>
<!-- myNode is a number (!= zero) -->
</xsl:otherwise>
</xsl:choose>
You could always use something like this:
string(//Sesscode) castable as xs:decimal
castable is documented by W3C here.
I've been dealing with 01 - which is a numeric.
string(number($v)) != string($v) makes the segregation

Resources