Generating a sequence that will sort in alphabetic order - xslt-2.0

Is there a built-in or simple way to generate a sequence of values that will sort in alphabetical order?
As an example, the following item elements have a child sequence element intended for sorting on. The data is being generated for another system that requires this information, since it does not work with the document ordering.
<list>
<item>
<sequence>1</sequence>
</item>
<item>
<sequence>2</sequence>
</item>
<item>
<sequence>3</sequence>
</item>
<item>
<sequence>4</sequence>
</item>
...
<item>
<sequence>14</sequence>
</item>
<item>
<sequence>15</sequence>
</item>
Instead of sorting numerically, the system sorts alphabetically and it cannot change this behavior. As a result the items are sorted as 1,10,11,12,13,14,15,2,3,4,5,6,7,8,9
Furthermore, the actual number of items is unbounded, so there needs to be general way of generating an alphabetic sequence that will sort in the right order. That said, the number of items should be relatively small, under 1000.
This is what I have so far:
<xsl:variable name="idchars" as="xs:string *"
select="
('0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'a','b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z')
"
/>
<xsl:variable name="idlen" as="xs:integer" select="count($idchars)" />
<xsl:function name="f:genId" as="xs:string">
<xsl:param name="pSeq" as="xs:integer" />
<xsl:variable name="vLen" as="xs:integer" select="$pSeq idiv $idlen" />
<xsl:value-of>
<xsl:for-each select="1 to $vLen">
<xsl:sequence select="$idchars[$idlen]" />
</xsl:for-each>
<xsl:sequence select="$idchars[$pSeq mod $idlen]" />
</xsl:value-of>
</xsl:function>

You could use format-number() in order to generate a left-padded sequence of numbers with a variable length of leading zeros, then convert each of those digits into a letter using codepoints-to-string(), and then join them together with string-join():
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:f="local">
<xsl:output indent="yes"/>
<xsl:function name="f:genId" as="xs:string">
<xsl:param name="pSeq" as="xs:integer" />
<xsl:param name="digits" as="xs:integer"/>
<xsl:variable name="picture" select="string-join( ((1 to $digits) ! '0'), '')" />
<xsl:variable name="padded-string" select="format-number($pSeq, $picture)" as="xs:string"/>
<xsl:variable name="padded-letter-seq" select="(1 to string-length($picture)) ! substring($padded-string, ., 1) ! codepoints-to-string(xs:integer(.) + 65)" as="xs:string*"/>
<xsl:sequence select="string-join($padded-letter-seq, '')"/>
</xsl:function>
<xsl:function name="f:genId" as="xs:string">
<xsl:param name="pSeq" as="xs:integer"/>
<xsl:sequence select="f:genId($pSeq, 4)"/>
</xsl:function>
<xsl:template match="/">
<xsl:sequence select="f:genId(3)"/>
<xsl:sequence select="f:genId(3,5)"/>
</xsl:template>
</xsl:stylesheet>

Just use the <xsl:sort> xslt element, simply like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/*">
<xsl:copy>
<xsl:apply-templates select="*">
<xsl:sort select="sequence"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<list>
<item>
<sequence>1</sequence>
</item>
<item>
<sequence>2</sequence>
</item>
<item>
<sequence>3</sequence>
</item>
<item>
<sequence>4</sequence>
</item>
...
<item>
<sequence>14</sequence>
</item>
<item>
<sequence>15</sequence>
</item>
</list>
the wanted, correct result is produced:
<list>
<item>
<sequence>1</sequence>
</item>
<item>
<sequence>14</sequence>
</item>
<item>
<sequence>15</sequence>
</item>
<item>
<sequence>2</sequence>
</item>
<item>
<sequence>3</sequence>
</item>
<item>
<sequence>4</sequence>
</item>
</list>
Do note:
This:
<xsl:sort select="sequence"/>
is equivalent to:
<xsl:sort select="sequence" data-type="text"/>
as "text" is the default value for the data-type attribute.
If it is necessary to sort numerically, one should explicitly use:
<xsl:sort select="sequence" data-type="number"/>

Related

Sum two elements by grouping using 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>.

Dynamically replacing substring in an given data(XML to fixed length)

Actually I have started with my XSLT work recently, I am facing difficulty in solving one of the requirement.
I am trying to fetch an substring from DATA element in the mentioned input i,e is ECHO and OKAY these codes need to be replaced with the values present under CODE/ECHO and CODE/OKAY in the same input. I had tried storing the substring in a variable and as the variable value and tag value would be same, I have tried to fetch that in . But its not working.
Is it that we cant use variables in the XPATHS or there is some other representation which needs to be used? Could anyone please help me with this.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output omit-xml-declaration="yes" />
<xsl:param name="break" select="'
'" />
<xsl:template match="/">
<xsl:variable name="String" select="substring(DATA, (string-length(substring(DATA,0,77)) + 1), 4)" />
<xsl:variable name="String1" >
<xsl:value-of select="Root/CODES/$String" />
</xsl:variable>
<xsl:value-of select="$break" />
<xsl:value-of select="$String1" />
</xsl:for-each>
</xsl:template>
Input:
<?xml version='1.0' encoding='utf-8'?>
<ROOT>
<INPUT>
<I_FILENAME>ERES</I_FILENAME>
</INPUT>
<CODES>
<ECHO>A1</ECHO>
<OKAY>A2</OKAY>
</CODES>
<TABLES>
<T_ER>
<item>
<DATA> HEADERERESRGCITIS220190301124112000000RGERSD46</DATA>
</item>
<item>
<DATA>000000 ABCD EF 0000000000 2018-11-060000000000EF 000000000000010000ECHO00400300000000000XXXXXX 000{ P 2018-11-05</DATA>
</item>
<item>
<DATA>000000 ABCD EF 0000000000 2018-11-060000000000EF 000000000000010000OKAY00400300000000000XXXXXX 000{ P 2018-11-05</DATA>
</item>
<item>
<DATA>TRAILERERESRGCITIS220190301124112000000001570000</DATA>
</item>
</T_ER>
</TABLES>
</ROOT>
EXPECTED OUT PUT:
HEADERERESRGCITIS220190301124112000000RGERSD46
000000 ABCD EF 0000000000 2018-11-060000000000EF 000000000000010000A100400300000000000XXXXXX 000{ P 2018-11-05
000000 ABCD EF 0000000000 2018-11-060000000000EF 000000000000010000A200400300000000000XXXXXX 000{ P 2018-11-05
<xsl:template match="INPUT|CODES">
</xsl:template>
<xsl:template match="TABLES">
<xsl:variable name="break" select="'
'" />
<xsl:for-each select="T_ER/item"><xsl:value-of select="$break"></xsl:value-of>
<xsl:value-of select="DATA"/>
</xsl:for-each>
</xsl:template>

Block XML elements if underneath item is missing or blank

I have an xml file which is generated by a process. This xml may have many products and Items under each product. However, there are scenarios when
product can have no items underneath as shown in the example below. In this situation, I want xml not to generate products where there is not item.
I want xsl script which should be able to do this. Can anyone please help?
The input xml file is
<?xml version="1.0" encoding="UTF-8"?>
<products>
<product>
<prod id>P16754</prod id>
<product-status>CREATED</product-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
<items/>
<created-on>2016-08-12T11:30:00</created-on>
<created-by>Administrator</created-by>
<last-changed-on>2016-08-04T17:34:00</last-changed-on>
<last-changed-by>ap0712</last-changed-by>
<delete>false</delete>
</product>
<product>
<prod id>P16754</prod id>
<product-status>CREATED</product-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
<items>
<item>
<item id>i16754</item id>
<item-status>CREATED</item-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
</item>
<items/>
<created-on>2016-08-12T11:30:00</created-on>
<created-by>Administrator</created-by>
<last-changed-on>2016-08-04T17:34:00</last-changed-on>
<last-changed-by>ap0712</last-changed-by>
<delete>false</delete>
</product>
</products>
Since the first product does not have item so this product as well as item should be removed from the output xml. The output should be
<products>
<product>
<prod id>P16754</prod id>
<product-status>CREATED</product-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
<items>
<item>
<item id>i16754</item id>
<item-status>CREATED</item-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
</item>
<items/>
<created-on>2016-08-12T11:30:00</created-on>
<created-by>Administrator</created-by>
<last-changed-on>2016-08-04T17:34:00</last-changed-on>
<last-changed-by>ap0712</last-changed-by>
<delete>false</delete>
</product>
</products>
The code I am using to achieve based upon suggestions here is
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Prod ID[not(items/item)]"/>
</xsl:stylesheet>
I see that it deletes the product and item if it is blank but I am getting the following in the output for example xslt code is coming in the output
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Prod ID[not(items/item)]"/>
</xsl:stylesheet>
Any time you want to transform a document and only make some changes you start with the identity transformation template
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
and then you add templates for those elements you want to transform. If you want to delete an element then you add an empty template matching that element, as you want to remove all product elements not having item elements you use
<xsl:template match="product[not(items/item)]"/>
So all you need is
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="product[not(items/item)]"/>
</xsl:transform>
which transforms the corrected input
<?xml version="1.0" encoding="UTF-8"?>
<products>
<product>
<prod-id>P16754</prod-id>
<product-status>CREATED</product-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
<items/>
<created-on>2016-08-12T11:30:00</created-on>
<created-by>Administrator</created-by>
<last-changed-on>2016-08-04T17:34:00</last-changed-on>
<last-changed-by>ap0712</last-changed-by>
<delete>false</delete>
</product>
<product>
<prod-id>P16754</prod-id>
<product-status>CREATED</product-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
<items>
<item>
<item-id>i16754</item-id>
<item-status>CREATED</item-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
</item>
</items>
<created-on>2016-08-12T11:30:00</created-on>
<created-by>Administrator</created-by>
<last-changed-on>2016-08-04T17:34:00</last-changed-on>
<last-changed-by>ap0712</last-changed-by>
<delete>false</delete>
</product>
</products>
into the output
<?xml version="1.0" encoding="UTF-8"?><products>
<product>
<prod-id>P16754</prod-id>
<product-status>CREATED</product-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
<items>
<item>
<item-id>i16754</item-id>
<item-status>CREATED</item-status>
<validation-status>Valid</validation-status>
<duplication-status>Unique</duplication-status>
<content-status>New</content-status>
</item>
</items>
<created-on>2016-08-12T11:30:00</created-on>
<created-by>Administrator</created-by>
<last-changed-on>2016-08-04T17:34:00</last-changed-on>
<last-changed-by>ap0712</last-changed-by>
<delete>false</delete>
</product>
</products>
online at http://xsltransform.net/bFWR5DD.

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