Breaking for-each loop on a conditional base - for-loop

I'm new to xslt 2.0, I would like to set the value to a variable in for-each loop only once (means if the value set, I want to come out of the loop).
For now it keep iterating for all the users. I just want to come out of the loop once the value set (immediately after my first attemp). I'm not sure how to break if the value set once.
Can you please help me on the below code ?
XSLT Code:
<xsl:variable name="v_first_name">
<xsl:for-each select="$emailList/emails/child::*">
<xsl:variable name="mailid" select="id" />
<xsl:for-each select="$userList/users/child::*">
<xsl:if test="emailid = $mailid">
<xsl:if test="firstname eq 'Antony'">
<xsl:value-of select="firstname" />
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:variable>
<xsl:if test="$v_first_name != ''">
<first_name>
<xsl:value-of select="$v_first_name" />
</first_name>
</xsl:if>
XML O/p:
<first_name>AntonyAntonyAntonyAntony</first_name>
Expected XML O/P:
<first_name>Antony</first_name>
Note1: Please note that I'm using xslt 2.0 and my lists can have duplicates (So Antony can come twice, but I want only once (or unique)).
Note2: I also tried with position(), but couldn't find it work as the condition () can match at any position.
Thanks in advance.

Start with XPath and simply select the nodes you are looking for instead of considering for-each a "loop". If you select e.g. $userList/users/*[emailid = $emailList/emails/*/id] you select child elements from users which have a matching emailid in $emailList/emails/*.
I am not sure which sense it makes to hard code a first name value and then output it but of course you can select e.g. $userList/users/*[emailid = $emailList/emails/*/id and firstname = 'Antony']/lastname. That gives you a sequence of element nodes, if you want the first use a positional predicate e.g. depending on the structure of your input $userList/users/*[emailid = $emailList/emails/*/id and firstname = 'Antony'][1]/lastname or, of all selected elements ($userList/users/*[emailid = $emailList/emails/*/id and firstname = 'Antony']/lastname)[1].

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

using preceding-sibling with with xsl:sort

I'm trying to use preceding-sibling and following-sibling with a subset of records with a sort on them. The problem that the preceding / following brings back values from the original xml order:
<Salaries>
<Salary>
<Base>1000</Base>
<CreatedDate xmlns:d7p1="http://schemas.datacontract.org/2004/07/System">
<d7p1:DateTime>2016-01-09T14:38:54.8440764Z</d7p1:DateTime>
<d7p1:OffsetMinutes>0</d7p1:OffsetMinutes>
</CreatedDate>
</Salary>
<Salary>
<Base>2000</Base>
<CreatedDate xmlns:d7p1="http://schemas.datacontract.org/2004/07/System">
<d7p1:DateTime>2015-01-09T14:38:54.8440764Z</d7p1:DateTime>
<d7p1:OffsetMinutes>0</d7p1:OffsetMinutes>
</CreatedDate>
</Salary>
<Salary>
<Base>3000</Base>
<CreatedDate xmlns:d7p1="http://schemas.datacontract.org/2004/07/System">
<d7p1:DateTime>2017-01-09T14:38:54.8440764Z</d7p1:DateTime>
<d7p1:OffsetMinutes>0</d7p1:OffsetMinutes>
</CreatedDate>
</Salary>
</Salaries>
When I use a sort under a for-each (Salaries/Salary) with a c# function to add offset minutes into a date and convert to a long number 201701010000 for example(to make manipulation in xslt easier).
<xsl:sort select="number(cs:Convertdatetolong(cs:AddOffsetMinutes(substring(p:CreatedDate/d5p1:DateTime,1,19),p:CreatedDate/d5p1:OffsetMinutes)))" order="ascending"/>
The sort works perfectly and I get the records out in the following order:
2000
1000
3000
The problem comes if I use preceding-sibling / preceding (and following). I would expect the first record (2000) to have no preceding record and the last record (3000) to have no following.
However when I use the preceding / following I get the previous record and the next record from the original XML:
2000 (preceding - 1000 / following - 3000)
1000 (preceding - / following - 2000)
3000 (preceding - 2000 / following - )
I would like to be able to compare against the previous record (in the sorted order) and the current record (in the sorted order):
2000 (preceding - / following - 1000)
1000 (preceding - 2000 / following 3000)
3000 (preceding - 1000 / following - )
I've tried preceding-sibling and preceding
<xsl:value-of select="preceding::p:Salary[1]/p:Base"/>
<xsl:value-of select="preceding-sibling::p:Salary[1]/p:Base"/>
<xsl:value-of select="preceding::p:Salary[position()=1]/p:Base"/>
(the salary is in a different namespace (p)
Is this actually possible or do I have to use variables to save the previous record's data to compare against?
Any ideas gratefully received. I'm using xslt 1.0
Although XSLT/XPath often talks of a "sequence of nodes", it's actually more accurate to think of it as a "sequence of node references" - because, for example, the same node can appear more than once in the sequence. When you sort a sequence of node references, you don't change the individual nodes in any way, you only change the sequence. That means the nodes still exist in their original tree exactly where they were before, and their parents, siblings, and descendants are exactly as they were before.
What you want is not the preceding and following siblings of the node, but the nodes that come before and after it in the sorted sequence, which is a quite different thing.
One way to do this is to construct a new tree containing copies of the original nodes, which you get, for example, if you do
<xsl:variable name="x">
<xsl:for-each ...>
<xsl:sort ...>
<xsl:copy-of select="."/>
The sibling relationships of the copied nodes will then reflect the sorted order. There's the minor problem that in XSLT 1.0, $x is a result tree fragment so you have to convert it to a node-set using the exslt:node-set() function.
In fact in XSLT 1.0 that's probably the only way of doing it, because the XSLT 1.0 data model only has node sets, not sequences, which means there is no way of capturing and processing a sequence of nodes in anything other than document order. The 2.0 model has much more flexibility and power. Upgrade if you can - XSLT 1.0 is approaching 20 years old.
Thanks to Michael for the answer. Posted here for completeness. Complicated because of the name spaces in use in the xml:
<!-- Puts the whole of the Salary Node into a variable-->
<xsl:variable name="SALARY" >
<xsl:copy-of select="p:Salaries" />
</xsl:variable>
<!-- Puts the the required key data into a node-set with the correct sort applied-->
<xsl:variable name="SAL">
<xsl:for-each select="msxsl:node-set($SALARY)//p:Salary">
<xsl:sort select="number(cs:Convertdatetolong(cs:AddOffsetMinutes(substring(p:CreatedDate/d5p1:DateTime,1,19),p:CreatedDate/d5p1:OffsetMinutes)))" order="ascending"/>
<xsl:copy-of select="." />
</xsl:for-each>
</xsl:variable>
<!-- Quick Output-->
<xsl:for-each select="msxsl:node-set($SAL)//p:Salary">
<xsl:text>Sa:</xsl:text>
<xsl:value-of select="position()" />
<xsl:text>Preceding:</xsl:text>
<xsl:value-of select="preceding-sibling::p:Salary[1]/p:Base"/>
<xsl:value-of select="$newline" />
<xsl:text>Current:</xsl:text>
<xsl:value-of select="p:Base"/>
<xsl:value-of select="$newline" />
<xsl:text>Following:</xsl:text>
<xsl:value-of select="following-sibling::p:Salary[1]/p:Base"/>
<xsl:value-of select="$newline"/>
</xsl:for-each>
The preceding-sibling axis gets the preceding siblings of the context node in document order.
To refer to the preceding siblings of a node after sorting, you will need to store the sorted nodes in a variable first - and, in XSLT 1.0, convert the variable into a node-set.

How do I select previous cousin in same position?

I have an xml document with a section roughly like this:
<a>
<b></b>
<b>Previous</b>
<b></b>
...
</a>
<a>
<b></b>
<b>Current</b>
<b></b>
...
</a>
...
I have the following section in my xsl:
<xsl:template match="/">
<xsl:apply-templates select="//a"/>
</xsl:template>
<xsl:template match="//a">
<xsl:for-each select="./b[text() != ???]">
...
</xsl:for-each>
</xsl:template>
I need the ??? to select the "c" element that is in the same position (within its parent "a" element) as the current "c" but in the previous "a" element (an example pair are marked as Current and Previous in the xml source).
Essentially this requires passing the position() of the for-each loop to its own select statement. This sounds kind of impossible - as the select statement is only evaluated once.
Now it would be possible to pass the position() from within the for-each loop and save it to a variable which could then be used in an if statement. However, the snag is I want to know when the last iteration has taken place. If I use an if statement then this breaks the meaning of last() - if the last iteration fails the if test.
Try this:
<xsl:for-each select="/b[ text() != ../../preceding-sibling::a/b[position(.)] ]" >
the preceding-sibling:: axis selects the node before the current step

XPATH: Ignore milli seconds from date

I am trying to extract using the following
<xsl:template match="//alarms:alarmRaisedTime">
<xsl:variable name="secondsSince1970" select="(xs:dateTime(.) - xs:dateTime('1970-01-01T00:00:00')) div xs:dayTimeDuration('PT1S')" />
<xsl:element name="alarmRaisedTime" namespace="MY NAME SPACE">
<xsl:value-of select="$secondsSince1970"/>
</xsl:element>
</xsl:template>
Output I get is
<alarmRaisedTime>1367855105.001</alarmRaisedTime>
I would like to get the below given output (Note Milli Seconds is removed)
<alarmRaisedTime>1367855105</alarmRaisedTime>
I tried the following:
<xsl:value-of select="fn:substring-before($secondsSince1970,.)"/>
but it did not work.
Even simpler:
floor($secondsSince1970)
By definition:
The floor function returns the largest (closest to positive infinity) number that is not greater than the argument and that is an integer.
Try
<xsl:value-of select="fn:substring-before($secondsSince1970,'.')"/>
The . in your expression refers to the context item alarms:alarmsRaisedTime

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