XSL fetch min/max of modified attributes into variables - xslt

I would like to find a "nicer" solution to get the minimum and maximum values of attributes and save them into accesible variables. I would love to get away from the for-each-loop too. How is that possible?
My XML:
<Rows>
<Entry value1="16,423" value2="18,123" />
<Entry value1="423" value2="11,588" />
<Entry value1="1,168" value2="521" />
</Rows>
And my XSL:
<xsl:for-each select="Rows/Entry/#value1|Rows/Entry/#value2">
<xsl:sort select="." data-type="number" />
<xsl:choose>
<xsl:when test="position() = 1">
<xsl:variable name="min" select="format-number(translate(.,',',''),'#')" />
</xsl:when>
<xsl:when test="position() = last()">
<xsl:variable name="max" select="format-number(translate(.,',',''),'#')" />
</xsl:when>
</xsl:choose>
</xsl:for-each>
The desired output should be $min=423 and $max=18123 as numbers and accesible outside the for-each-loop

Well there is XSLT 2.0 since 2007 (implemented by XSLT processors like Saxon 9, AltovaXML, XmlPrime) where you can simply do (assuming you have the declaration xmlns:xs="http://www.w3.org/2001/XMLSchema" on your xsl:stylesheet element):
<xsl:variable name="min" select="min(Rows/Entry/(#value1, #value2)/xs:decimal(translate(., ',', ''))"/>
<xsl:variable name="max" select="max(Rows/Entry/(#value1, #value2)/xs:decimal(translate(., ',', ''))"/>
If you really want to store a formatted string in a variable you can of course do that as well with e.g.
<xsl:variable name="min" select="format-number(min(Rows/Entry/(#value1, #value2)/xs:decimal(translate(., ',', '')), '#')"/>
<xsl:variable name="max" select="format-number(max(Rows/Entry/(#value1, #value2)/xs:decimal(translate(., ',', '')), '#')"/>
As for XSLT 1.0, there I think the sorting with for-each is the right approach but you would need to pull the xsl:variable outside the for-each e.g.
<xsl:variable name="min">
<xsl:for-each select="Rows/Entry/#value1|Rows/Entry/#value2">
<xsl:sort select="translate(., ',', '')" data-type="number"/>
<xsl:if test="position() = 1">
<xsl:value-of select="format-number(., '#')"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="max">
<xsl:for-each select="Rows/Entry/#value1|Rows/Entry/#value2">
<xsl:sort select="translate(., ',', '')" data-type="number"/>
<xsl:if test="position() = last()">
<xsl:value-of select="format-number(.,'#')" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
As an alternative you could replace the for-each with apply-templates and then write a template matching #value1 | #value2 but while I think most tasks to transform nodes are better done using push style in XSLT I think for finding a minimum or maximum value the for-each is fine.

I'm not sure if it is absolutely correct but I tried this for min
(/Rows/Entry/#value1|/Rows/Entry/#value2)[not((/Rows/Entry/#value1|/Rows/Entry/#value2) < .)]
and this for max
(/Rows/Entry/#value1|/Rows/Entry/#value2)[not((/Rows/Entry/#value1|/Rows/Entry/#value2) > .)]
and it gave me values you mentioned. But for simplification I worked with xml with values without ",".

Related

XSLT: How do I use a concatenated string as a variable name with xsl:for-each select?

I'm being put on a system migration project, and my task is to receive a SOAP XML request from System A, then convert 6 parameter names inside the request into new names readable by System B, and output a file with just the parameters changed. (all param names changed)
Request:
<q0:Command>
<q0:Transaction>
<q0:Operation namespace="Provisioning" name="Create" modifier="Data">
<q0:ParameterList>
<q0:StringParameter name="Action">Create</q0:StringParameter>
<q0:StructParameter name="Create">
<q0:StringParameter name="UserID">1234567X</q0:StringParameter>
<q0:StringParameter name="Device1">abcd567890</q0:StringParameter>
<q0:StringParameter name="Contract1">Postpaid</q0:StringParameter>
<q0:StringParameter name="Line1">990108024011900</q0:StringParameter>
<q0:StringParameter name="Device2">efgh567890</q0:StringParameter>
<q0:StringParameter name="Contract2">Postpaid</q0:StringParameter>
<q0:StringParameter name="Line2">990104562499105</q0:StringParameter>
</q0:StructParameter>
</q0:ParameterList>
</q0:Operation>
</q0:Transaction>
</q0:Command>
First, I just manually recreated each param with the new name in a for-each select block, and it worked. The situation is, there is now a requirement that the values shouldn't be hardcoded one-by-one, but instead checked in a loop (Just in case the number of devices per user increases).
So I thought of declaring a $LoopIndex variable, and concatenating it to strings that match the parameter names, like so:
So I implemented Tim's answer below into my for-each section, like so:
<xsl:for-each select="//q0:CommandRequestData/q0:Command/q0:Transaction/q0:Operation/q0:ParameterList/q0:StructParameter[#name='Create']">
<xsl:for-each select="q0:StringParameter">
<xsl:choose>
<xsl:when test="#name = 'UserID'">
<ins:Parameter name="USER_ID" value="{string()}" />
</xsl:when>
<xsl:when test="starts-with(#name, 'Device')">
<ins:Parameter name="concat('DEVICE_ID_', substring(., 7))" value="{string()}" />
</xsl:when>
<xsl:when test="starts-with(#name, 'Contract')">
<ins:Parameter name="concat('CONTRACT_TYPE_', substring(., 9))" value="{string()}" />
</xsl:when>
<xsl:when test="starts-with(#name, 'Line')">
<ins:Parameter name="concat('LINE_ID_', substring(., 5))" value="{string()}" />
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:for-each>
And the above code isn't working. The page isn't understanding that I mean "#name='Device1'" when I say "#name=concat('Device', $LoopIndex)". Is this kind of thing possible with XSLT or will I need to stick to hardcoding the parameter assignments?
Unfortunately, it's only semi-successful. The page is literally taking the "concat('DEVICE_ID_', substring(., 7))" as a string for the parameter name, instead of performing the concatenation.
Update: Got inspired by Tim's answer and found a 'simple' way of using position() to just increment the numbers:
<xsl:for-each select="//q0:CommandRequestData/q0:Command/q0:Transaction/q0:Operation/q0:ParameterList/q0:StructParameter[#name='Create']">
<xsl:for-each select="q0:StringParameter[#name='UserID']">
<ins:Parameter name="USER_ID" value="{string()}" />
</xsl:for-each>
<xsl:for-each select="q0:StringParameter[starts-with(#name, 'Device')]">
<xsl:variable name="DeviceIndex" select="position()" />
<ins:Parameter name="DEVICE_ID_{$DeviceIndex}" value="{string()}" />
</xsl:for-each>
<xsl:for-each select="q0:StringParameter[starts-with(#name, 'Contract')]">
<xsl:variable name="ContractIndex" select="position()" />
<ins:Parameter name="CONTRACT_TYPE_{$ContractIndex}" value="{string()}" />
</xsl:for-each>
<xsl:for-each select="q0:StringParameter[starts-with(#name, 'Line')]">
<xsl:variable name="LineIndex" select="position()" />
<ins:Parameter name="LINE_ID_{$LineIndex}" value="{string()}" />
</xsl:for-each>
</xsl:for-each>
The resulting SOAP XML request passed to System B now successfully looks like below. It's not arranged like the SOAP request...but I'll take it! :)
<q0:Command>
<q0:Transaction>
<q0:Operation namespace="Provisioning" name="Create" modifier="Data">
<q0:ParameterList>
<q0:StringParameter name="Action">Create</q0:StringParameter>
<q0:StructParameter name="Create">
<ins:Parameter name="USER_ID" value="1234567X"/>
<ins:Parameter name="DEVICE_ID_1" value="abcd567890"/>
<ins:Parameter name="DEVICE_ID_2" value="efgh567890"/>
<ins:Parameter name="CONTRACT_TYPE_1" value="Postpaid"/>
<ins:Parameter name="CONTRACT_TYPE_2" value="Postpaid"/>
<ins:Parameter name="LINE_ID_1" value="990108024011900"/>
<ins:Parameter name="LINE_ID_2" value="990104562499105"/>
</q0:StructParameter>
</q0:ParameterList>
</q0:Operation>
</q0:Transaction>
</q0:Command>
I would use template matching here on the attribute, along with the identity template, rather than xsl:for-each and xsl:choose, just to keep the code cleaner (although it is not strictly necessary to solve your problem).
But to solve your problem you can use string functions to extract the current number from the #name attribute.
Try this XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:q0="q0" >
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="q0:StructParameter[#name='Create']/q0:StringParameter/#name">
<xsl:attribute name="name">
<xsl:choose>
<xsl:when test=". = 'UserID'">
<xsl:text>USER_ID</xsl:text>
</xsl:when>
<xsl:when test="starts-with(., 'Device')">
<xsl:value-of select="concat('DEVICE_ID_', substring(., 7))" />
</xsl:when>
<xsl:when test="starts-with(., 'Contract')">
<xsl:value-of select="concat('CONTRACT_TYPE_', substring(., 9))" />
</xsl:when>
<xsl:when test="starts-with(., 'Line')">
<xsl:value-of select="concat('LINE_ID_', substring(., 5))" />
</xsl:when>
<xsl:otherwise>NA</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
Note that you would have to change the namespace URI for q0 to match your actual one accordingly.

XSLT Expertise required duplicate values

Question edited, more information added
viv:tokenize=str:tokenize
viv:value-of=str:value-of
Part1 - Declaration and assigning value
<declare name="searchhistories" />
<set-var name="searchhistories">
<value-of select="concat(viv:value-of('searchquery','var'),'|',viv:replace(viv:value-of('searchhistory', 'var'),concat(viv:value-of('searchquery','var'),'\|'),'','g'))" />
</set-var>
Part 2: tokenize and de-duplicate
<xsl:for-each select="viv:tokenize($searchhistories,'|',false, false)">
<xsl:variable name="i" select="position()"/>
<xsl:if test="$i < 11">
<xsl:value-of select="." /> |
</xsl:if>
</xsl:for-each>
Able to tokenize but de-duplication not working
What should be code for de-duplication
<xsl:for-each select=***distinct-values***("viv:tokenize($searchhistories,'|',false, false)")>
Something like this ?
Try
<xsl:for-each select="set:distinct(viv:tokenize($searchhistories,'|',false, false))">
with the stylesheet declaring xmlns:set="http://exslt.org/sets" e.g.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:set="http://exslt.org/sets" exclude-result-prefixes="set">
The answer is based on the documentation you linked to in your comment, I am not able to test that.
But http://xsltransform.net/ej9EGcy uses the EXSLT version of tokenize and works fine:
<xsl:template match="item">
<xsl:copy>
<xsl:for-each select="set:distinct(str:tokenize(., '|'))">
<xsl:if test="position() > 1">|</xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:copy>
</xsl:template>

XSLT tokenize nodeset

I'm trying to create a variable that stores the value of an input string (TypeInput) in init cap form. This new variable will be used in different places in my stylesheet. I created a template that I call to convert the input string to init cap form. However, when I run the stylesheet, the resulting variable TypeInputInitCap shows up as NodeSet(1) in the debugger and doesn't output text in my output. Any ideas why? See sample below.
<xsl:variable name="TypeInputInitCap">
<xsl:call-template name="ConvertToInitCapString">
<xsl:with-param name="str" select="$TypeInput"></xsl:with-param>
</xsl:call-template>
</xsl:variable>
<xsl:template name="ConvertToInitCapString">
<xsl:param name="str"></xsl:param>
<!-- Extract each component of the name delimited by . -->
<xsl:variable name="TokenNodeSet">
<xsl:for-each select="tokenize($str, '.')">
<!-- Init cap each component -->
<xsl:value-of select="concat(upper-case(substring(.,1,1)), lower-case(substring(.,2)))"></xsl:value-of>
</xsl:for-each>
</xsl:variable>
<xsl:for-each select="$TokenNodeSet">
<xsl:value-of select="."></xsl:value-of>
<xsl:if test="not(last())">
<xsl:text>.</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
I think that the problem is that the $TokenNodeSet variable contains just a single string, and so the second for-each just loops once.
What about doing this instead:
<xsl:template name="ConvertToInitCapString">
<xsl:param name="str"></xsl:param>
<xsl:for-each select="tokenize($str, '\.')">
<xsl:value-of select="concat(upper-case(substring(.,1,1)), lower-case(substring(.,2)))"/>
<xsl:if test="not(last())">
<xsl:text>.</xsl:text>
</xsl:if>
</xsl:for-each>
EDIT
Fixed the tokenize() call above as suggested by LarsH in the comments
I would replace
<xsl:variable name="TokenNodeSet">
<xsl:for-each select="tokenize($str, '.')">
<!-- Init cap each component -->
<xsl:value-of select="concat(upper-case(substring(.,1,1)), lower-case(substring(.,2)))"></xsl:value-of>
</xsl:for-each>
</xsl:variable>
with
<xsl:variable name="TokenNodeSet" select="for $token in tokenize($str, '\.') return concat(upper-case(substring($token,1,1)), lower-case(substring($token,2)))" />
or better yet with
<xsl:variable name="TokenNodeSet" as="xs:string*" select="for $token in tokenize($str, '\.') return concat(upper-case(substring($token,1,1)), lower-case(substring($token,2)))" />
or finally as it is XSLT 2.0 where there are no nodesets I would rename the variable as e.g.
<xsl:variable name="TokenSequence" as="xs:string*" select="for $token in tokenize($str, '\.') return concat(upper-case(substring($token,1,1)), lower-case(substring($token,2)))" />
Thanks all for your help. The second part in my template was not necessary, so I'm now using this version, which works. It re-adds a '.' character between the tokens. (I didn't use the short version suggested in this thread because I will end up with an extra dot at the end if concatenated.):
<xsl:template name="ConvertToInitCapString">
<xsl:param name="str" select="."></xsl:param>
<!-- Extract each component of the name delimited by . -->
<xsl:for-each select="tokenize($str, '\.')">
<!-- Init cap each component -->
<xsl:value-of select="concat(upper-case(substring(.,1,1)), lower-case(substring(.,2)))"></xsl:value-of>
<xsl:if test="position() != last()">
<xsl:value-of select="'.'"></xsl:value-of>
</xsl:if>
</xsl:for-each>
</xsl:template>

XSLT Paging - default to current Date

I am using an xslt transform to show a long list of events.
It has paging, but what I would like is for it to default to the first events that are closest to the current date.
I'm assuming you have a useful date format (YYYY-MM-DD).
<xsl:param name="currentDate" select="''" /><!-- fill this from outside! -->
<xsl:template name="isPageSelected"><!-- returns true or false -->
<xsl:param name="eventsOnPage" /><!-- expects a node-set of events -->
<xsl:choose>
<xsl:when test="$eventsOnPage">
<!-- create a string "yyyy-mm-dd,YYYY-MM-DD-" (note the trailing dash) -->
<xsl:variable name="dateRange">
<xsl:for-each select="$eventsOnPage">
<xsl:sort select="date" />
<xsl:if test="position() = 1">
<xsl:value-of select="concat(date, ',')" />
</xsl:if>
<xsl:if test="position() = last()">
<xsl:value-of select="concat(date, '-')" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<!-- trailing dash ensures that the "less than" comparison succeeds -->
<xsl:value-of select="
$currentDate >= substring-before($dateRange, ',')
and
$currentDate < substring-after($dateRange, ',')
" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="false()" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
So in your paging routine, to find out if the current page is the selected one, invoke
<xsl:variable name="isPageSelected">
<xsl:call-template name="isPageSelected">
<xsl:with-param name="eventsOnPage" select="event[whatever]" />
</xsl:call-template>
</xsl:variable>
<!-- $isPageSelected now is true or false, proceed accordingly -->
Since sorting in XSLT 1.0 is horribly, horribly broken, your best bet is to find an extension or include a Unix-style time somewhere in your source XML so you can sort on that (although an ISO-formatted string might also work).

index in loop XSL

I have two nested loop in XSL like this, at this moment I use position() but it's not what I need.
<xsl:for-each select="abc">
<xsl:for-each select="def">
I wanna my variable in here increasing fluently 1,2,3,4,5.....n
not like 1,2,3,1,2,3
</xsl:for-each>
</xsl:for-each>
Can you give me some idea for this stub. Thank you very much!
With XSL, the problem is you cannot change a variable (it's more like a constant that you're setting). So incrementing a counter variable does not work.
A clumsy workaround to get a sequential count (1,2,3,4,...) would be to call position() to get the "abc" tag iteration, and another call to position() to get the nested "def" tag iteration. You would then need to multiply the "abc" iteration with the number of "def" tags it contains. That's why this is a "clumsy" workaround.
Assuming you have two nested "def" tags, the XSL would look as follows:
<xsl:for-each select="abc">
<xsl:variable name="level1Count" select="position() - 1"/>
<xsl:for-each select="def">
<xsl:variable name="level2Count" select="$level1Count * 2 + position()"/>
<xsl:value-of select="$level2Count" />
</xsl:for-each>
</xsl:for-each>
Just change the way to select the items to loop over:
<xsl:for-each select="abc/def">
<xsl:value-of select="position()"/>
</xsl:for-each>
Should you specifically need to keep the nested loops, consider adding yet another loop like this:
<xsl:variable name="items" select="//abc/def"/>
<xsl:for-each select="abc">
<xsl:for-each select="def">
<xsl:variable name="id" select="generate-id()"/>
<xsl:for-each select="$items">
<xsl:if test="generate-id()=$id">
<xsl:value-of select="position()"/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
<xsl:for-each select="abc">
<xsl:variable name="i" select="position()"/>
<xsl:for-each select="def">
<xsl:value-of select="$i" />
</xsl:for-each>
</xsl:for-each>
This is an extension of pythonquick's answer that handles different numbers of sub-elements:
<xsl:for-each select="abc">
<xsl:variable name="level1Position" select="position()"/>
<xsl:variable name="priorCount" select="count(../abc[position() < $level1Position]/def)"/>
<xsl:for-each select="def">
<xsl:variable name="level2Count" select="$priorCount + position()"/>
<xsl:value-of select="$level2Count" />
</xsl:for-each>
</xsl:for-each>
Input:
<root>
<abc>
<def>A</def>
<def>B</def>
<def>C</def>
</abc>
<abc>
<def>D</def>
<def>E</def>
</abc>
<abc>
<def>F</def>
</abc>
<abc>
<def>G</def>
<def>H</def>
<def>I</def>
</abc>
</root>