Sum two elements by grouping using XSLT 2.0 - xslt-2.0

I'm trying to sum two elements "amount" and "retroAmount" group by "tmid" using xslt 2.0 and I tried two methods, in method-1 everything is stacking up and in the method-2 it displays NaN. Any ideas about how this can be fixed?
Here is my XML file:
<?xml version="1.0" encoding="UTF-8"?>
<Request xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<row>
<tmid>abc</tmid>
<amount>651.03</amount>
<retroAmount>0</retroAmount>
</row>
<row>
<tmid>abc</tmid>
<amount>250.75</amount>
<retroAmount>-10</retroAmount>
</row>
<row>
<tmid>abc</tmid>
<amount>132</amount>
<retroAmount>-16.1</retroAmount>
</row>
<row>
<tmid>xyz</tmid>
<amount>129.19</amount>
<retroAmount>49.96</retroAmount>
</row>
<row>
<tmid>xyz</tmid>
<amount>148.76</amount>
<retroAmount>0</retroAmount>
</row>
<row>
<tmid>xyz</tmid>
<amount>92.29</amount>
<retroAmount>12</retroAmount>
</row>
</Request>
Output I am expecting:
<top xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Results>
<tmId>abc</tmId>
<total>1007.68</total>
</Results>
<Results>
<tmId>xyz</tmId>
<total>432.2</total>
</Results>
</top>
Any help is appreciated.
The XSLT code I was playing with:
Method-1 (everything is stacking up or being displayed without summing)
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="2.0">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<root>
<xsl:for-each-group select="Request/row"
group-by="tmid">
<row>
<tmid>
<xsl:value-of
select="current-grouping-key()"
/>
</tmid>
<xsl:for-each-group select="current-group()" group-by=".">
<amount>
<xsl:value-of select="sum(number(current-group()/amount))"/>
</amount>
<retroamount>
<xsl:value-of select="sum(number(current-group()/retroAmount))"/>
</retroamount>
</xsl:for-each-group>
</row>
</xsl:for-each-group>
</root>
</xsl:template>
</xsl:stylesheet>
Method-2 (I was only using "amount" and still it is displaying NaN, I would like to sum up both "amount" and "retroAmount"
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="2.0">
<xsl:output method="xml" omit-xml-declaration="yes" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/*">
<top>
<xsl:for-each-group select="//tmid" group-by=".">
<Results>
<tmId>
<xsl:sequence
select="current-grouping-key()"
/>
</tmId>
<total>
<xsl:sequence select="sum(number(current-group()/amount))"/>
</total>
</Results>
</xsl:for-each-group>
</top>
</xsl:template>
</xsl:stylesheet>

You basically want
<xsl:template match="Request">
<xsl:copy>
<xsl:for-each-group select="row" group-by="tmid">
<Results>
<tmId>{current-grouping-key()}</tmId>
<total>{sum(current-group()!(amount, retroAmount))}</total>
</Results>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
(that is XSLT 3 with XPath 3.1 syntax, but in XSLT 2 with XPath 2 syntax you would use
<xsl:template match="Request">
<xsl:copy>
<xsl:for-each-group select="row" group-by="tmid">
<Results>
<tmId>
<xsl:value-of select="current-grouping-key()"/>
</tmId>
<total>
<xsl:value-of select="sum(current-group()/(amount, retroAmount))"/>
</total>
</Results>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
I only later noticed that the Request element is meant to be transformed to a top element so change the <xsl:template match="Request"><xsl:copy>...</xsl:copy></xsl:template> from above suggestions to <xsl:template match="Request"><step>...</step></xsl:template>.

Related

Convert Date from DD-MMM-YYYY to YYYY-MMM-DD using xslt 2.0

I have a XML that has a date formatted as (2019-Aug-19) (YYYY-MMM-DD).
I want this to be transformed to (2019-08-19) (YYYY-MM-DD)
Hear is a sample what i have done. But no luck
<xsl:value-of select="format-dateTime(xs:dateTime(concat(date, T00:00:00Z')), '[D01]-[M01]-[Y0001]')"/>
Here you go may be another simple method:
XML:
<time>2019-Aug-19T00:00:00Z</time>
XSLT:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">
<xsl:output method="text" doctype-public="XSLT-compat" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />
<xsl:variable name="dt" select="time"/>
<xsl:template match="/">
<xsl:value-of select="format-dateTime(xs:dateTime(replace($dt, substring-before(substring-after($dt,'-'),'-') ,format-number(index-of(('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov', 'dec'),lower-case(substring-before(substring-after(lower-case($dt),'-'), '-'))),'00'))),'[D01]-[M01]-[Y0001]')"/>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:transform>
Result:
19-08-2019
For YYYY-MM-DD you can do:
<xsl:value-of select="format-dateTime(xs:dateTime(replace($dt, substring-before(substring-after($dt,'-'),'-') ,format-number(index-of(('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov', 'dec'),lower-case(substring-before(substring-after(lower-case($dt),'-'), '-'))),'00'))),'[Y0001]-[M01]-[D01]')"/>
Result:
2019-08-19
See Link: http://xsltransform.net/nbiCsZm

Unable to group the element value using group by in XSLT

I have an XML lie below:
<Products>
<Product1>
<Reference>000510143244</Reference>
<Value1>543</Value1>
</Product1>
</Products>
<Products>
<Product1>
<Reference>000510143244</Reference>
<Value1>543</Value1>
</Product1>
</Products>
<Products>
<Product1>
<Reference>45768799322</Reference>
<Value1>543</Value1>
</Product1>
</Products>
<Products>
<Product2>
<Reference>35726318090</Reference>
<Value1>543</Value1>
</Product2>
</Products>
<Products>
<Product2>
<Reference>35726318090</Reference>
<Value1>543</Value1>
</Product2>
</Products>
I want to get only first value of the Product1 reference...but I am unable to get that.Also it is not mandatory that Product 1 will always be the first element in input xml.
Any suggestions how can I get that?
I have tried to get the value as :
<xsl:template match="//Products">
<xsl:variable name="Product1">
<xsl:for-each-group select="/Reference" group-by="/Reference">
<xsl:copy-of select="." />
</xsl:for-each-group>
</xsl:variable>
</xsl:template>
Update:1
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:template match="Products[child::Product1][1]">
<xsl:value-of select="." />
</xsl:template>
<xsl:template match="text()" />
</xsl:stylesheet>
My expected output is :000510143244
To get the first occurrence of <Products> who has <Product1>, you might need to match the parent tag or root tag of your input XML.
Assuming your input as below:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<root>
<Products>
<Product2>
<Reference>35726318090</Reference>
</Product2>
</Products>
<Products>
<Product1>
<Reference>02563899183</Reference>
</Product1>
</Products>
<Products>
<Product1>
<Reference>000510143244</Reference>
</Product1>
</Products>
<Products>
<Product1>
<Reference>000510143244</Reference>
</Product1>
</Products>
<Products>
<Product2>
<Reference>35726318090</Reference>
</Product2>
</Products>
</root>
The following code can give you the result:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:template match="root">
<xsl:for-each-group select="Products/Product1" group-by="Reference">
<xsl:copy-of select="current-group()[1]" />
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
See the demo: https://xsltfiddle.liberty-development.net/3NJ38Zx
Update:
OR you can simply achieve it by following code:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:template match="Products[child::Product1][1]">
<xsl:copy-of select="." />
</xsl:template>
<xsl:template match="text()" />
</xsl:stylesheet>
Update 2:
<xsl:template match="root">
<xsl:variable name="ref">
<xsl:for-each-group select="Products/Product1" group-by="Reference">
<xsl:copy-of select="current-group()[1]/Reference" />
</xsl:for-each-group>
</xsl:variable>
<xsl:value-of select="$ref"/>
</xsl:template>
https://xsltfiddle.liberty-development.net/3NJ38Zx/1
Update 3:
You cannot assign a value to global variable from a template.
There are two ways to get what you required.
1) Create a global variable as below which will take first <Products> whose child element is <Product1> and will display it's Reference
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:variable name="ref" select="root/Products[child::Product1][1]/Product1/Reference" />
<xsl:template match="/">
<xsl:value-of select="$ref" />
</xsl:template>
</xsl:stylesheet>
2) You can modify the template as below to get the result.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:template match="Products[child::Product1][1]/Product1/Reference">
<xsl:value-of select="." />
</xsl:template>
<xsl:template match="text()" />
</xsl:stylesheet>

Change tag name while transformation using XSLT

I am working on XML transformation using XSLT and facing issue while renaming tag. Please find below detail for the same. My transformed XML should have BookName instead of Name and LibraryName instead of Name tag.
Input XML
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Catalog xmlns="http://example.com">
<Books>
<Book>
<Name>Wise Otherwise</Name>
<author>Great Expectations</author>
</Book>
<Book>
<Name>Rich Dad Poor Dad</Name>
<author>Orange</author>
</Book>
</Books>
<libraries>
<library>
<Name> Forsyth </Name>
<city> Cumming </city>
</library>
<library>
<Name> COBB </Name>
<city> Marietta </city>
</library>
</libraries>
</Catalog>
Expected XML After Transformation
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Catalog xmlns="http://example.com">
<Books>
<Book>
<BookName>Wise Otherwise</BookName>
<author>Great Expectations</author>
</Book>
<Book>
<Name>Rich Dad Poor Dad</Name>
<author>Orange</author>
</Book>
</Books>
<libraries>
<library>
<LibraryName> Forsyth </LibraryName>
<city> Cumming </city>
</library>
<library>
<LibraryName> COBB </LibraryName>
<city> Marietta </city>
</library>
</libraries>
</Catalog>
My XSLT for the same
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns="http://example.com">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="ns:Name">
<xsl:for-each select="Catalog/Books/Book/Name">
<BookName>
<xsl:apply-templates />
</BookName>
</xsl:for-each>
<xsl:for-each select="Catalog/libraries/library/Name">
<LibraryName>
<xsl:apply-templates />
</LibraryName>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
You can use this XSLT for reference:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns="http://example.com" xmlns="http://example.com" exclude-result-prefixes="ns">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="ns:Book/ns:Name">
<BookName>
<xsl:apply-templates/>
</BookName>
</xsl:template>
<xsl:template match="ns:library/ns:Name">
<LibraryName>
<xsl:apply-templates/>
</LibraryName>
</xsl:template>
</xsl:stylesheet>
I declared the namespace with and without prefix. Therefore all new created elements will belong to the default namespace. Also excluded the prefixed one since it is not used.
You can write several templates for matching the nodes you want to change. For example read this tutorial: http://www.xmlplease.com/xsltidentity

Change XML element name using XSLT

I am trying to change XML node name but it doesn't allow me to do so. In my below code I I have two templates 1. Change Node name 2.Create parent node for DocumentReference. Please see my XML and XSLT.
My XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<DataArea>
<PurchaseOrder>
<PurchaseOrderLine>
<DocumentReference>
<DocumentID>
<ID>23423</ID>
</DocumentID>
</DocumentReference>
<DocumentReference>
<DocumentID>
<ID>23424</ID>
</DocumentID>
</DocumentReference>
<Item>
<CustomerItemID>
<!-- ArtNr -->
<ID>444</ID>
</CustomerItemID>
</Item>
<Quantity unitCode="PCE">17.3</Quantity>
</PurchaseOrderLine>
</PurchaseOrder>
</DataArea>
Expected Result
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<DataArea>
<PurchaseOrder>
<POL>
<DocumentReference>
<DocumentID>
<ID>23423</ID>
</DocumentID>
</DocumentReference>
<DocumentReference>
<DocumentID>
<ID>23424</ID>
</DocumentID>
</DocumentReference>
<Item>
<CustomerItemID>
<!-- ArtNr -->
<ID>444</ID>
</CustomerItemID>
</Item>
<Quantity unitCode="PCE">17.3</Quantity>
</POL>
</PurchaseOrder>
</DataArea>
My XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="PurchaseOrderLine">
<POL>
<xsl:apply-templates />
</POL>
</xsl:template>
<xsl:template match="PurchaseOrderLine">
<xsl:copy>
<Kiran>
<xsl:apply-templates select="#*|DocumentReference"/>
</Kiran>
<xsl:apply-templates select="#*|Item|Quantity"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Then I think you want the template to look like
<xsl:template match="PurchaseOrderLine">
<POL>
<xsl:apply-templates select="#*"/>
<Kiran>
<xsl:apply-templates select="DocumentReference"/>
</Kiran>
<xsl:apply-templates select="node() except DocumentReference" />
</POL>
</xsl:template>

XSLT - make xsl:analyze-string return string instead of sequence of strings?

Is it possible to make xsl:analyze-string return one string instead of a sequence of strings?
Background: I'd like to use xsl:analyze-string in a xsl:function that should encapsulate the pattern matching. Ideally, the function should return an xs:string to be used as sort criteria in an xsl:sort element.
At the moment, i have to apply string-join() on every result of the function call since xsl:analyze-string returns a sequence of strings, and xsl:sort doesn't accept such a sequence as sort criteria. See line 24 of the stylesheet:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="www.my-personal-namespa.ce"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output indent="yes" method="xml" />
<xsl:function name="my:sortierung" >
<xsl:param name="inputstring" as="xs:string"/>
<xsl:analyze-string select="$inputstring" regex="[0-9]+">
<xsl:matching-substring>
<xsl:value-of select="format-number(number(.), '00000')" />
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select="." />
</xsl:non-matching-substring>
</xsl:analyze-string>
</xsl:function>
<xsl:template match="/input">
<result>
<xsl:apply-templates select="value" >
<xsl:sort select="string-join((my:sortierung(.)), ' ')" />
</xsl:apply-templates>
</result>
</xsl:template>
<xsl:template match="value">
<xsl:copy-of select="." />
</xsl:template>
</xsl:stylesheet>
with this input:
<?xml version="1.0" encoding="UTF-8"?>
<input>
<value>A 1 b 120</value>
<value>A 1 b 1</value>
<value>A 1 b 2</value>
<value>A 1 b 1a</value>
</input>
In my example, is there a way to modify the xsl:function to return a xs:string instead of a sequence?
There are several ways I think, you could put the result of the analyze-string into a variable inside of the function and then use xs:sequence select="string-join($var, ' ')" in the function.
However the following with xsl:value-of should also do:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="www.my-personal-namespa.ce"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="my xs">
<xsl:output indent="yes" method="xml" />
<xsl:function name="my:sortierung" as="xs:string">
<xsl:param name="inputstring" as="xs:string"/>
<xsl:value-of separator=" ">
<xsl:analyze-string select="$inputstring" regex="[0-9]+">
<xsl:matching-substring>
<xsl:value-of select="format-number(number(.), '00000')" />
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select="." />
</xsl:non-matching-substring>
</xsl:analyze-string>
</xsl:value-of>
</xsl:function>
<xsl:template match="/input">
<result>
<xsl:apply-templates select="value" >
<xsl:sort select="my:sortierung(.)" />
</xsl:apply-templates>
</result>
</xsl:template>
<xsl:template match="value">
<xsl:copy-of select="." />
</xsl:template>
</xsl:stylesheet>

Resources