Extend attrbiute instead of setting it? - xslt

I'm building a quite complex XSLT in order to generate some HTML markup.
On one my goal is to "extend" the class attribute of the generated markup using some templates.
Unfortunately, it does not works, because the XSLT tag <xsl:attribute> is only able to "set" attribute. Not to manipulate existing ones.
When I try, the original attribute is wiped.
Here is a small reproduction:
XML:
<node>
<item value="1" type="abc"/>
<item value="20" type="zxy"/>
</node>
XSL:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="utf-8" indent="no"/>
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="/node/item">
<p class="{#type}">
<xsl:call-template name='rule1' />
<xsl:call-template name='rule2' />
</p>
</xsl:template>
<xsl:template name='rule1'>
<xsl:attribute name='class'>
<xsl:choose>
<xsl:when test="#value mod 2 = 0">alpha</xsl:when>
<xsl:otherwise>omega</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
</xsl:template>
<xsl:template name='rule2'>
<xsl:attribute name='class'>
<xsl:choose>
<xsl:when test="#value mod 10 = 0">beta</xsl:when>
<xsl:otherwise>gamma</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
I would like to output:
<?xml version="1.0" encoding="utf-8"?>
<p class="abc omega"/>
<p class="zxy beta alpha"/>
But it outputs
<?xml version="1.0" encoding="utf-8"?>
<p class="omega"/>
<p class="beta"/>
is it possible to preserve the original attribute, or retrieve it within the utility template to reuse it?

It would seem that this answer would only satisfy a single deep, non-reiterated-upon class.
Include this at the start of the template you are calling:
<xsl:variable name="type" select="#type"/>
And then when it comes to updating the attribute value, try:
<xsl:attribute select="class">
<xsl:choose>
<xsl:when test="#value mod 2 = 0">
<xsl:value-of select="concat($type, ' alpha')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat($type, ' omega')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
Again, I'm not super comfortable with XSLT but I did just try something similar in a test recently and it worked a charm. Hope it helps.
EDIT: I'm not sure but you might have to put the escaped value for a "space" in the concat function, I forget.

An approaching solution could be (thanks to #aleski suggestions)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="utf-8" indent="no"/>
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="/node/item">
<p>
<xsl:attribute name='class'>
<xsl:value-of select="#type" />
<xsl:call-template name='rule1' />
<xsl:call-template name='rule2' />
</xsl:attribute>
</p>
</xsl:template>
<xsl:template name='rule1'>
<xsl:choose>
<xsl:when test="#value mod 2 = 0"><xsl:value-of select="' alpha'" /></xsl:when>
<xsl:otherwise><xsl:value-of select="' omega'" /></xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name='rule2'>
<xsl:choose>
<xsl:when test="#value mod 10 = 0"><xsl:value-of select="' beta'" /></xsl:when>
<xsl:otherwise><xsl:value-of select="' gamma'" /></xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

Do you like obscure coding solutions? If so, you can re-write your solution as this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="utf-8" indent="no"/>
<xsl:template match="/node/item">
<p class="{#type} {substring('alphaomega', 1 + 5 * (#value mod 2 = 0), 5)} {normalize-space(substring('beta gamma', 1 + 5 * (#value mod 10 = 0), 5))}">
</p>
</xsl:template>
</xsl:stylesheet>
So, have three Attribute Value Templates in one attribute. If you take a look at one of them...
{substring('alphaomega', 1 + 5 * (#value mod 10 = 0), 5)}
This takes advantage of the fact that "true" evaluates to 1 in a numeric expression, and "false" evaluates to 0. So, when the expression is true, the first five characters of the string are returned. When it is false, the next five characters are.

Related

How to create a composite key for muenchian grouping

These are just three records of my client's xml, but it shows the problem:
<Export>
<Record>
<JournalX>01-252-5390</JournalX>
<PositionX></PositionX>
<DepartmentX></DepartmentX>
<Description><![CDATA[UK - Take The Cash]]></Description>
<Amount>116.66</Amount>
<PayDate>06/30/2022</PayDate>
<PPEndDate>06/30/2022</PPEndDate>
</Record>
<Record>
<JournalX>5200</JournalX>
<PositionX></PositionX>
<DepartmentX>262</DepartmentX>
<Description><![CDATA[UK - Salary]]></Description>
<Amount>3655.92</Amount>
<PayDate>06/30/2022</PayDate>
<PPEndDate>06/30/2022</PPEndDate>
</Record>
<Record>
<JournalX>5200</JournalX>
<PositionX>311</PositionX>
<DepartmentX>310</DepartmentX>
<Description><![CDATA[UK - Salary]]></Description>
<Amount>1424.99</Amount>
<PayDate>06/30/2022</PayDate>
<PPEndDate>06/30/2022</PPEndDate>
</Record>
</Export>
What needs to happen is to sum Amount by Description and AcctNo. As you can see there is not AcctNo in the XML. This is how I created an AcctNo variable:
<xsl:variable name="AcctNo">
<xsl:choose>
<xsl:when test="string-length(JournalX) > 4">
<xsl:value-of select="JournalX" />
</xsl:when>
<xsl:when test="string-length(PositionX) != 3 or PositionX = '' ">
<xsl:value-of select="concat('01-', DepartmentX, '-', JournalX)" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('01-', PositionX, '-', JournalX)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
Alternatively, I created an AcctNo function in the CData section, basically the same algorithm. But the problem with either of these, is that I have to create the Key outside the template definition and the variable is defined within the template, therefore not available to the variable.
An additional problem, at least I think it may be, is that they want the output to be csv.
Can anyone help me? Thank You so much, Greg
I have copied code I found on the web and tried to modify it which is how I found out about the limitation.
Any help would be really appreciated.
Using XSLT 3 with
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:output method="text" indent="yes" html-version="5"/>
<xsl:template match="/Export">
<xsl:for-each-group select="Record" composite="yes" group-by="Description, if (string-length(JournalX) > 4) then JournalX else if (string-length(PositionX) != 3 or PositionX = '') then concat('01-', DepartmentX, '-', JournalX) else concat('01-', PositionX, '-', JournalX)">
<xsl:value-of select="sum(current-group()/Amount)"/>
<xsl:text>
</xsl:text>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
I get
116.66
3655.92
1424.99
using XSLT 1.0 with a two step transformation I get the same result
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl"
version="1.0">
<xsl:output method="text" indent="yes" />
<xsl:key name="group-by-desc-and-acctno" match="Record" use="concat(Description, '|', AcctNo)"/>
<xsl:template match="/Export">
<xsl:variable name="records-with-added-acctno-rtf">
<xsl:apply-templates select="Record"/>
</xsl:variable>
<xsl:variable name="records-with-added-acctno" select="exsl:node-set($records-with-added-acctno-rtf)/Record"/>
<xsl:for-each select="$records-with-added-acctno[generate-id() = generate-id(key('group-by-desc-and-acctno', concat(Description, '|', AcctNo))[1])]">
<xsl:value-of select="sum(key('group-by-desc-and-acctno', concat(Description, '|', AcctNo))/Amount)"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template match="Record">
<xsl:copy>
<xsl:copy-of select="*"/>
<AcctNo>
<xsl:choose>
<xsl:when test="string-length(JournalX) > 4">
<xsl:value-of select="JournalX" />
</xsl:when>
<xsl:when test="string-length(PositionX) != 3 or PositionX = '' ">
<xsl:value-of select="concat('01-', DepartmentX, '-', JournalX)" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('01-', PositionX, '-', JournalX)"/>
</xsl:otherwise>
</xsl:choose>
</AcctNo>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

precalculating node-set via copy-of and accessing ancestors (XSLT 1.0)

I want to precalculate a subtree of nodes in an source XML, and the process them seperately (because I want the subset to be processed in different ways), and access some values from ancestors.
simple example
<numbers count="5">
<number value="1"/>
<number value="2"/>
<number value="3"/>
<number value="4"/>
<number value="5"/>
</numbers>
and lets say I have an xslt (MSXML) to extract the even nodes somehow
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<evens>
<xsl:for-each select="numbers/number">
<xsl:choose>
<xsl:when test="#value mod 2 = 0">
<even>
<xsl:attribute name="count">
<xsl:value-of select="../#count"/>
</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="#value"/>
</xsl:attribute>
</even>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</evens>
</xsl:template>
</xsl:stylesheet>
and we get..
<evens>
<even count="5" value="2" />
<even count="5" value="4" />
</evens>
nice...
but how can I seperate the filtering from the processing so something like...
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template name="calculateNodes">
<xsl:for-each select="numbers/number">
<xsl:choose>
<xsl:when test="#value mod 2 = 0">
<xsl:copy-of select="."/>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<xsl:template match="/">
<xsl:variable name="nodes">
<xsl:call-template name="calculateNodes"/>
</xsl:variable>
<evens>
<xsl:for-each select="msxsl:node-set($nodes)/number">
<even>
<xsl:attribute name="count">
<xsl:value-of select="../#count"/>
</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="#value"/>
</xsl:attribute>
</even>
</xsl:for-each>
</evens>
</xsl:template>
</xsl:stylesheet>
this gives.
<evens>
<even count="" value="2" />
<even count="" value="4" />
</evens>
so...the ancestors arent copied.
Is there an idiomatic way to get out of this?
A copied node exists on its own, outside of the original tree. In your example, the parent of number is the $nodes variable, which does not have any attributes.
Why don't you do simply:
<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="/numbers">
<xsl:variable name="nodes" select="number[#value mod 2 = 1]"/>
<evens>
<xsl:for-each select="$nodes">
<even count="{../#count}" value="{#value}"/>
</xsl:for-each>
</evens>
</xsl:template>
</xsl:stylesheet>
This way you have a variable containing a reference to the original nodes, instead of a copy. Then you also have access to the original parent. And the content of the variable is a node-set; you don't need to convert it.
this seems to work
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template name="calculateNodes">
<xsl:for-each select="numbers/number">
<xsl:choose>
<xsl:when test="#value mod 2 = 0">
<numberWrapper>
<xsl:attribute name="count">
<xsl:value-of select="../#count"/>
</xsl:attribute>
<xsl:copy-of select="."/>
</numberWrapper>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<xsl:template match="/">
<xsl:variable name="nodes">
<xsl:call-template name="calculateNodes"/>
</xsl:variable>
<evens>
<xsl:for-each select="msxsl:node-set($nodes)/numberWrapper">
<even>
<xsl:attribute name="count">
<xsl:value-of select="#count"/>
</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="number/#value"/>
</xsl:attribute>
</even>
</xsl:for-each>
</evens>
</xsl:template>
</xsl:stylesheet>

Issue with this xsl template to calculate max for node list

The below for-each loop is only picking up first value for X.. and not actually finding maximum could you please let me know the issue here.
<xsl:for-each select=".//X">
<xsl:choose>
<xsl:when test="position() = 1">
<xsl:value-of select="abc:set_variable($_XDOCTX, 'v1t1', abc:normalize_string($_XDOCTX, (.//.)[1]))"/>
</xsl:when>
<xsl:when test="abc:nOne(abc:normalize_string($_XDOCTX, (.//.)[1])) > number(abc:get_variable($_XDOCTX, 'v1t1'))">
<xsl:value-of select="abc:set_variable($_XDOCTX, 'v1t1', abc:normalize_string($_XDOCTX, (.//.)[1]))"/>
</xsl:when>
</xsl:choose>
</xsl:for-each>
To sort such large sequences of digits numerically, you can split them into substrings and sort by each substring individually - for example:
XML
<input>
<item>123456789.0123456780</item>
<item>123456789.0123456781</item>
<item>123456789.0123456782</item>
<item>123456789.0123456783</item>
<item>123456789.0123456784</item>
<item>123456789.0123456785</item>
<item>123456789.0123456786</item>
<item>123456789.0123456787</item>
<item>123456789.0123456788</item>
<item>123456789.0123456789</item>
<item>123456789.012344</item>
<item>123456789.012346</item>
</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:strip-space elements="*"/>
<xsl:template match="/input">
<output>
<xsl:for-each select="item">
<xsl:sort select="substring-before(., '.')" data-type="number" order="descending"/>
<xsl:sort select="concat('0.', substring-after(., '.'))" data-type="number" order="descending"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<output>
<item>123456789.012346</item>
<item>123456789.0123456789</item>
<item>123456789.0123456788</item>
<item>123456789.0123456787</item>
<item>123456789.0123456786</item>
<item>123456789.0123456785</item>
<item>123456789.0123456784</item>
<item>123456789.0123456783</item>
<item>123456789.0123456782</item>
<item>123456789.0123456781</item>
<item>123456789.0123456780</item>
<item>123456789.012344</item>
</output>
Note that the limitation is processor-dependent: I could not reproduce the problem using either Xalan or Saxon.

How to pattern match in XSLT and add values

As part of an XSLT, I need to add all the values of the "Duration" element and display the value. Now, the below XML is a part of the larger XML I'm working on. In the below XML, I need to match
a/TimesheetDuration/Day*/Duration, add the values and display them. I dont want to store all the values in variables and add them. Is there any other clean way of doing this?
<?xml version="1.0" ?>
<a>
<TimesheetDuration>
<Day1>
<BusinessDate>6/12/2013</BusinessDate>
<Duration>03:00</Duration>
</Day1>
<Day2>
<BusinessDate>6/13/2013</BusinessDate>
<Duration>04:00</Duration>
</Day2>
<Day3>
<BusinessDate>6/14/2013</BusinessDate>
<Duration>05:00</Duration>
</Day3>
</TimesheetDuration>
</a>
An XPath 2.0 solution, assuming the durations are in the form HH:MM, would be
sum(for $d in a//Duration
return xs:dayTimeDuration(replace($d, '(..):(..)', 'PT$1H$2M')))
In xslt 1.0 you could do it for example with following stylesheet
<?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" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<Durations>
<xsl:apply-templates select="a/TimesheetDuration/node()[starts-with(name(),'Day')]" />
<xsl:variable name="hours">
<xsl:call-template name="sumHours">
<xsl:with-param name="Day" select="a/TimesheetDuration/node()[starts-with(name(),'Day')][1]" />
</xsl:call-template>
</xsl:variable>
<SumOfHours>
<xsl:value-of select="$hours" />
</SumOfHours>
<!-- Sum of minutes would be calculated similarly -->
</Durations>
</xsl:template>
<xsl:template match="node()[starts-with(name(),'Day')]">
<xsl:copy-of select="Duration" />
</xsl:template>
<xsl:template name="sumHours">
<xsl:param name="tmpSum" select="0" />
<xsl:param name="Day" />
<xsl:variable name="newTmpSum" select="$tmpSum + substring-before($Day/Duration, ':')" />
<xsl:choose>
<xsl:when test="$Day/following-sibling::node()[starts-with(name(),'Day')]">
<xsl:call-template name="sumHours">
<xsl:with-param name="tmpSum" select="$newTmpSum" />
<xsl:with-param name="Day" select="$Day/following-sibling::node()[starts-with(name(),'Day')]" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$newTmpSum" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
It produces output
<?xml version="1.0" encoding="UTF-8"?>
<Durations>
<Duration>03:00</Duration>
<Duration>04:00</Duration>
<Duration>01:00</Duration>
<SumOfHours>8</SumOfHours>
</Durations>

How I can parse Pipeline sign from string in xslt

How I can parse pipeline sign from fields in xslt. e.g.
dummy1|dummy2|dummy3|dummy4
Regards,
Sarah
If you use an XSLT 2.0 processor you can use the tokenize function (http://www.w3.org/TR/xpath-functions/#func-tokenize) e.g. with an input of
<foo>dummy1|dummy2|dummy3|dummy4</foo>
you can match
<xsl:template match="foo">
<xsl:value-of select="tokenize(., '\|')"/>
</xsl:template>
to output dummy1 dummy2 dummy3 dummy4. If you use an XSLT 1.0 processor you can check whether it supports an extension function like http://www.exslt.org/str/functions/tokenize/ or you need to write a recursive, named template splitting up the input.
In xlst 1.0 I usually use recursive call of named template, e.g.
<?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" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:variable name="dummyVar" select="'dummy1|dummy2|dummy3|dummy4'" />
<xsl:variable name="delimiter" select="'|'" />
<xsl:template match="/">
<dummies>
<xsl:call-template name="parseDummy">
<xsl:with-param name="parsedString" select="$dummyVar" />
</xsl:call-template>
</dummies>
</xsl:template>
<xsl:template name="parseDummy">
<xsl:param name="parsedString" />
<xsl:choose>
<xsl:when test="contains($parsedString, $delimiter)">
<xsl:element name="{substring-before($parsedString, $delimiter)}" />
<xsl:call-template name="parseDummy">
<xsl:with-param name="parsedString" select="substring-after($parsedString, $delimiter)" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:element name="{$parsedString}" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
It's on you what you need to do with parsed values, in example I construct elements with names of dummies.
Be careful about context which is changing.