XPath 2.0 (and 3.0) has convenient aggregate functions sum() and avg(), but nothing that returns the product of a sequence of numeric atomic values.
Although implementing such a function is trivial in a language that allows assignment statements, it seems not to be that easy in XSLT. The only way I've found to get an aggregate product is to use recursion:
<xsl:function name="fn:products">
<xsl:param name="input" />
<xsl:param name="total" />
<xsl:if test="exists($input)">
<xsl:variable name="x" select="$input[1] * $total"/>
<xsl:sequence select="$x"/>
<xsl:sequence select="fn:products(subsequence($input,2),$x)"/>
</xsl:if>
</xsl:function>
But the above is actually my adaptation of a function that appears on p.994 of Michael Kay's book XSLT 2.0 and XPath 2.0 4th Edition. The original function outputs a sequence of running balances by recursively adding a sequence of numbers.
My version just multiplies the numbers, instead of adding them. The result is then a sequence of "running products". For example, fn:products((4, 0.75, 10, 0.7), 1) returns the sequence 4, 3, 30, 21.
Since I'm only interested in the product of all numbers, I just take the last item of the output with a filter expression: fn:products((4, 0.75, 10, 0.7), 1)[last()].
It works. But just for the sake of making good code, I wonder if there is a way to make the function to just directly return the last item as the aggregate product (i.e. just get 21, in the example above).
I tried a couple of things, but they just broke the function. Is there a way to achieve this? Is it possible also to implement it in a better, leaner way (the original function was not meant to return a singleton sequence)?
If you use one of the fancy processors, you can use dyn:evaluate.
<?xml version="1.0" encoding="UTF-8"?>
<test>
<num>4</num>
<num>0.75</num>
<num>10</num>
<num>0.7</num>
</test>
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dyn="http://exslt.org/dyn/dyn.xml"
extension-element-prefixes="dyn"
>
<xsl:template match="/">
<xsl:variable name="run">
<xsl:for-each select="test/num"><xsl:value-of select="."/><xsl:if test="position() != last()"> * </xsl:if></xsl:for-each>
</xsl:variable>
<xsl:value-of select="dyn:evaluate(string($run))"/>
</xsl:template >
</xsl:stylesheet>
Related
I have the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<XmlTest>
<Pictures attr="Pic1">Picture 1</Pictures>
<Pictures attr="Pic2">Picture 2</Pictures>
<Pictures attr="Pic3">Picture 3</Pictures>
</XmlTest>
While this XSL does what is expected (output the attr of the first picture):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/XmlTest">
<xsl:variable name="FirstPicture" select="Pictures[1]">
</xsl:variable>
<xsl:value-of select="$FirstPicture/#attr"/>
</xsl:template>
</xsl:stylesheet>
It seems to be not possible to do the same inside the variable declaration using xsl:copy-of:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:template match="/XmlTest">
<xsl:variable name="FirstPicture">
<xsl:copy-of select="Pictures[1]"/>
</xsl:variable>
<xsl:value-of select="$FirstPicture/#attr"/>
</xsl:template>
</xsl:stylesheet>
Curious:
If I just select "$FirstPicture" instead of "$FirstPicture/#attr" in the second example, it outputs the text node of Picture 1 as expected...
Before you all suggest me to rewrite the code:
This is just a simplified test, my real aim is to use a named template to select a node into the variable FirstPicture and reuse it for further selections.
I hope someone could help me to understand the behavior or could suggest me a proper way to select a node with code which could be easily reused (the decission which node is the first one is complex in my real application). Thanks.
Edit (thanks to Martin Honnen):
This is my working solution example (which additionally uses a seperate template to select the requested picture node), using the MS XSLT processor:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
version="1.0">
<xsl:template match="/XmlTest">
<xsl:variable name="FirstPictureResultTreeFragment">
<xsl:call-template name="SelectFirstPicture">
<xsl:with-param name="Pictures" select="Pictures" />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="FirstPicture" select="msxsl:node-set($FirstPictureResultTreeFragment)/*"/>
<xsl:value-of select="$FirstPicture/#attr"/>
<!-- further operations on the $FirstPicture node -->
</xsl:template>
<xsl:template name="SelectFirstPicture">
<xsl:param name="Pictures"/>
<xsl:copy-of select="$Pictures[1]"/>
</xsl:template>
</xsl:stylesheet>
Not nice, that it is in XSLT 1.0 not possible to output a node directly from a template, but with the extra variable it is at least not impossible.
Well with an XSLT 1.0 processor if you do
<xsl:variable name="FirstPicture">
<xsl:copy-of select="Pictures[1]"/>
</xsl:variable>
the variable is a result tree fragment and all you can do with that in pure XSLT 1.0 is output it with copy-of (or value-of). If you want to apply XPath you first need to convert the result tree fragment into a node set, most XSLT 1.0 processors support an extension function for that so try
<xsl:variable name="FirstPictureRtf">
<xsl:copy-of select="Pictures[1]"/>
</xsl:variable>
<xsl:variable name="FirstPicture" select="exsl:node-set(FirstPictureRtf)/Pictures/#attr">
where you define xmlns:exsl="http://exslt.org/common" in your stylesheet.
Note that you will need to check whether your XSLT 1.0 processor supports the EXSLT extension function or a similar one in another namespace (as for instance the various MSXML versions do).
using pure XSLT 1.0, how can I conditionally assign the node. I am trying something like this but it's not working.
<xsl:variable name="topcall" select="//topcall"/>
<xsl:variable name="focusedcall" select="//focusedcall" />
<xsl:variable name="firstcall" select="$topcall | $focusedcall"/>
For variable firstcall, I am doing the conditional node selection. if there is a topcall then assign it to firstcall, othersie assign firstcall to the focusedcall.
This should work:
<xsl:variable name="firstcall" select="$topcall[$topcall] |
$focusedcall[not($topcall)]" />
In other words, select $topcall if $topcall nodeset is non-empty; $focusedcall if $topcall nodeset is empty.
Re-Update regarding "it can be 5-6 nodes":
Given that there may be 5-6 alternatives, i.e. 3-4 more besides $topcall and $focusedcall...
The easiest solution is to use <xsl:choose>:
<xsl:variable name="firstcall">
<xsl:choose>
<xsl:when test="$topcall"> <xsl:copy-of select="$topcall" /></xsl:when>
<xsl:when test="$focusedcall"><xsl:copy-of select="$focusedcall" /></xsl:when>
<xsl:when test="$thiscall"> <xsl:copy-of select="$thiscall" /></xsl:when>
<xsl:otherwise> <xsl:copy-of select="$thatcall" /></xsl:otherwise>
</xsl:choose>
</xsl:variable>
However, in XSLT 1.0, this will convert the output of the chosen result to a result tree fragment (RTF: basically, a frozen XML subtree). After that, you won't be able to use any significant XPath expressions on $firstcall to select things from it. If you need to do XPath selections on $firstcall later, e.g. select="$firstcall[1]", you then have a few options...
Put those selections into the <xsl:when> or <xsl:otherwise> so that they happen before the data gets converted to an RTF. Or,
Consider the node-set() extension, which converts an RTF to a nodeset, so you can do normal XPath selections from it. This extension is available in most XSLT processors but not all. Or,
Consider using XSLT 2.0, where RTFs are not an issue at all. In fact, in XPath 2.0 you can put normal if/then/else conditionals inside the XPath expression if you want to.
Implement it in XPath 1.0, using nested predicates like
:
select="$topcall[$topcall] |
($focusedcall[$focusedcall] | $thiscall[not($focusedcall)])[not($topcall)]"
and keep on nesting as deep as necessary. In other words, here I took the XPath expression for 2 alternatives above, and replaced $focusedcall with
($focusedcall[$focusedcall] | $thiscall[not($focusedcall)])
The next iteration, you would replace $thiscall with
($thiscall[$thiscall] | $thatcall[not($thiscall)])
etc.
Of course this becomes hard to read, and error-prone, so I would not choose this option unless the others aren't feasible.
Does <xsl:variable name="firstcall" select="($topcall | $focusedcall)[1]"/> do what you want? That is usually the way to take the first node in document order of different types of nodes.
I. XSLT 1.0 Solution This short (30 lines), simple and parameterized transformation works with any number of node types/names:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pRatedCalls">
<call type="topcall"/>
<call type="focusedcall"/>
<call type="normalcall"/>
</xsl:param>
<xsl:variable name="vRatedCalls" select=
"document('')/*/xsl:param[#name='pRatedCalls']/*"/>
<xsl:variable name="vDoc" select="/"/>
<xsl:variable name="vpresentCallNames">
<xsl:for-each select="$vRatedCalls">
<xsl:value-of select=
"name($vDoc//*[name()=current()/#type][1])"/>
<xsl:text> </xsl:text>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<xsl:copy-of select=
"//*[name()
=
substring-before(normalize-space($vpresentCallNames),' ')]"/>
</xsl:template>
</xsl:stylesheet>
When applied to this XML document (do note the document order doesn't coincide with the specified priorities in the pRatedCalls parameter):
<t>
<normalcall/>
<focusedcall/>
<topcall/>
</t>
produces exactly the wanted, correct result:
<topcall/>
when the same transformation is applied to the following XML document:
<t>
<normalcall/>
<focusedcall/>
</t>
again the wanted and correct result is produced:
<focusedcall/>
Explanation:
The names of the nodes that are to be searched for (as many as needed and in order of priority) are specified by the global (typically externally specified) parameter named $pRatedCalls.
Within the body of the variable $vpresentCallNames we generate a space-separated list of names of elements that are both specified as a value of the type attribute of a call elementin the$pRatedCalls` parameter and also are names of elements in the XML document.
Finally, we determine the first such name in this space-separated list and select all elements in the document, that have this name.
II. XSLT 2.0 solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pRatedCalls" select=
"'topcall', 'focusedcall', 'normalcall'"/>
<xsl:template match="/">
<xsl:sequence select=
"//*
[name()=$pRatedCalls
[. = current()//*/name()]
[1]
]"/>
</xsl:template>
</xsl:stylesheet>
hi all i have written a logic based on a requirement concact more than two data at a time in my xslt code but i m not reaching my expected output can any one give some suggestions
here is my xml
<Swift>
<block4>
<tag>
<name>50K</name>
<value>
0101/0457887750
SAMAROCA
MENENDEZ Y PELAYO
</value>
</tag>
</block4>
</Swift>
i have written an xslt here :
<xsl:template match="swift/message/block4/tag [name='50K']">
<xsl:variable name ="del50k" select ="(translate(substring-after(value,'
'),'
','~'))"/>
<xsl:value-of select="concat(substring(value, 1, 5), ',',substring(substring-before(value,'
'),6), ',',$del50k)" />
</xsl:template>
is that way doing is correct or not ? can any one help
EXPECTED OUTPUT:
0101/,0457887750,SAMAROCA~MENENDEZ Y PELAYO
I'm giving you a full working example based on your input. A few notes:
Use normalize-space() and split the string by space.
Just play with substring-before and substring-after.
make sure to use xsl:strip-space.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output omit-xml-declaration="yes" method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="space" select="' '"/>
<xsl:template match="block4/tag[name='50K']">
<xsl:variable name="value" select="normalize-space(value)"/>
<xsl:variable name="code" select="substring-before($value,$space)"/>
<xsl:variable name="string1" select="concat(
substring-before($code,'/'),
'/,',
substring-after($code,'/'))"/>
<xsl:variable name="string2" select="substring-before(
substring-after($value,$space),
$space)"/>
<xsl:variable name="string3" select="substring-after(
substring-after($value,$space),
$space)"/>
<xsl:value-of select="concat($string1,',',$string2,'~',$string3)"/>
</xsl:template>
<xsl:template match="name|value"/>
</xsl:stylesheet>
Your biggest problem is that value is you context node (defined in your template's match attribute), but you're referring to value in your XPath. This will look for a value node within the value node, which is obviously wrong.
In your <xsl:variable> and <xsl:value-of> statements, change refences to value to ., to refer to the current node instead.
I think that's probably not the only issue, but given that your template isn't going to match anything in that document anyway, it's difficult to derive where else it could be going wrong. One possible additional problem is that your substring-before(value,'
') predicate within your <xsl:value-of> isn't going to return anything with the formatting given, as there's a newline before the 0101/etc... Now I think about it, that's also going to be issue in the substring-after in the previous line. That's very dependent on how it's actually formatted though, but from what you've given here, it is a problem.
I was wondering whether it's possible to sort some elements first and store them (already sorted) in a variable. I would need to refer to them thought XSLT that's why I'd like to store them in a variable.
I was trying to do the following, but it doesn't seem to work
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:variable name="deposits">
<xsl:for-each select="/BookingCostings/MultiDeposits">
<xsl:sort select="substring(#DepositDate, 1, 4)" />
<xsl:sort select="substring(#DepositDate, 6, 2)" />
<xsl:sort select="substring(#DepositDate, 9, 2)" />
</xsl:for-each>
</xsl:variable>
I was trying to sort the elements by #DepositDate in the format 'yyyy-mm-dd' and store them all in the $deposits variable. So that later, I could access them using $deposits[1].
I would appreciate any help and tips!
Thanks a lot!
Using XSLT version 2.0 you could use perform-sort and tell that your variable is of type of a sequence of MultiDeposits using the as keyword (as="element(MultiDeposits)+")
Since your data is already as yyyy-mm-dd you can avoid to use the subtring to get each part of the date and use the sort directly on the field
with this sample xml:
<?xml version="1.0" encoding="ISO-8859-1"?>
<BookingCostings>
<MultiDeposits depositDate="2001-10-09">1</MultiDeposits>
<MultiDeposits depositDate="1999-10-09">2</MultiDeposits>
<MultiDeposits depositDate="2010-08-09">3</MultiDeposits>
<MultiDeposits depositDate="2010-07-09">4</MultiDeposits>
<MultiDeposits depositDate="1998-01-01">5</MultiDeposits>
</BookingCostings>
and using the XSLT version 2.0 sheet:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<xsl:variable name="deposits" as="element(MultiDeposits)+">
<xsl:perform-sort select="BookingCostings/MultiDeposits">
<xsl:sort select="#depositDate"/>
</xsl:perform-sort>
</xsl:variable>
first date:<xsl:value-of select="$deposits[1]/#depositDate"/>,
last date:<xsl:value-of select="$deposits[last()]/#depositDate"/>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
the ouput will be:
first date:1998-01-01, last date:2010-08-09
Firstly, in your variable declaration, you do need to do something to create new nodes. Strictly speaking, you are not sorting them, but just reading through them in a given order. I think you need to add some sort of xsl:copy command.
<xsl:variable name="deposits">
<xsl:for-each select="/BookingCostings/MultiDeposits">
<xsl:sort select="substring(#DepositDate, 1, 4)" />
<xsl:sort select="substring(#DepositDate, 6, 2)" />
<xsl:sort select="substring(#DepositDate, 9, 2)" />
<xsl:copy-of select=".|#*" />
</xsl:for-each>
</xsl:variable>
What this creates is a 'node-set', but to access it you will need to make use of an extension function in XSLT. Which one you use depends on the XSLT processor you are using. In the example I am about to give, I am using the Microsoft one.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ms="urn:schemas-microsoft-com:xslt" version="1.0">
Then, to access the nodes in your variable, you can do something like this
<xsl:value-of select="ms:node-set($deposits)/MultiDeposits[1]/#DepositDate" />
Here is a good article to read up on node-sets
Xml.com article on Node-Sets
Guess (don't have dev env to hand):
Add
<xsl:value-of select="." />
Before the closing </xsl:for-each>
How can find max of three numbers in XSL ?
More Information : I have three numbers say 1.0, 2.0, 5.0....I don't have any nodes set... I would like to find maximum of 1.0, 2.0 and 5.0.....
Example :
AIM : TO know which type of node <MYNODE>, <DOC>, <PIC>
is having maximum count and whant's the count number ?
<ROOT>
<MYNODES>
<MYNODE>A</MYNODE>
<MYNODE>B</MYNODE>
<MYNODE>C</MYNODE>
<MYNODE>D</MYNODE>
</MYNODES>
<DOCS>
<DOC>1</DOC>
<DOC>2</DOC>
<DOC>3</DOC>
</DOC>
<PICS>
<PIC>a.jpeg</PIC>
<PIC>b.jpeg</PIC>
<PIC>c.jpeg</PIC>
<PIC>d.jpeg</PIC>
<PIC>e.jpeg</PIC>
</PICS>
</ROOT>
With your input XML, you would find the maximum count you are looking for like this:
<xsl:variable name="vMaxChildren">
<xsl:for-each select="/ROOT/*">
<xsl:sort select="count(*)" data-type="number" order="descending" />
<xsl:if test="position() = 1">
<xsl:value-of select="concat(name(), ': ', count(*))" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="$vMaxChildren" />
Which would produce:
PICS: 5
This question is incorrectly formulated and the provided "XML document' is not well-formed!
Do note that it is generally meaningless to ask about the maximum of a set of numbers. There can be more than one number with the highest value. Therefore, the solutions below show just the first item with the maximum value.
This is one possible XSLT 1.0 solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="vNameMaxCount">
<xsl:for-each select="*/*">
<xsl:sort select="count(*)" data-type="number"
order="descending"/>
<xsl:if test="position() = 1">
<xsl:value-of select="concat(name(),'+', count(*))"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
One element with maximum children is: <xsl:text/>
<xsl:value-of select="substring-before($vNameMaxCount, '+')"/>
Maximum number of children: <xsl:text/>
<xsl:value-of select="substring-after($vNameMaxCount, '+')"/>
</xsl:template>
</xsl:stylesheet>
when the above transformation is applied on the following XML document (produced from the one provided after spending 10 minutes to make it well-formed!):
<ROOT>
<MYNODES>
<MYNODE>A</MYNODE>
<MYNODE>B</MYNODE>
<MYNODE>C</MYNODE>
<MYNODE>D</MYNODE>
</MYNODES>
<DOCS>
<DOC>1</DOC>
<DOC>2</DOC>
<DOC>3</DOC>
</DOCS>
<PICS>
<PIC>a.jpeg</PIC>
<PIC>b.jpeg</PIC>
<PIC>c.jpeg</PIC>
<PIC>d.jpeg</PIC>
<PIC>e.jpeg</PIC>
</PICS>
</ROOT>
the wanted result is produced
One element with maximum children is: PICS
Maximum number of children: 5
An XSLT 2.0 solution (actually just an XPath 2.0 soulution):
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:sequence select=
"for $vmaxChildrein in max(/*/*/count(*)),
$vmaxNode in */*[count(*) = $vmaxChildrein][1]
return
(name($vmaxNode),
'has the max no of children:',
$vmaxChildrein
)
"/>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the above document, the wanted result is produced:
PICS has the max no of children: 5
For finding the maximum of more tricky properties that cannot be immediately expressed as an XPath expression and used in <xsl:sort>, do use the f:maximum() function of FXSL.
5.0 is the largest of the three numbers. Hardcode it and be done with it. :-)
Seriously though, you may wish to take another path. Logic like this is trivial in other languages, but can be a pain in XSLT. You should consider writing a simple extension for the XSLT engine rather than messing around trying to make XSLT do what you want it to.