XSLT Need to Limit Return of Multiple Instances in XML File to 18 Characters - xslt

I currently have the following code to combine multiple instances of Ustrd into one returned value:
<Ustrd>
<xsl:value-of select="a:RmtInf/a:Ustrd"/>
</Ustrd>
This returns:
<Ustrd>Item-1 Item-2 Item-3</Ustrd>
The problem is that I need to limit this to 18 characters, and the substring function does not work with a sequence of items.
Tried:
<Ustrd>
<xsl:value-of select="substring(a:RmtInf/a:Ustrd, 1, 18"/>
</Ustrd>
Expected Result:
<Ustrd>Item-1 Item-2 Item</Ustrd>

Use string-join first e.g. substring(string-join(a:RmtInf/a:Ustrd, ' '), 1, 18). In XPath 3.1 you can also write that as a:RmtInf/a:Ustrd => string-join(' ') => substring(1, 18).

Here's a way this could be done in XSLT 1.0.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Ustrd>
<xsl:variable name="temp">
<xsl:for-each select="RmtInf/Ustrd">
<xsl:value-of select="."/>
<xsl:if test="position()!=last()">
<xsl:value-of select="' '"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="substring($temp,1,18)"/>
</Ustrd>
</xsl:template>
</xsl:stylesheet>
(Only need to add your namespace.)
See it working here: https://xsltfiddle.liberty-development.net/pPgzCL4

Related

XSLT sort across nodes and text

I'm trying to find, sort, and output a string of copyright years. I've got a working bit of code, but I just found that some of my years are not in the same tags as others.
Initially I thought all my years were in the following tag: <copyright-year>2020</copyright-year>, see below for a working bit of code to find, sort, and output those.
I just found that some of my copyright years look like this: <copyright-statement>© 2017 Company. All rights reserved.</copyright-statement>.
I can find the years in these statements using //copyright-statement/substring(.,3,4). However, when I tried to search for both types like this: <xsl:for-each-group select="//copyright-year|copyright-statement/substring(., 3, 4)" group-by="text()">, it gives the following warning:
Required item type of document-order sorter is node(); supplied expression ((./copyright-statement)/(fn:substring(...))) has item type xs:string. The expression can succeed only if the supplied value is an empty sequence.
And obviously doesn't work. Any idea how to merge these two sets of years to get: <output>2020, 2019, 2017</output>?
Sample XML
<?xml version="1.0" encoding="UTF-8"?>
<book>
<book-meta>
<copyright-year>2020</copyright-year>
</book-meta>
<body>
<book-part>
<book-part-meta>
<copyright-year>2019</copyright-year>
</book-part-meta>
</book-part>
</body>
<back>
<copyright-statement>© 2017 Company. All rights reserved.</copyright-statement>
</back>
</book>
Sample 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"
exclude-result-prefixes="xs"
version="2.0">
<xsl:template match="book">
<xsl:variable name="years">
<xsl:for-each-group select="//copyright-year" group-by="text()">
<xsl:sort select="." order="descending"/>
<xsl:value-of select="."/><xsl:if test="position() != last()"><xsl:text>, </xsl:text></xsl:if>
</xsl:for-each-group>
</xsl:variable>
<output><xsl:value-of select="$years"/></output>
</xsl:template>
</xsl:stylesheet>
Which version of which XSLT processor do you use? XSLT 3 has a sort function
<xsl:value-of select="reverse(sort(distinct-values((//copyright-year/xs:integer(.), //copyright-statement/xs:integer(substring(.,3,4))))))" separator=", "/>
https://xsltfiddle.liberty-development.net/bwdwsd
It might be easier to read that with the new => arrow operator:
<xsl:value-of
select="(//copyright-year/xs:integer(.), //copyright-statement/xs:integer(substring(.,3,4)))
=> distinct-values()
=> sort()
=> reverse()"
separator=", "/>
https://xsltfiddle.liberty-development.net/bwdwsd/2
But in general the step you need is to simply ensure you work with atomic values e.g. xs:integers seems the right value for years. I think in XSLT 2 I would wrap perform-sort into a function:
<xsl:function name="mf:sort" as="item()*">
<xsl:param name="input" as="item()*"/>
<xsl:perform-sort select="$input">
<xsl:sort order="descending"/>
</xsl:perform-sort>
</xsl:function>
<xsl:template match="book">
<xsl:value-of select="mf:sort(distinct-values((//copyright-year/xs:integer(.), //copyright-statement/xs:integer(substring(.,3,4)))))" separator=", "/>
</xsl:template>
https://xsltfiddle.liberty-development.net/bwdwsd/1
Here's one way to get the specify output;
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/book">
<xsl:variable name="years" as="xs:string*">
<xsl:perform-sort>
<xsl:sort select="." data-type="number" order="descending"/>
<xsl:apply-templates select="//(copyright-year | copyright-statement)"/>
</xsl:perform-sort>
</xsl:variable>
<output>
<xsl:value-of select="$years" separator=","/>
</output>
</xsl:template>
<xsl:template match="copyright-statement">
<xsl:value-of select="substring-before(substring-after(., '© '), ' ')"/>
</xsl:template>
</xsl:stylesheet>
Demo: https://xsltfiddle.liberty-development.net/bwdwsd/3

XSLT 1.0 - how to check when condition for string

I am trying to conditional check on the input xml file and place the value.
input xml:
<workorder>
<newwo>1</newwo>
</workorder>
If newwo is 1, then I have to set in my output as "NEW" else "OLD"
Expected output is:
newwo: "NEW"
my xslt is:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">
<xsl:template match="/">
<xsl:apply-templates select="NEWWO" />
</xsl:template>
<xsl:template match="/NEWWO">
<xsl:text>{
newwo:"
</xsl:text>
<xsl:choose>
<xsl:when test="NEWWO != '0'">NEW</xsl:when>
<xsl:otherwise>OLD</xsl:otherwise>
</xsl:choose>
<xsl:text>"
}</xsl:text>
</xsl:template>
Please help me. Thanks in advance!
I see a number of reasons you aren't getting output.
The xpaths are case sensitive. NEWWO is not going to match newwo.
You match / and then apply-templates to newwo (case fixed), but newwo doesn't exist at that context. You'll either have to add */ or workorder/ to the apply-templates (like select="*/newwo") or change / to /* or /workorder in the match.
You match /newwo (case fixed again), but newwo is not the root element. Remove the /.
You do the following test: test="newwo != '0'", but newwo is already the current context. Use . or normalize-space() instead. (If you use normalize-space(), be sure to test against a string. (Quote the 1.))
Here's an updated example.
XML Input
<workorder>
<newwo>1</newwo>
</workorder>
XSLT 1.0
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:apply-templates select="newwo" />
</xsl:template>
<xsl:template match="newwo">
<xsl:text>{
newwo: "</xsl:text>
<xsl:choose>
<xsl:when test=".=1">NEW</xsl:when>
<xsl:otherwise>OLD</xsl:otherwise>
</xsl:choose>
<xsl:text>"
}</xsl:text>
</xsl:template>
</xsl:stylesheet>
Output
{
newwo: "NEW"
}
You try it as below
<?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" version="1.0">
<xsl:template match="/">
<xsl:choose>
<xsl:when test="/workorder/newwo = 1">
<xsl:text disable-output-escaping="no"> newwo:New</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text disable-output-escaping="no"> newwo:Old</xsl:text> </xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

Greatest number from the tags in XML

I have an xml with different name tags at different places. What I would need to do is, sort the needed one and get the max value of them.
Input XML:
<SUBSCRIBER>
<OnPeakAccountID>10</OnPeakAccountID>
<OnPeakSmsExpDate>**20640217172520**</OnPeakSmsExpDate>
<UnliSmsOnCtl>20140204173322</UnliSmsOnCtl>
<BucketMocOn>840</BucketMocOn>
<BucketMocOnExp>20140204173322</BucketMocOnExp>
<BucketMocTri>10000</BucketMocTri>
<BucketMocTriExp>**20140210235959**</BucketMocTriExp>
<UnliNxbFbcCtl>**20140210235959**</UnliNxbFbcCtl>
<BucketIM6VolFbc>10000</BucketIM6VolFbc>
<BucketIM6VolFbcExp>**20140210235959**</BucketIM6VolFbcExp>
<UnliEmail1FbcCtl>**20140210235959**</UnliEmail1FbcCtl>
<UnliIM2FbcCtl>**20140210235959**</UnliIM2FbcCtl>
<UnliPhoto1FbcCtl>**20140210235959**</UnliPhoto1FbcCtl>
<UnliSns1FbcCtl>**20140210235959**</UnliSns1FbcCtl>
<UnliBoost1FbcCtl>**20140210235959**</UnliBoost1FbcCtl>
<UnliBrws1FbcCtl>**20140210235959**</UnliBrws1FbcCtl>
</SUBSCRIBER>
For example, In the above XML, I need to find the greatest value from the elements marked with **<>**. To be honest, I am still fighting for the logic and nothing is working at my end. Any help/suggestions would be appreciated. Thanks for your support.
I think you want this template:
<xsl:stylesheet version='1.0' xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="SUBSCRIBER">
<xsl:for-each select="OnPeakSmsExpDate|UnliSmsOnCtl|BucketMocOnExp|BucketMocTriExp|UnliNxbFbcCtl|BucketIM6VolFbcExp|UnliEmail1FbcCtl|UnliIM2FbcCtl|UnliPhoto1FbcCtl|UnliSns1FbcCtl|UnliBoost1FbcCtl|UnliBrws1FbcCtl">
<xsl:sort order="descending"/>
<xsl:if test="position() = 1">
<greatest><xsl:value-of select="."/></greatest>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
XSLT 2.0
<xsl:template match="/SUBSCRIBER">
<max><xsl:value-of select="max((BucketMocTriExp, UnliNxbFbcCtl, BucketIM6VolFbcExp, UnliEmail1FbcCtl, UnliIM2FbcCtl, UnliPhoto1FbcCtl, UnliSns1FbcCtl, UnliBoost1FbcCtl, UnliBrws1FbcCtl))"/></max>
</xsl:template>
EDIT:
To include a node conditionally, use the following construct:
<xsl:value-of select="max((a, b, c, if(d > 0) then e else ())) "/>
This XSLT sorts elements with value (containing 2 asterisk at start and at end) in descending order and extrats the first one:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:template match="SUBSCRIBER">
<xsl:for-each select="*[starts-with(.,'**') and substring(.,string-length(.) - 1) = '**']">
<xsl:sort select="number(substring(substring(.,3),1,string-length(substring(.,3)) - 2))" order="descending"/>
<xsl:if test="position() = 1">
<xsl:value-of select="substring(substring(.,3),1,string-length(substring(.,3)) - 2)"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

check for successively numbered attributes

I have a situation where I need to check for attribute values that may be successively numbered and input a dash between the start and end values.
<root>
<ref id="value00008 value00009 value00010 value00011 value00020"/>
</root>
The ideal output would be...
8-11, 20
I can tokenize the attribute into separate values, but I'm unsure how to check if the number at the end of "valueXXXXX" is successive to the previous value.
I'm using XSLT 2.0
You can use xsl:for-each-group with #group-adjacent testing for the number() value subtracting the position().
This trick was apparently invented by David Carlisle, according to Michael Kay.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:output indent="yes"/>
<xsl:template match="/">
<xsl:variable name="vals"
select="tokenize(root/ref/#id, '\s?value0*')[normalize-space()]"/>
<xsl:variable name="condensed-values" as="item()*">
<xsl:for-each-group select="$vals"
group-adjacent="number(.) - position()">
<xsl:choose>
<xsl:when test="count(current-group()) > 1">
<!--a sequence of successive numbers,
grab the first and last one and join with '-' -->
<xsl:sequence select="
string-join(current-group()[position()=1
or position()=last()]
,'-')"/>
</xsl:when>
<xsl:otherwise>
<!--single value group-->
<xsl:sequence select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:variable>
<xsl:value-of select="string-join($condensed-values, ',')"/>
</xsl:template>
</xsl:stylesheet>

XSL associative sorting using a field substring

The transformation I am writing must compose a comma separated string value from a given node set. The resulting string must be sorted according to a random (non-alphabetic) mapping for the first character in the input values.
I came up with this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:tmp="http://tempuri.org"
exclude-result-prefixes="tmp"
>
<xsl:output method="xml" indent="yes"/>
<tmp:sorting-criterion>
<code value="A">5</code>
<code value="B">1</code>
<code value="C">3</code>
</tmp:sorting-criterion>
<xsl:template match="/InputValueParentNode">
<xsl:element name="OutputValues">
<xsl:for-each select="InputValue">
<xsl:sort select="document('')/*/tmp:sorting-criterion/code[#value=substring(.,1,1)]" data-type="number"/>
<xsl:value-of select="normalize-space(.)"/>
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
It doesn't work and looks like the XPath document('')/*/tmp:sorting-criterion/code[#value=substring(.,1,1)] does not evaluate as I expect. I've checked to substitute the substring(.,1,1) for a literal and it evaluates to the proper value.
So, am I missing something that makes the sorting XPath expression not to evaluate as I expect or is it simply impossile to do it this way?
If not possible to create a XPath expression that works, is there a work around to achieve my purpose?
Note: I'm constrained to XSLT-1.0
Sample Input:
<?xml version="1.0" encoding="utf-8"?>
<InputValueParentNode>
<InputValue>A input value</InputValue>
<InputValue>B input value</InputValue>
<InputValue>C input value</InputValue>
</InputValueParentNode>
Expected ouput:
<?xml version="1.0" encoding="utf-8"?>
<OutputValues>B input value,C input value,A input value</OutputValues>
Replace the self::node() abbreviation ., with current() function.
A better predicate would be: starts-with(normalize-space(current()),#value)
Besides changing transformation according to Alejandro´s answer, I found it better to use a XSL variable for th mapping data to avoid declaration of a dummy namespace (tmp) as seen in Dimitre´s answer to another related question.
My final implementation:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/InputValueParentNode">
<xsl:variable name="sorting-map">
<i code="A" priority="5"/>
<i code="B" priority="1"/>
<i code="C" priority="3"/>
</xsl:variable>
<xsl:variable name="sorting-criterion" select="document('')//xsl:variable[#name='sorting-map']/*"/>
<xsl:element name="OutputValues">
<xsl:for-each select="InputValue">
<xsl:sort select="$sorting-criterion[#code=substring(normalize-space(current()),1,1)]/#priority" data-type="number"/>
<xsl:value-of select="normalize-space(current())"/>
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>