Issues while performing Arithmetic Operations on Strings using XSLT 2.0 or 3.0 - xslt-2.0

I've been having issues while doing arithmetic operations on following XML
Source XML
<?xml version="1.0" encoding="UTF-8"?>
<Compensation>
<Salary>
<BasePay>$18600.12</BasePay>
<Bonus>$3500.99</Bonus>
<Gym>$670</Gym>
<Tax>$30,000</Tax>
</Salary>
<Salary>
<BasePay>$28600.12</BasePay>
<Bonus>$1500.99</Bonus>
<Gym/>
<Tax>$50,000</Tax>
</Salary>
</Compensation>
Current XSLT
<?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:this="urn:this-stylesheet"
exclude-result-prefixes="xs this"
version="2.0">
<xsl:output method="xml" indent="yes"/>
<xsl:function name = "this:translateCurrency">
<xsl:param name="stringValue"/>
<xsl:value-of select="format-number(xs:decimal(translate(xs:string($stringValue), '$,','')), '#.##')"/>
</xsl:function>
<xsl:template match="Compensation">
<Worker>
<xsl:for-each select="Salary">
<Comp>
<Amount>
<xsl:value-of select="this:translateCurrency(BasePay) - this:translateCurrency(Tax) "/>
</Amount>
<NoBonus>
<xsl:value-of select="this:translateCurrency(BasePay) + this:translateCurrency(Gym) "/>
</NoBonus>
</Comp>
</xsl:for-each>
</Worker>
</xsl:template>
</xsl:stylesheet>
Currency symbol and commas will always be present in amount related XML elements such as <BasePay> <Bonus> <Gym> <Tax> which i am translating and converting to decimal before adding or substracting.
There are two issues
1. Since my source XML have many Amount related fields, I have declared a function for translating and converting to decimal. However, I'm unable to get my function rounding to two decimal points. I was expecting following line of code in my function will be able to round to two decimal points.
<xsl:value-of select="format-number(xs:decimal(translate(xs:string($stringValue), '$,','')), '#.##')"/>
2. It's possible that some of the amount fields may be null for e.g. <Gym/> is null in my Source XML and current version of XSLT returns Cannot convert to string "" to xs:decimal no digits in value.
I tried $stringValue!='' in xsl:function statement and Gym!='' but to no avail.
Can anyone help me figure out what i should be doing to get my function round to two decimal points and get past no digits in value error?
<NoBonus>
<xsl:value-of select="this:translateCurrency(BasePay) + this:translateCurrency(Gym!='') "/>
</NoBonus>
Expected Result
<?xml version="1.0" encoding="UTF-8"?>
<Worker>
<Comp>
<Amount>-11399.88</Amount>
<NoBonus>19270.12</NoBonus>
</Comp>
<Comp>
<Amount>-21399.88</Amount>
<NoBonus>28600.12</NoBonus>
</Comp>
</Worker>

If you want to convert a string to a decimal value then don't use format-number on it. So for your input values to be converted into xs:decimals you need e.g.
<xsl:function name="this:translateCurrency" as="xs:decimal">
<xsl:param name="input" as="xs:string"/>
<xsl:sequence
select="if ($input = '')
then 0
else xs:decimal(translate($input, '$,', ''))"/>
</xsl:function>
Then use those xs:decimal values in any arithmetic computations, only where you need to output the final result of an arithmetic computation in a certain format use format-number on that result to ensure e.g. you get two decimals.

Related

split xml file with multiple value using xslt

trying to split the xml file having multiple elements into separate xml having different elements.
Input File:
<Person>
<firstname>ABC</firstname>
<lastname>ABC</lastname>
<address>address1</address>
<address>address2</address>
<city>city</city>
<state>state</state>
<currency>currency1</currency>
<currency>currency2</currency>
</Person>
Need to split above file into two files as
Output file-1
<Person>
<firstname>ABC</firstname>
<lastname>ABC</lastname>
<address>address1</address>
<city>city</city>
<state>state</state>
<currency>currency1</currency>
</Person>
Output file -2
<Person>
<firstname>ABC</firstname>
<lastname>ABC</lastname>
<address>address2</address>
<city>city</city>
<state>state</state>
<currency>currency2</currency>
</Person>
Here's a generic solution that outputs N files where N is the maximum number of same-name children elements, where file N contains the Nth instance of each element name if there are at least N, or the first one otherwise:
<xsl:template match="/*">
<xsl:variable name="this" select="."/>
<xsl:variable name="names" select="distinct-values(*/name())"/>
<xsl:for-each select="1 to max(
for $name in $names return count(*[name()=$name]))"/>
<xsl:variable name="n" select="."/>
<xsl:result-document href="file{.}">
<xsl:element name="{name($this)}">
<xsl:for-each-group select="$this/*" group-by="name()">
<xsl:copy-of select="(current-group()[$n], .)[1]"/>
</xsl:for-each-group>
</xsl:element>
</xsl:result-document>
</xsl:for-each>
</xsl:template>
This should do what you want with the example input you have shown, but whether it does the right thing with any other input is anyone's guess, because you have under-specified the requirements.

How to break the input xml into token and read the token values

Below is the input to the xslt
<docValues>
01|1596056|CCCCCCCCCDD|028571|ABCCHAS|29150699|150800|FFSSSSFFFF|005| |N|N|002| | |0000020319|29150699|163000|29150699|153100|666666|20140627|400|RRRRR|400| |20150701
02|1596056|028571|29150699|0001|400| | |0001|THIS IS MY SERVICE,,| | | |0901.99| |0.5|
03|1596056|028571|29150699|0001|5103|29150699|29150699| |1.000|99.098| |
<docValues>
Below are the details of xml input
01 : First Line Number
02 : Second Line Number
03 : Third Line Number
XSLT should read input xml and outout below xml
<SO>
<line01_2nd_token>1596056</line01_2nd_token>
<line01_4th_token>028571</line01_4th_token>
<line01_5th_token>ABCCHAS</line01_4th_token>
<SO>
<PARIS>
<line01_4th_token>028571</line01_4th_token>
<line02_5th_token>0001</line02_5th_token>
<line03_11th_token>99.098</line03_11th_token>
</PARIS>
<MY_SERVICE>
<line01_4th_token>028571</line01_4th_token>
<line03_5th_token>0001</line02_5th_token>
<line02_6th_token>400</line02_6th_token>
</MY_SERVICE>
To achieve above output : the lines(01,02,03) in input xml needs to be break into tokens with | as delimiter, so that i can read the desired token value in the respective lines (01,02,03)
So here my question is how to break the input xml into token and read the token values.
Is there any way to achieve the desired output
Please help me to solve this problem
Well, there is a function tokenize you could use twice, first with the second argument '\r?\n' to find the lines, then you can tokenize each line on '\|'.
Here is an example extracting some tokens, you should be able to complete that on your own:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output indent="yes"/>
<xsl:template match="docValues">
<xsl:variable name="lines" select="tokenize(., '\r?\n')[normalize-space()]"/>
<xsl:variable name="line1-tokens" select="tokenize($lines[1], '\|')"/>
<SO>
<line01_2nd_token><xsl:value-of select="$line1-tokens[2]"/></line01_2nd_token>
</SO>
<PARIS>
<line01_4th_token><xsl:value-of select="$line1-tokens[4]"/></line01_4th_token>
</PARIS>
<MY_SERVICE>
<line01_4th_token><xsl:value-of select="$line1-tokens[4]"/></line01_4th_token>
</MY_SERVICE>
</xsl:template>
</xsl:stylesheet>

For each in xslt

##I need to iterate over commas to break the characters between comma in xslt. There can be at the max 15 words separated by comma##
For example
`Input
<root>
<child>A,B,C,D</child>
</root>
Output
<root>
<List>A</List>
<List>B</List>
<List>C</List>
<List>D</List> `
You may use fn:tokenize to accomplish this. It seperates a string by a delimiter and returns the individual letters without the delimiter.
fn:tokenize("abracadabra", "(ab)|(a)") returns ("", "r", "c", "d", "r", "")
For further reference: http://www.w3.org/TR/xpath-functions/#func-tokenize
Are you sure that you're using xslt 2.0, because fn:tokenize should work:
(Don't use the 'fn' namespace, this is the default namespace in xpath build in functions)
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="/root">
<output>
<xsl:for-each select="tokenize(child, ',')">
<child><xsl:value-of select="."/></child>
</xsl:for-each>
</output>
</xsl:template>
</xsl:transform>
Example:
http://xsltransform.net/eiQZDbf

Why does index-of() return multiple values when applied to a sequence of unique nodes?

I'm using xpath2's index-of value to return the index of current() within a sorted sequence of nodes. Using SAXON, the sorted sequence of nodes are unique, yet index-of returns a sequence of two values.
This does not happen all the time, just very occasionally, but not for any reason I can find. Can someone please explain what is going on?
I have worked up a minimal example based on an example of data that routines gives this odd behavior.
The source data is:
<data>
<student userID="1" userName="user1"/>
<session startedOn="01/16/2012 15:01:18">
</session>
<session startedOn="11/16/2011 13:31:33">
</session>
</data>
My xsl document puts the session nodes into a sorted sequence $orderd at the very top of the root template:
<xsl:template match="/">
<xsl:variable name="nodes" as="node()*" select="/data/session"></xsl:variable>
<xsl:variable name="orderd" as="node()*">
<xsl:for-each select="$nodes">
<xsl:sort select="xs:dateTime(xs:dateTime(concat(substring(normalize-space(#startedOn),7,4),'-',substring(normalize-space(#startedOn),1,2),'-',substring(normalize-space(#startedOn),4,2),'T',substring(normalize-space(#startedOn),12,8)))
)" order="ascending"/>
<xsl:sequence select="."/>
</xsl:for-each>
</xsl:variable>
Since the nodes were already ordered by #startOn but in the opposite order, the sequence $orderd should be the same as document-ordered sequence $nodes, except in reverse order.
When I create output using a for-each statement, I find that somehow the two nodes are seen as identical when tested using index-of.
The code below is used to output data (and comes immediately after the chunk above):
<output>
<xsl:for-each select="$nodes">
<xsl:sort select="position()" order="descending"></xsl:sort>
<xsl:variable name="index" select="index-of($orderd,current())" as="xs:integer*"></xsl:variable>
<xsl:variable name="pos" select="position()"></xsl:variable>
<session reverse-documentOrder="{$pos}" sortedOrder="{$index}"/>
</xsl:for-each>
</output>
As the output (shown below) indicates, the index-of function is returning the sequence (1,2), meaning that it sees both nodes as identical. I have checked the expression used to sort the values, and it produces distinct and well-formed date-Time strings.
<output>
<session reverse=documentOrder="1"
sortedOrder="1 2"/>
<session reverse-documentOrder="2"
sortedOrder="1 2"/>
</output>
Not relying on the generate-id() function, which is XSLT function, but not XPath function, one can write a simple index-of() function that operates on node identity:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vNum3" select="/*/*[3]"/>
<xsl:variable name="vSeq" select="/*/*[1], /*/*[3], /*/*[3]"/>
<xsl:template match="/">
<xsl:sequence select="my:index-of($vSeq, $vNum3)"/>
</xsl:template>
<xsl:function name="my:index-of" as="xs:integer*">
<xsl:param name="pSeq" as="node()*"/>
<xsl:param name="pNode" as="node()"/>
<xsl:for-each select="$pSeq">
<xsl:if test=". is $pNode">
<xsl:sequence select="position()"/>
</xsl:if>
</xsl:for-each>
</xsl:function>
</xsl:stylesheet>
when this transformation is applied on the following 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>
the wanted, correct result is returned:
2 3
Explanation: Use of the is operator.
The documentation http://www.w3.org/TR/xpath-functions/#func-index-of of index-of says "The items in the sequence $seqParam are compared with $srchParam under the rules for the eq operator. Values of type xs:untypedAtomic are compared as if they were of type xs:string.". So you are trying to compare untyped element nodes and that means they are compared as strings and both session elements have the same white space only string contents. That way both are compared as equal.
I am not sure what to suggest as I am not sure what you want to achieve but I hope the above explains the result you get.

XSLT 2.0 - looping over nodeset variable, but need to process other elements in loop as well

I have a XML like this:
<?xml version="1.0" encoding="UTF-8"?>
<nodes>
<n c="value2"/>
<n>Has a relation to node with value2</n>
<n>Has a relation to node with value2</n>
<n c="value"/>
<n>Has a relation to node with value</n>
<n c="value1"/>
<n>Has a relation to node with value1</n>
</nodes>
I sort all elements which have attributes in variable, then I iterate over this variable in for-each loop. But at the end of each loop, I need to print value of those elements which are below the currently processed element(in original XML) and have no atrribute.
That means: call apply-templates on <n> without attribute, but the "select" attr. in apply-templates does not work, probably because I´m now in variable loop.
Is there a solution for that?
Thanks
Here is the XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="nodes">
<xsl:variable name="sorted">
<xsl:for-each select="n[#c]">
<xsl:sort select="#c"></xsl:sort>
<xsl:copy-of select="."></xsl:copy-of>
</xsl:for-each>
</xsl:variable>
<xsl:for-each select="$sorted/n">
<xsl:value-of select="#c"></xsl:value-of>
<xsl:apply-templates select="/nodes/n[2]"></xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="n[not(#c)]">
<xsl:value-of select="."></xsl:value-of>
</xsl:template>
</xsl:stylesheet>
This is just example,all this is a part of bigger project:)
Desired output with a more complicated XPAth(now even the simple one does not work) is:
Value
Has a relation to node with value
Value1
Has a relation to node with value1
Value2
Has a relation to node with value2
Has a relation to node with value2
Is it a bit clearer now?
Some thoughts: apply-templates without a select processes the child node of the current context node; in your input sample the n elements do not have any children at all. Furthermore in your variable you do a copy-of meaning you create new nodes that have no relation to the nodes in the input sample. So while I am not sure what you want to achieve your construction with apply-templates inside the for-each does not make sense, given the input sample you have posted and the variable you use.
I suspect you could use the XSLT 2.0 for-each-group group-starting-with as in
<xsl:template match="nodes">
<xsl:for-each-group select="n" group-starting-with="n[#c]">
<xsl:sort select="#c"/>
<xsl:value-of select="#c"/>
<xsl:apply-templates select="current-group() except ."/>
</xsl:for-each-group>
</xsl:template>
If that does not help then consider to post a small input sample with sample data and the corresponding output sample you want to create with XSLT 2.0, then we can make suggestions on how to achieve that.
[edit] Now that you have posted an output sample I post an enhanced version of my previous suggestion:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="text"/>
<xsl:template match="nodes">
<xsl:for-each-group select="n" group-starting-with="n[#c]">
<xsl:sort select="#c"/>
<xsl:value-of select="#c"/>
<xsl:text>
</xsl:text>
<xsl:apply-templates select="current-group() except ."/>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="n[not(#c)]">
<xsl:value-of select="."/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
When I use Saxon 9.3 and run the stylesheet against your latest input sample the result is as follows:
value
Has a relation to node with value
value1
Has a relation to node with value1
value2
Has a relation to node with value2
Has a relation to node with value2
That is what you asked for I think so try that approach with your more complex real input.

Resources