xsl - format-number from one to two digits 1 => 01 - xslt

is there in XSLT some function or somethg to alow format digits
like eg 1 will become 01
so if I have
<a>10</a>
<b>5</b>
they will apear as
A10
and
B05

Function Formating numbers:
<xsl:template match="root/*">
<xsl:value-of select="format-number(.,'00')"/>
</xsl:template>
XSLT 1.0 uppercase of nodenames:
<xsl:variable name="smallcase" select="'abcdefghijklmnopqrstuvwxyz'" />
<xsl:variable name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" />
<xsl:value-of select="translate(name(), $smallcase, $uppercase)" />
Combined:
<xsl:value-of select="concat(translate(name(), $smallcase, $uppercase), format-number(.,'00'))" />

format-number(5, '00')
returns "05".
Similarly,
format-number(10, '00')
returns "10".

Related

XSLT mapping in SOA transformation to get max date from last month

I am trying to create an XSLT mapping to get the last(max) day of the previous month.
Eg- If I pass a value of 2019-10-17 to the mapping it should return
2019-09-30. The date format that I am using here is YYYY-MM-DD.
tried to get the month from the current data and subtract it with 1 so that it would return the previous month. But I am not able to get the max date of the last month.
xp20:month-from-dateTime (/ns0:ddSelecCorpoMasterOutputCollection/ns0:ddSelecCorpoMasterOutput/ns0:FROM_DATE_FILTER ) - 1
input- sysdate
o/p- maxdate of previous month
eg- i/p-2019-10-18
o/p- 2019-09-30
Thanks in advance.
Finding the last day of previous month in pure XSLT 1.0:
<xsl:template name="end-of-last-month">
<xsl:param name="date"/>
<!-- extract date components -->
<xsl:variable name="year" select="substring($date, 1, 4)"/>
<xsl:variable name="month" select="substring($date, 6, 2)"/>
<!-- go one month back -->
<xsl:variable name="y" select="$year - ($month = 1)"/>
<xsl:variable name="m" select="($month + 10) mod 12 + 1"/>
<!-- get month length -->
<xsl:variable name="cal" select="'312831303130313130313031'"/>
<xsl:variable name="leap" select="not($y mod 4) and $y mod 100 or not($y mod 400)"/>
<xsl:variable name="month-length" select="substring($cal, 2*($m - 1) + 1, 2) + ($m=2 and $leap)" />
<!-- output -->
<xsl:value-of select="$y" />
<xsl:value-of select="format-number($m, '-00')" />
<xsl:text>-</xsl:text>
<xsl:value-of select="$month-length" />
</xsl:template>
Example:
XML
<input>
<date>2019-01-15</date>
<date>2019-02-15</date>
<date>2019-03-15</date>
<date>2019-04-15</date>
<date>2019-05-15</date>
<date>2019-06-15</date>
<date>2019-07-15</date>
<date>2019-08-15</date>
<date>2019-09-15</date>
<date>2019-10-15</date>
<date>2019-11-15</date>
<date>2019-12-15</date>
<date>2020-01-15</date>
<date>2020-02-15</date>
<date>2020-03-15</date>
</input>
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/input">
<output>
<xsl:for-each select="date">
<end-of-last-month date="{.}">
<xsl:call-template name="end-of-last-month">
<xsl:with-param name="date" select="."/>
</xsl:call-template>
</end-of-last-month>
</xsl:for-each>
</output>
</xsl:template>
<xsl:template name="end-of-last-month">
<xsl:param name="date"/>
<!-- extract date components -->
<xsl:variable name="year" select="substring($date, 1, 4)"/>
<xsl:variable name="month" select="substring($date, 6, 2)"/>
<!-- go one month back -->
<xsl:variable name="y" select="$year - ($month = 1)"/>
<xsl:variable name="m" select="($month + 10) mod 12 + 1"/>
<!-- get month length -->
<xsl:variable name="cal" select="'312831303130313130313031'"/>
<xsl:variable name="leap" select="not($y mod 4) and $y mod 100 or not($y mod 400)"/>
<xsl:variable name="month-length" select="substring($cal, 2*($m - 1) + 1, 2) + ($m=2 and $leap)" />
<!-- output -->
<xsl:value-of select="$y" />
<xsl:value-of select="format-number($m, '-00')" />
<xsl:text>-</xsl:text>
<xsl:value-of select="$month-length" />
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<output>
<end-of-last-month date="2019-01-15">2018-12-31</end-of-last-month>
<end-of-last-month date="2019-02-15">2019-01-31</end-of-last-month>
<end-of-last-month date="2019-03-15">2019-02-28</end-of-last-month>
<end-of-last-month date="2019-04-15">2019-03-31</end-of-last-month>
<end-of-last-month date="2019-05-15">2019-04-30</end-of-last-month>
<end-of-last-month date="2019-06-15">2019-05-31</end-of-last-month>
<end-of-last-month date="2019-07-15">2019-06-30</end-of-last-month>
<end-of-last-month date="2019-08-15">2019-07-31</end-of-last-month>
<end-of-last-month date="2019-09-15">2019-08-31</end-of-last-month>
<end-of-last-month date="2019-10-15">2019-09-30</end-of-last-month>
<end-of-last-month date="2019-11-15">2019-10-31</end-of-last-month>
<end-of-last-month date="2019-12-15">2019-11-30</end-of-last-month>
<end-of-last-month date="2020-01-15">2019-12-31</end-of-last-month>
<end-of-last-month date="2020-02-15">2020-01-31</end-of-last-month>
<end-of-last-month date="2020-03-15">2020-02-29</end-of-last-month>
</output>
Demo: https://xsltfiddle.liberty-development.net/94AbWB5
If you have access to the XPath 2.0 date/time library,
(1) convert to an xs:date
<xsl:variable name="d" select="xs:date($in)"/>
(2) extract the day of the month:
<xsl:variable name="dom" select="day-from-date($d)"/>
(3) subtract this number of days from the date:
<xsl:variable name="result" select="$d - xs:dayTimeDuration('P1D') * $dom"/>

Better way to cycle xsl:for-each letter of the alphabet?

I have a long XML file from which I ned to pull out book titles and other information, then sort it alphabetically, with a separator for each letter. I also need a section for items that don't begin with a letter, say a number or symbol. Something like:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
…
I also need to create a separate list of authors, created from the same data but showing different kinds of information.
How I'm currently doing it
This is simplified, but I basically have this same code twice, once for titles and once for authors. The author version of the template works with different elements and does different things with the data, so I can't use the same template.
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'#'" />
</xsl:call-template>
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'A'" />
</xsl:call-template>
…
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'Z'" />
</xsl:call-template>
<xsl:template name="BIP-letter">
<xsl:param name="letter" />
<xsl:choose>
<xsl:when test="$letter = '#'">
<xsl:text>#</xsl:text>
<xsl:for-each select="//Book[
not(substring(Title,1,1) = 'A') and
not(substring(Title,1,1) = 'B') and
…
not(substring(Title/,1,1) = 'Z')
]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$letter" />
<xsl:for-each select="//Book[substring(Title,1,1) = $letter]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
My questions
The code above works just fine, but:
Manually cycling through each letter gets very long, especially having to do it twice. Is there a way to simplify that? Something like a <xsl:for-each select="[A-Z]"> that I could use to set the parameter when calling the template?
Is there a simpler way to select all titles that don't begin with a letter? Something like //Book[not(substring(Title,1,1) = [A-Z])?
There may be cases where the title or author name starts with a lowercase letter. In the code above, they would get grouped with under the # heading, rather than with the actual letter. The only way I can think to accommodate that—doing it manually—would significantly bloat up the code.
This solution answers all questions asked:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vLowercase" select="'abcdefghijklmnopqrstuvuxyz'"/>
<xsl:variable name="vUppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vDigits" select="'0123456789'"/>
<xsl:key name="kBookBy1stChar" match="Book"
use="translate(substring(Title, 1, 1),
'abcdefghijklmnopqrstuvuxyz0123456789',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ##########'
)"/>
<xsl:template match="/*">
<xsl:apply-templates mode="firstInGroup" select=
"Book[generate-id()
= generate-id(key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)[1]
)
]">
<xsl:sort select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book" mode="firstInGroup">
<xsl:value-of select="'
'"/>
<xsl:value-of select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
<xsl:apply-templates select=
"key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)">
<xsl:sort select="Title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="'
'"/>
<xsl:value-of select="concat(Title, ' - ', Binding, ', $', price)"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following xml document (none provided in the question!):
<Books>
<Book>
<Title>After the Sands</Title>
<Binding>paperback</Binding>
<price>24.95</price>
</Book>
<Book>
<Title>Cats Galore: A Compendium of Cultured Cats</Title>
<Binding>hardcover</Binding>
<price>5.00</price>
</Book>
<Book>
<Title>Arctic Spirit</Title>
<Binding>hardcover</Binding>
<price>65.00</price>
</Book>
<Book>
<Title>1494</Title>
<Binding>hardcover</Binding>
<price>9.99</price>
</Book>
<Book>
<Title>Back to the Front</Title>
<Binding>paperback</Binding>
<price>18.95</price>
</Book>
</Books>
the wanted, correct result is produced:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
C
Cats Galore: A Compendium of Cultured Cats - hardcover, $5.00
Explanation:
Use of the Muenchian method for grouping
Use of the standard XPath translate() function
Using mode to process the first book in a group of books starting with the same (case-insensitive) character
Using <xsl:sort> to sort the books in alphabetical orser
The most problematic part is this:
I also need a section for items that don't begin with a letter, say a number or symbol.
If you have a list of all possible symbols that an item can begin with, then you can simply use translate() to convert them all to the # character. Otherwise it gets more complicated. I would try something like:
XSLT 1.0 (+ EXSLT node-set())
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:key name="book" match="Book" use="index" />
<xsl:template match="/Books">
<!-- first-pass: add index char -->
<xsl:variable name="books-rtf">
<xsl:for-each select="Book">
<xsl:copy>
<xsl:copy-of select="*"/>
<index>
<xsl:variable name="index" select="translate(substring(Title, 1, 1), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $index)">
<xsl:value-of select="$index"/>
</xsl:when>
<xsl:otherwise>#</xsl:otherwise>
</xsl:choose>
</index>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="books" select="exsl:node-set($books-rtf)/Book" />
<!-- group by index char -->
<xsl:for-each select="$books[count(. | key('book', index)[1]) = 1]">
<xsl:sort select="index"/>
<xsl:value-of select="index"/>
<xsl:text>
</xsl:text>
<!-- list books -->
<xsl:for-each select="key('book', index)">
<xsl:sort select="Title"/>
<xsl:value-of select="Title"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="Binding"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="Price"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, this still leaves the problem of items that begin with a diacritic, e.g. "Österreich" or say a Greek letter. Under this method they too will be clumped under #.
Unfortunately, the only good solution for this is to move to XSLT 2.0.
Demo: https://xsltfiddle.liberty-development.net/jyRYYjj/2

XSLT time zone conversion

I'm working with a system that will only output the server time in Central Time Zone (CT). I need to convert this in XSLT to US Eastern Time.
Is there a built in method to translate this or do I need to use Regex?
<node time="02:14 pm CT" />
Current Output: 02:14 pm CT
Desired Output: 03:14 pm ET
There are at least two main paths to choose between, converting it into a time and using a time based library, or taking it as a string and doing straight string manipulation. The following is string manipulation:
<xsl:variable name="time" select="'11:14 pm CT'"/> <!-- the input value -->
<xsl:variable name="hours" select="number(substring-before($time,':'))"/> <!-- numeric hours -->
<xsl:variable name="mer" select="substring($time,7,2)"/> <!-- the am or pm part -->
<xsl:choose>
<xsl:when test="$hours < 12"> <!-- if we are 01-11 -->
<xsl:value-of select="substring(concat('0', $hours + 1), string-length(concat('0', $hours + 1)) - 1, 2)"/> <!-- add an hour and repad the string with leading zero, messy -->
</xsl:when>
<xsl:otherwise>
<xsl:text>01</xsl:text> <!-- we were 12, so just use 01 -->
</xsl:otherwise>
</xsl:choose>
<xsl:value-of select="substring($time, 3,4)"/> <!-- pull the minutes forward -->
<xsl:choose>
<xsl:when test="not($hours = 11)"> <!-- if we were not 11 for hours we keep the same am/pm -->
<xsl:value-of select="$mer"/>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="$mer = 'pm'"> <!-- otherwise we flip it -->
<xsl:text>am</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>pm</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
<xsl:text> ET</xsl:text>
There is no built-in method for this in XSLT 1.0. Regardless, it would have been fairly trivial to do - except for the fact that your time input is in 12-hour format. This makes the process rather tedious, so I have split it off to a processing template:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<node>
<xsl:attribute name="time">
<xsl:call-template name="time-offset">
<xsl:with-param name="time" select="node/#time"/>
</xsl:call-template>
</xsl:attribute>
</node>
</xsl:template>
<xsl:template name="time-offset">
<xsl:param name="time"/>
<xsl:param name="offset" select="1"/>
<xsl:param name="h12" select="substring($time, 1, 2)"/>
<xsl:param name="pm" select="contains($time,'p') or contains($time,'P')"/>
<xsl:param name="h24" select="$h12 mod 12 + 12*$pm"/>
<xsl:param name="newH24" select="($h24 + $offset + 24) mod 24"/>
<xsl:param name="newH12" select="($newH24 + 11) mod 12 + 1"/>
<xsl:param name="am.pm" select="substring('AMPM', 1 + 2*($newH24 > 11), 2)"/>
<xsl:value-of select="concat(format-number($newH12, '00'), substring($time, 3, 4), $am.pm, ' ET')"/>
</xsl:template>
</xsl:stylesheet>
When the above stylesheet is applied to the example input:
<node time="12:14 am CT" />
the result is:
<?xml version="1.0" encoding="UTF-8"?>
<node time="01:14 AM ET"/>

How to trim result string value?

<xsl:value-of select="IPADDRESS" />
The above line returns the IP address 192.123.201.21 but I want the output to be 192.123.201. How to split the string at . and remove the last token?
In XSLT 1.0 you need to work a little harder at it:
<xsl:variable name="lastOctet" select="substring-after(substring-after(substring-after(IPADDRESS, '.'), '.'), '.')" />
<xsl:value-of select="substring(IPADDRESS, 1, string-length(IPADDRESS) - string-length($lastOctet) - 1)" />
The XPath 1.0 substring-before and substring-after functions can give you the substring before/after the first occurrence of a given separator, but to find the substring before the last occurrence you need to use a tail-recursive template
<xsl:template name="substring-before-last">
<xsl:param name="str" />
<xsl:param name="separator" />
<xsl:param name="prefix" select="''" /><!-- first segment - no prefix -->
<xsl:variable name="after-first" select="substring-after($str, $separator)" />
<xsl:if test="$after-first">
<xsl:value-of select="concat($prefix, substring-before($str, $separator))" />
<xsl:call-template name="substring-before-last">
<xsl:with-param name="str" select="$after-first" />
<xsl:with-param name="separator" select="$separator" />
<!-- for second and subsequent segments, prepend a $separator -->
<xsl:with-param name="prefix" select="$separator" />
</xsl:call-template>
</xsl:if>
</xsl:template>
This template keeps writing out segments between separators until it reaches a point where there are no more instances of the separator string. You would call it by replacing your xsl:value-of element with
<xsl:call-template name="substring-before-last">
<xsl:with-param name="str" select="IPADDRESS" />
<xsl:with-param name="separator" select="'.'" /><!-- note the single quotes -->
</xsl:call-template>
With XSLT 2.0 you could use <xsl:value-of select="tokenize(IPADDRESS, '\.')[position() lt last()]" separator="."/>.
This should work (referring to your title: "how to trim result string value?"):
<xsl:value-of select="substring(IPADDRESS,1,11)" />
Can you rely on the IPADDRESS element to always have the same structure and content? If so, there is no need to tokenize.

XSLT count comma values count

I have a value like integer="1,2,3,4,5" in the xml. How can I count the total number using XSLT. So that the output gives me a count of 5
Regards,
Sam
Here's one way (there may be others). Simply translate all commas into empty strings, and then compare in difference in length of strings:
<xsl:value-of
select="string-length(#integer)
- string-length(translate(#integer, ',', '')) + 1" />
If you need to handle empty strings, try this instead
<xsl:value-of
select="string-length(#integer)
- string-length(translate(#integer, ',', ''))
+ 1 * (string-length(#integer) != 0)" />
If you want to count the comma-separated-values, but ALSO be able to reference the individual items, you can use a recursive template like such.
This XSLT 1.0 style-sheet will convert the comma-separated-values into nodes and then count them ...
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="as-nodes">
<xsl:call-template name="parse-comma-separated-values">
<xsl:with-param name="csv" select="t/#csv" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="count(msxsl:node-set($as-nodes)/*)" />
</xsl:template>
<xsl:template name="parse-comma-separated-values">
<xsl:param name="csv" />
<xsl:choose>
<xsl:when test="$csv = ''"/>
<xsl:when test="not( contains( $csv, ','))">
<value-node value="{$csv}" />
</xsl:when>
<xsl:otherwise>
<value-node value="{substring-before($csv,',')}" />
<xsl:call-template name="parse-comma-separated-values">
<xsl:with-param name="csv" select="substring-after($csv,',')"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
... when applied to this input document ...
<t csv="1,2,3,4,5"/>
... produces ...
5