idiomatic alternative to choose -> test -> value-of (XSLT 1.0) - xslt

In the work I do I seem to see a lot of code liek this..
<xsl:choose>
<xsl:when test="long_xpath_to_optional/#value1">
<xsl:value-of select="long_xpath_to_optional/#value"/>
</xsl:when>
<xsl:when test="another_long_xpath_to_optional/#value">
<xsl:value-of select="another_long_xpath_to_optional/#value"/>
</xsl:when>
<etc>
</etc>
<otherwise>
<xsl:value-of select="default_long_xpath_to_value"/>
</otherwise>
</xsl:choose>
its very long and very repetitive.
When I'm were working in some other (psuedo) language I would go
let values = concat(list(long_xpath_to_optional_value),list(another_long_xpath_to_optional_value))
let answer = tryhead(values,default_long_xpath_to_value)
i.e. create a list of values in priority order, and then take the head.
I only evaluate each path once
how would you do something similar in XSLT 1.0 (we can use node-sets).

I was wondering if you can create a node-set somehow
You can - but it's not going to be any shorter:
<xsl:variable name="values">
<xsl:apply-templates select="long_xpath_to_optional/#value" mode="values"/>
<xsl:apply-templates select="another_long_xpath_to_optional/#value" mode="values"/>
<xsl:apply-templates select="default_long_xpath_to_value/#value" mode="values"/>
</xsl:variable>
<xsl:value-of select="exsl:node-set($values)/value[1]" xmlns:exsl="http://exslt.org/common"/>
and then:
<xsl:template match="#value" mode="values">
<value>
<xsl:value-of select="."/>
</value>
</xsl:template>
But at least the repetition is eliminated.
Alternatively, you could do:
<xsl:template match="#value" mode="values">
<xsl:value-of select="."/>
<xsl:text>|</xsl:text>
</xsl:template>
and then:
<xsl:value-of select="substring-before($values, '|')"/>

To use variables you write
<xsl:variable name="value1" select="long_xpath_to_optional/#value1"/>
<xsl:variable name="value2" select="another_long_xpath_to_optional/#value"/>
<xsl:variable name="value3" select="default_long_xpath_to_value"/>
and then in XPath 2 or 3 all you would need is ($value1, $value2, $value3)[1] or head(($value1, $value2, $value3)) but in XSLT 1 with XPath 1 all you can write as a single expression is ($value1 | $value2 | $value3)[1] which sorts in document order so unless the document order is the same as your test order this wouldn't work to check the values; rather you would need to maintain the
<xsl:choose>
<xsl:when test="$value1">
<xsl:value-of select="$value1"/>
</xsl:when>
<xsl:when test="$value2">
<xsl:value-of select="$value2"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value3"/>
</xsl:otherwise>
</xsl:choose>
Of course in XPath 2 you wouldn't really need the variables and could use (long_xpath_to_optional/#value1, another_long_xpath_to_optional/#value, default_long_xpath_to_value)[1] as well directly.

Related

Passing node and field dynamically to xslt function

I am trying to make something to act as a null check decimal fields for use in xslt transformations. I was thinking I could also make similar for other data types as I need. I made this
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wd="urn:com.workday.report/CR_Question"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:this="urn:this-stylesheet"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:function name="this:NullCheckField" as="xsd:string">
<xsl:param name="node" as="xsd:string"/>
<xsl:param name="fieldName" as="xsd:string"/>
<xsl:variable name="variableName">
<xsl:choose>
<xsl:when test="not(exists(concat($node, '/', $fieldName)))">
<xsl:value-of select="format-number(0, '#.00')"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="x">
<xsl:value-of select="concat($node, '/', $fieldName)"/>
</xsl:variable>
<xsl:variable name="y">
<xsl:value-of select="$x"/>
</xsl:variable>
<xsl:value-of select="format-number(number($y), '#.00')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:value-of select="$variableName"/>
</xsl:function>
<xsl:output method="text" indent="yes"/>
<xsl:template match="wd:Report_Entry">
<xsl:value-of select="this:NullCheckField('wd:Report_Data/wd:Report_Entry','wd:deduction_amount')"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
and i tried calling it with this line
<wd:Report_Data xmlns:wd="urn:com.workday.report/CR_Question">
<wd:Report_Entry>
<wd:Employee_ID>1555111</wd:Employee_ID>
<wd:Check_Date>2022-04-28</wd:Check_Date>
<wd:NegDeductIndc>0</wd:NegDeductIndc>
<wd:Deduction_YTD>0</wd:Deduction_YTD>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:Employee_ID>1555222</wd:Employee_ID>
<wd:Check_Date>2022-04-28</wd:Check_Date>
<wd:NegDeductIndc>0</wd:NegDeductIndc>
<wd:Deduction_YTD>0</wd:Deduction_YTD>
</wd:Report_Entry>
</wd:Report_Data>
When I run this, the test should fail and use the hard coded 0 since the node is not there, but instead of dropping into
<xsl:when test="not(exists(concat($node, '/', $fieldName)))">
<xsl:value-of select="format-number(0, '#.00')"/>
</xsl:when>
It keeps dropping into the and failing out. I am not sure of two things.
The first is why is my exist check not working correctly and falling into the
Other question - am I able to pass the node names and concat them like this?
Edit: Updated function as of now
<!-- param type 1 = decimal -->
<!-- param type 2 = integer -->
<!-- param type 3 = string -->
<xsl:function name="this:NullCheckField" as="xsd:string">
<xsl:param name="node"/>
<xsl:param name="type" as="xsd:integer"/>
<xsl:variable name="variableName">
<xsl:choose>
<xsl:when test="not(exists($node))">
<xsl:choose>
<xsl:when test="$type = 1">
<xsl:value-of select="format-number(0, '#.00')"/>
</xsl:when>
<xsl:when test="$type = 2">
<xsl:value-of select="0"/>
</xsl:when>
<xsl:otherwise>
<xsl:text/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="x">
<xsl:value-of select="$node[0]"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="$type = 1">
<xsl:value-of select="format-number(number($x), '#.00')"/>
</xsl:when>
<xsl:when test="$type = 2">
<xsl:value-of select="number($x)"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$x"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:value-of select="$variableName"/>
</xsl:function>
The problem I am trying to resolve is my integration will not always produce the empty nodes. Sometimes there is a missing node and I have been doing checks like this
<xsl:choose>
<xsl:when test="not(exists(wd:DPDEEYDEDS))">
<xsl:value-of select="' '"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wd:DPDEEYDEDS"/>
</xsl:otherwise>
</xsl:choose>
Some of these are decimal, some are integer, some are spaces. I'm trying to do a dynamic null check. The function will be called, the field node evaluated, if it is not a missing or empty node then it formats the value, otherwise return a default 0|0.00|' '.
Unless you want to move to XSLT with xsl:evaluate support the whole idea of passing in strings with XPath fragments is nonsense.
If, in the context of template match="wd:Report_Entry", you want to pass that Report_Entry element to a function use . e.g. this:my-function(., 'deduction_amount'). The first function argument should be of type node() or element(), certainly you don't want to atomize the node to a string.
Then you could check e.g. test="$node/*[local-name() = $fieldName]" to check for the existence of a child element with local name $fieldName.
I don't see any need to work with element names as as strings, you can simply pass in e.g. this:my-function(wd:deduction_amount), with the type of the paramter being node()? or node()* and then do any exists check on that, if needed.
The whole construction of nested, untyped variables is unnecessarily complicated and inefficient.
I don't even see the need for the function, doing e.g. format-number((wd:deduction_amount, 0)[1], '#.00') inside the template suffices.

Check if a map is empty in xslt

I create a map variable and want to check if it has items.
<xsl:variable as="map(xs:string, xs:string)*" name="ancestorsMap">
<xsl:map>
<xsl:for-each select="$nodes">
<xsl:variable name="ancestor" select="(ancestor::node()[not(descendant-or-self::layer)])[1]/#xml:id"/>
<xsl:if test="exists($ancestor)">
<xsl:map-entry key="string(#xml:id)" select="string($ancestor)"/>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:variable name="check">
<xsl:choose>
<xsl:when test="empty($ancestorsMap)">
<xsl:value-of select="'NaN'"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$ancestorsMap"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
When it is empty, the oXygen variable panel shows:
Value type: map(*)1
Value: map{}
I tried fn:empty() so far
It looks like
test="map:size($ancestorsMap) = 0"
works fine. But maybe there are better approaches.

Populate a variable with a subtree

in a version="2.0" stylesheet:
the following code produces the correct output
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:value-of select="/t:Flow/t:FHeader/t:Producer/t:Repository" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj"/>
but this one does not
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:value-of select="/t:Flow/t:FHeader/t:Producer" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj/t:Repository"/>
How can I get the second code to run as expected ?
If needed, is there a solution in v3 ?
this code does not run either
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:copy-of select="/t:Flow/t:FHeader/t:Producer" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj/t:Repository"/>
relevant xml input
<Flow>
<FHeader>
<Producer>
<Repository>tests.com</Repository>
</Producer>
</FHeader>
</Flow>
You can simply select <xsl:variable name="obj" select="/t:Flow/t:FHeader/t:Producer/t:Repository[current()/t:ReferencedObjectType='Asset']"/>. Or, as Tim already commented, use xsl:copy-of, also taking into account that you then later on need e.g. $obj/t:Producer/t:Repository to select the right level.
Or learn about the as attribute and use e.g. <xsl:variable name="obj" as="element()*">...<xsl:copy-of select="/t:Flow/t:FHeader/t:Producer"/> ...</xsl:variable>, then you later on can use e.g. $obj/t:Repository.
There is also xsl:sequence to select input nodes instead of copying them, in particular with xsl:variable if you use the as attribute. This might consume less memory.
Furthermore XPath 2 and later have if (condition-expression) then expression else expression conditional expressions at the expression level so you might not need XSLT with xsl:choose/xsl:when but could use the <xsl:variable name="obj" select="if (t:ReferencedObjectType='Asset']) then /t:Flow/t:FHeader/t:Producer else if (...) then ... else ()"/>, that way you would select e.g. an input t:Producer element anyway and if you use the variable you can directly select the t:Repository child.

XSLT Optimizing xsl:choose and str:tokenize

I have been researching and haven't been able to find anything related to optimizing XSLT. Below is the snippet that I am working on and wanted to see if anything can be done to help with the xslt transformation.
<xsl:template match="a:OBR/*">
<xsl:choose>
<xsl:when test ="name() = 'OBR-10' and string-length(.) = 0">
<OBR-10>USER</OBR-10>
</xsl:when>
<xsl:when test ="name() = 'OBR-18'">
<OBR-18>
<xsl:value-of select ="//a:PV1/a:PV1-44"/>
</OBR-18>
</xsl:when>
<xsl:when test ="name() = 'OBR-19'">
<OBR-19>
<xsl:if test = "string-length(str:tokenize(../a:OBR-18,'^')[5]) > 0">
<xsl:value-of select ="str:tokenize(../a:OBR-18,'^')[5]"/>
</xsl:if>
</OBR-19>
</xsl:when>
<xsl:when test ="name() = 'OBR-33'">
<OBR-33>
<xsl:value-of select ="translate(../parent::a:ORC[1]/a:ORC-4,'^','~')"/>
</OBR-33>
</xsl:when>
<xsl:when test="name()='NTE'">
<NTE>
<xsl:apply-templates/>
</NTE>
</xsl:when>
<xsl:when test="name()='DG1'"/>
<!--<DG1>
<xsl:apply-templates/>
</DG1>
</xsl:when>-->
<xsl:when test="name()='OBX'">
<OBX>
<xsl:apply-templates/>
</OBX>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
I would suggest to write code like
<xsl:template match="a:OBR/*">
<xsl:choose>
<xsl:when test ="name() = 'OBR-10' and string-length(.) = 0">
<OBR-10>USER</OBR-10>
</xsl:when>
as
<xsl:template match="a:OBR/OBR-10[string-length() = 0]">
<xsl:copy>USER</xsl:copy>
</xsl:template>
or perhaps
<xsl:template match="a:OBR/OBR-10[. = '']">
<xsl:copy>USER</xsl:copy>
</xsl:template>
that is, to write templates that match each element by its name and if needed a predicate/condition instead of that odd approach to match on * and then test the name. I don't see that necessarily as an optimization (you would have to measure with a particular implementation) but as a clear and modular coding style.
The
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
would then be code as
<xsl:template match="a:OBR/*">
<xsl:copy-of select="."/>
</xsl:template>
or probably already covered by an identity transformation template set up as the starting point to initiate and keep up the processing.
You would have to show the namespaces in the input document and the XSLT to allow a precise suggestion in terms of namespaces (could be that you want/need xsl:template match="a:OBR/a:OBR-10[string-length() = 0]").

How to store the current path in xsl?

I would like to store the path of the current node so I can reused it in an expression in XSLT. Is it possible?
<!-- . into $path? -->
<xsl:value-of select="$path" />
Hi, I would like to store the path of
the current node so I can reused it in
an expression in XSLT. Is it possible?
It is possible for any given node to construct an XPath expression that, when evaluated, selects exactly this node. In fact more than one XPath expression exists that selects the same node.
See this answer for the exact XSLT code that constructs such an XPath expression.
The problem is that this XPath expression cannot be evaluated during the same transformation in XSLT 1.0 or XSLT 2.0, unless the EXSLT extension function dyn:evaluate is used (and very few XSLT 1.0 processors implement dyn:evaluate() ).
What you want can be achieved in an easier way in XSLT using the <xsl:variable> instruction:
<xsl:variable name="theNode" select="."/>
This variable can be referenced anywhere in its scope as $theNode, and can be passed as parameter when applying or calling templates.
No, this is not possible with vanilla XSLT 1.0. There is no easy way to retrieve an XPath expression string for a given node, and there is definitely no way to evaluate a string that looks like XPath as if it was XPath.
There are extensions that support dynamic evaluation of XPath expressions, but these are not compatible with every XSLT processor.
In any case, if you provide more detail around what you are actually trying to do, there might be another way to do it.
As #Dimitre and #Tomalak have point out, I don't think it has some value in the same transformation to obtain a string representing an XPath expression for a given node, and then select the node "parsing" such string. I could see some value in performing those operations in different transformations.
Besides that, this stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:for-each select=".|//node()|//#*">
<xsl:variable name="vPath">
<xsl:apply-templates select="." mode="getPath"/>
</xsl:variable>
<xsl:value-of select="concat($vPath,'
')"/>
<xsl:call-template name="select">
<xsl:with-param name="pPath" select="$vPath"/>
</xsl:call-template>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template match="/|node()|#*" mode="getPath" name="getPath">
<xsl:apply-templates select="parent::*" mode="getPath"/>
<xsl:text>/</xsl:text>
<xsl:choose>
<xsl:when test="self::*">
<xsl:value-of select="concat(name(),'[',
count(preceding-sibling::*
[name() =
name(current())]) + 1,
']')"/>
</xsl:when>
<xsl:when test="count(.|../#*)=count(../#*)">
<xsl:value-of select="concat('#',name())"/>
</xsl:when>
<xsl:when test="self::text()">
<xsl:value-of
select="concat('text()[',
count(preceding-sibling::text()) + 1,
']')"/>
</xsl:when>
<xsl:when test="self::comment()">
<xsl:value-of
select="concat('comment()[',
count(preceding-sibling::comment()) + 1,
']')"/>
</xsl:when>
<xsl:when test="self::processing-instruction()">
<xsl:value-of
select="concat('processing-instruction()[',
count(preceding-sibling::
processing-instruction()) + 1,
']')"/>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="select">
<xsl:param name="pPath"/>
<xsl:param name="pContext" select="/"/>
<xsl:param name="pInstruction" select="'value-of'"/>
<xsl:variable name="vPosition"
select="number(
substring-before(
substring-after($pPath,
'['),
']'))"/>
<xsl:variable name="vTest"
select="substring-before(
substring-after($pPath,
'/'),
'[')"/>
<xsl:variable name="vPath" select="substring-after($pPath,']')"/>
<xsl:choose>
<xsl:when test="$vPath">
<xsl:call-template name="select">
<xsl:with-param name="pPath" select="$vPath"/>
<xsl:with-param name="pContext"
select="$pContext/*[name()=$vTest]
[$vPosition]"/>
<xsl:with-param name="pInstruction"
select="$pInstruction"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vContext"
select="$pContext/node()
[self::*[name()=$vTest]|
self::comment()[$vTest='comment()']|
self::text()[$vTest='text()']|
self::processing-instruction()
[$vTest =
'processing-instruction()']]
[$vPosition]|
$pContext[$pPath='/']|
$pContext/#*[name() =
substring($pPath,3)]
[not($vTest)]"/>
<xsl:choose>
<xsl:when test="$pInstruction='value-of'">
<xsl:value-of select="$vContext"/>
</xsl:when>
<xsl:when test="$pInstruction='copy-of'">
<xsl:copy-of select="$vContext"/>
</xsl:when>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
With this input:
<?somePI pseudoAttributes?>
<root>
<!-- This is a comment -->
<node attribute="Value">text</node>
</root>
Output:
/
text
/processing-instruction()[1]
pseudoAttributes
/root[1]
text
/root[1]/comment()[1]
This is a comment
/root[1]/node[1]
text
/root[1]/node[1]/#attribute
Value
/root[1]/node[1]/text()[1]
text