XSLT sort across nodes and text - xslt

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

Related

XSLT: Copying node data to another node by matching attribute value, Efficiently?

I coded the XSLT to copy one node data to another by validating the attribute value, I got the desired output but I'm curious to know whether there is an efficient way to do this or if this is the only way to do it. [I'm not an XSLT expert] Can someone help !!!
Please use this link to check instantly.
https://xsltfiddle.liberty-development.net/pNvtBH2/3
Actual XML:
<?xml version="1.0" encoding="utf-8" ?>
<section>
<p>note 1 : 1</p>
<p>note 2 : 2</p>
<p>note 3 : 3</p>
<note id="test1">hello one</note>
<note id="test2">hello two</note>
<note id="test3">hello <i>three</i></note>
<note id="test4">hello <i>four</i></note>
</section>
Output:
<?xml version="1.0" encoding="UTF-8"?><section>
<p>note 1 : <a>hello one</a></p>
<p>note 2 : <a>hello two</a></p>
<p>note 3 : <a>hello <i>three</i></a></p>
<note id="test4">hello <i>four</i></note>
</section>
XSLT Code:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="#all"
version="3.0">
<xsl:output method="xml" />
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="a">
<a>
<xsl:variable name="href" select="#href" />
<xsl:choose>
<xsl:when test="$href = //note/#id">
<xsl:copy-of select="//note[#id=$href]/node()" />
</xsl:when>
</xsl:choose>
</a>
</xsl:template>
<xsl:template match="note">
<xsl:choose>
<xsl:when test="#id = //a/#href">
<xsl:apply-templates select="node" />
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Whether it's efficient or not depends on how smart the optimizer in your XSLT processor is. Saxon-EE will do a pretty good job on this, Saxon-HE less so.
If you want to make it efficient on all processors, use keys. Replace an expression like //note[#id=$href] with a call on the key() function. Declare the key as
<xsl:key name="k" match="note" use="id"/>
and then you can get the matching nodes using key('k', $href).
The xsl:when test="$href = //note/#id" is redundant, as xsl:copy-of will do nothing if nothing is selected.
In the note template, I'm not sure what
<xsl:when test="#id = //a/#href">
<xsl:apply-templates select="node" />
</xsl:when>
is trying to achieve, because you don't have any elements named "node". If the aim is to avoid processing a note element at this stage if it was already processed from an a element, then you could build another index with
<xsl:key name="a" match="a" use="1"/>
and replace test="#id = //a/#href" with test="key('a', #href)"

How are sequences spliced, and why is my variable's value a document node?

Look at the code 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"
exclude-result-prefixes="xs"
version="3.0">
<xsl:output indent="yes"/>
<xsl:template match="/">
<root>
<xsl:variable name="v1">
<xsl:variable name="a1" select="137"/>
<xsl:variable name="a2" select="(1, 3, 'abc')"/>
<xsl:variable name="a3" select="823"/>
<xsl:sequence select="$a1"/>
<xsl:sequence select="$a2"/>
<xsl:sequence select="$a3"/>
</xsl:variable>
<xsl:variable name="v2" as="item()+">
<xsl:variable name="b1" select="137"/>
<xsl:variable name="b2" select="(1, 3)"/>
<xsl:variable name="b3" select="823"/>
<xsl:variable name="b4" select="'abc'"/>
<xsl:sequence select="$b1"/>
<xsl:sequence select="$b2"/>
<xsl:sequence select="$b3"/>
<xsl:sequence select="$b4"/>
</xsl:variable>
<count>
<xsl:text>v1 count is: </xsl:text>
<xsl:value-of select="count($v1)"/>
</count>
<count>
<xsl:text>v2 count is: </xsl:text>
<xsl:value-of select="count($v2)"/>
</count>
<count>
<xsl:text>a2 count is: </xsl:text>
<xsl:value-of select="count((1, 3, 'abc'))"/>
</count>
</root>
</xsl:template>
</xsl:stylesheet>
The result ouput is:
<root>
<count>v1 count is: 1</count>
<count>v2 count is: 5</count>
<count>a2 count is: 3</count>
</root>
Why v2 count is different from v1 count? They seems to have the same items. How the sequence splice?
Why is v1 treated as the 'document-node' type?
Words "It looks like your post is mostly code; please add some more details." always prevent me to submit.
Well, you have different variable declarations, as one uses the as attribute and the other not.
And you seem to have inferred that your first case without any as declaration results in a document node (containing content).
As for the gory details of the various options, the spec treats your first case in https://www.w3.org/TR/xslt-30/#temporary-trees and outlines the various options of how as, select and content constructors in xsl:variable interact in https://www.w3.org/TR/xslt-30/#variable-values.

XSLT: Replace string with Abbreviations

I would like to know how to replace the string with the abbreviations.
My XML looks like below
<concept reltype="CONTAINS" name="Left Ventricular Major Axis Diastolic Dimension, 4-chamber view" type="NUM">
<code meaning="Left Ventricular Major Axis Diastolic Dimension, 4-chamber view" value="18074-5" schema="LN" />
<measurement value="5.7585187646">
<code value="cm" schema="UCUM" />
</measurement>
<content>
<concept reltype="HAS ACQ CONTEXT" name="Image Mode" type="CODE">
<code meaning="Image Mode" value="G-0373" schema="SRT" />
<code meaning="2D mode" value="G-03A2" schema="SRT" />
</concept>
</content>
</concept>
and I am selecting some value from the xml like,
<xsl:value-of select="concept/measurement/code/#value"/>
Now what I want is, I have to replace cm with centimeter. I have so many words like this. I would like to have a xml for abbreviations and replace from them.
I saw one similar example here.
Using a Map in XSL for expanding abbreviations
But it replaces node text, but I have text as attribute. Also, it would be better for me If I can find and replace when I select text using xsl:valueof select instead of having a separate xsl:template. Please help. I am new to xslt.
I have created XSLT v "1.1". For abbreviations I have created XML file as you have mentioned:
Abbreviation.xml:
<Abbreviations>
<Abbreviation>
<Short>cm</Short>
<Full>centimeter</Full>
</Abbreviation>
<Abbreviation>
<Short>m</Short>
<Full>meter</Full>
</Abbreviation>
</Abbreviations>
XSLT:
<xsl:stylesheet version="1.1" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" method="xml" />
<xsl:param name="AbbreviationDoc" select="document('Abbreviation.xml')"/>
<xsl:template match="/">
<xsl:call-template name="Convert">
<xsl:with-param name="present" select="concept/measurement/code/#value"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="Convert">
<xsl:param name="present"/>
<xsl:choose>
<xsl:when test="$AbbreviationDoc/Abbreviations/Abbreviation[Short = $present]">
<xsl:value-of select="$AbbreviationDoc/Abbreviations/Abbreviation[Short = $present]/Full"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$present"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
INPUT:
as you have given <xsl:value-of select="concept/measurement/code/#value"/>
OUTPUT:
centimeter
You just need to enhance this Abbreviation.xml to keep short and full value of abbreviation and call 'Convert' template with passing current value to get desired output.
Here a little shorter version:
- with abbreviations in xslt file
- make use of apply-templates with mode to make usage shorter.
But with xslt 1.0 node-set extension is required.
<?xml version="1.0" encoding="utf-8"?>
<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="xml" indent="yes"/>
<xsl:variable name="abbreviations_txt">
<abbreviation abbrev="cm" >centimeter</abbreviation>
<abbreviation abbrev="m" >meter</abbreviation>
</xsl:variable>
<xsl:variable name="abbreviations" select="exsl:node-set($abbreviations_txt)" />
<xsl:template match="/">
<xsl:apply-templates select="concept/measurement/code/#value" mode="abbrev_to_text"/>
</xsl:template>
<xsl:template match="* | #*" mode="abbrev_to_text">
<xsl:variable name="abbrev" select="." />
<xsl:variable name="long_text" select="$abbreviations//abbreviation[#abbrev = $abbrev]/text()" />
<xsl:value-of select="$long_text"/>
<xsl:if test="not ($long_text)">
<xsl:value-of select="$abbrev"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

XSLT 1.0 how increment a date

UPDATE: Cannot use any EXSLT extensions.
Also I'm using date in two different places and I only want to update one of them and not both.
I need to increment a date in my XSLT transformation. I'm using XSLT 1.0.
In source XML I have a date like this
<XML>
<Date>4/22/2011 3:30:43 PM</Date>
</XML>
Then I need to add 10 years to the output. Like this
<Output>
<Odate>4/22/2011 3:30:43 PM</Odate>
<Cdate>4/22/2021 3:30:43 PM</Cdate>
</Output>
How this can be done in XSLT 1.0. Thanks in advance.
The following is no general date arithmetic implementation but might suffice to increment the year part:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:param name="year-inc" select="10"/>
<xsl:template match="XML">
<Output>
<xsl:apply-templates/>
</Output>
</xsl:template>
<xsl:template match="Date">
<xsl:variable name="d0" select="substring-before(., '/')"/>
<xsl:variable name="d1" select="substring-before(substring-after(., '/'), '/')"/>
<xsl:variable name="d2" select="substring-after(substring-after(., '/'), '/')"/>
<xsl:variable name="new-year" select="substring($d2, 1, 4) + $year-inc"/>
<Cdate>
<xsl:value-of select="concat($d0, '/', $d1, '/', $new-year, substring($d2, 5))"/>
</Cdate>
</xsl:template>
</xsl:stylesheet>
Depends a little how pernickety you want to be, e.g. what's the date 10 years after 29 Feb 2004? There are a number of useful XSLT 1.0 date-handling routines you can download at www.exslt.org, I think they include both a parse-date template which will convert your US-format date into a standard ISO date, date arithmetic templates which will allow you to add a duration to an ISO-format date, and a format-date function that will turn it back into US format.
I have figured it with the help of #Martin. I extend on #Martin's code and only called the template when I need to modify the date.
XSLT:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="year-inc" select="10"/>
<xsl:template match="XML">
<Output>
<Odate>
<xsl:value-of select="Date"/>
</Odate>
<Cdate>
<xsl:call-template name="increment"/>
</Cdate>
</Output>
</xsl:template>
<xsl:template name="increment">
<xsl:variable name="d0" select="substring-before(Date, '/')"/>
<xsl:variable name="d1" select="substring-before(substring-after(Date, '/'), '/')"/>
<xsl:variable name="d2" select="substring-after(substring-after(Date, '/'), '/')"/>
<xsl:variable name="new-year" select="substring($d2, 1, 4) + $year-inc"/>
<xsl:value-of select="concat($d0, '/', $d1, '/', $new-year, substring($d2, 5))"/>
</xsl:template>
</xsl:stylesheet>
Output:
<?xml version="1.0" encoding="utf-8"?>
<Output>
<Odate>4/22/2011 3:30:43 PM</Odate>
<Cdate>4/22/2021 3:30:43 PM</Cdate>
</Output>

Convert short form days of the week to day names in xslt

I have some short form day names like so:
M -> Monday
T -> Tuesday
W -> Wednesday
R -> Thursday
F -> Friday
S -> Saturday
U -> Sunday
How can I convert an xml element like <days>MRF</days> into the long version <long-days>Monday,Thursday,Friday</long-days> using xslt?
Update from comments
Days will not be repeated
This stylesheet
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:d="day"
exclude-result-prefixes="d">
<d:d l="M" n="Monday"/>
<d:d l="T" n="Tuesday"/>
<d:d l="W" n="Wednesday"/>
<d:d l="R" n="Thursday"/>
<d:d l="F" n="Friday"/>
<d:d l="S" n="Saturday"/>
<d:d l="U" n="Sunday"/>
<xsl:variable name="vDays" select="document('')/*/d:d"/>
<xsl:template match="days">
<long-days>
<xsl:apply-templates
select="$vDays[contains(current(),#l)]"/>
</long-days>
</xsl:template>
<xsl:template match="d:d">
<xsl:value-of select="#n"/>
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:template>
</xsl:stylesheet>
With this input:
<days>MRF</days>
Output:
<long-days>Monday,Thursday,Friday</long-days>
Edit: For those who wander, retaining the sequence order:
<xsl:variable name="vCurrent" select="current()"/>
<xsl:apply-templates
select="$vDays[contains($vCurrent,#l)]">
<xsl:sort select="substring-before($vCurrent,#l)"/>
</xsl:apply-templates>
Note: Because days wouldn't be repeated, this is the same as looking up for item existence in sequence with empty string separator.
That should do it... (There might be more elegant solutions though... ;-)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/days">
<long-days>
<xsl:if test="contains(.,'M')">Monday<xsl:if test="string-length(substring-before(.,'M'))=string-length(.)-1">,</xsl:if></xsl:if>
<xsl:if test="contains(.,'T')">Tuesday<xsl:if test="string-length(substring-before(.,'T'))=string-length(.)-1">,</xsl:if></xsl:if>
<xsl:if test="contains(.,'W')">Wednesday<xsl:if test="string-length(substring-before(.,'W'))=string-length(.)-1">,</xsl:if></xsl:if>
<xsl:if test="contains(.,'R')">Thursday<xsl:if test="string-length(substring-before(.,'R'))=string-length(.)-1">,</xsl:if></xsl:if>
<xsl:if test="contains(.,'F')">Friday<xsl:if test="string-length(substring-before(.,'F'))=string-length(.)-1">,</xsl:if></xsl:if>
<xsl:if test="contains(.,'S')">Saturday<xsl:if test="string-length(substring-before(.,'S'))=string-length(.)-1">,</xsl:if></xsl:if>
<xsl:if test="contains(.,'U')">Sunday<xsl:if test="string-length(substring-before(.,'U'))=string-length(.)-1">,</xsl:if></xsl:if>
</long-days>
</xsl:template>
The currently accepted solution always displays the long days names in chronological order and in addition, it doesn't display repeating (with same code) days.
Suppose we have the following XML document:
<days>STMSU</days>
I. This XSLT 1.0 transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my" exclude-result-prefixes="my" >
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<my:days>
<M>Monday</M>
<T>Tuesday</T>
<W>Wednesday</W>
<R>Thursday</R>
<F>Friday</F>
<S>Saturday</S>
<U>Sunday</U>
</my:days>
<xsl:key name="kLongByShort" match="my:days/*"
use="name()"/>
<xsl:variable name="vstylesheet"
select="document('')"/>
<xsl:template match="days">
<long-days>
<xsl:call-template name="expand"/>
</long-days>
</xsl:template>
<xsl:template name="expand">
<xsl:param name="pcodeString" select="."/>
<xsl:if test="$pcodeString">
<xsl:variable name="vchar" select=
"substring($pcodeString,1,1)"/>
<xsl:for-each select="$vstylesheet">
<xsl:value-of select=
"concat(key('kLongByShort',$vchar),
substring(',',1,string-length($pcodeString)-1)
)
"/>
</xsl:for-each>
<xsl:call-template name="expand">
<xsl:with-param name="pcodeString" select=
"substring($pcodeString,2)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on the above document, produces the wanted, correct result:
<long-days>Saturday,Tuesday,Monday,Saturday,Sunday</long-days>
II. This XSLT 2.0 transformation:
<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 omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vshortCodes" as="xs:integer+"
select="string-to-codepoints('MTWRFSU')"/>
<xsl:variable name="vlongDays" as="xs:string+"
select="'Monday','Tuesday','Wenesday','Thursday',
'Friday','Saturday','Sunday'
"/>
<xsl:template match="days">
<long-days>
<xsl:for-each select="string-to-codepoints(.)">
<xsl:value-of separator="" select=
"for $pos in position() ne last()
return
($vlongDays[index-of($vshortCodes,current())],
','[$pos])
"/>
</xsl:for-each>
</long-days>
</xsl:template>
</xsl:stylesheet>
when applied on the same XML document:
<days>STMSU</days>
produce the wanted, correct result:
<long-days>Saturday,Tuesday,Monday,Saturday,Sunday</long-days>