XSLT: Concatenate a string n times - xslt-2.0

I want to concatenate a string for n times and set it as variable using XSLT 2.0. The string to be concatenated is ../, n is available as number in a variable.
I tried:
<xsl:variable name="count-dirs" select="count(tokenize($target-filepath, '/')) - 1"/>
<xsl:variable name="path" select="''"/>
<xsl:for-each select="1 to $count-dirs"><xsl:variable name="path" select="concat($path, '../')"/></xsl:for-each>

I think you want e.g. <xsl:variable name="path" select="string-join((1 to $count-dirs)!'../', '')"/> in XSLT 3 or <xsl:variable name="path" select="string-join(for $i in 1 to $count-dirs return '../', '')"/> in XSLT 2.

Life can be so easy...
<xsl:variable name="count-dirs" select="count(tokenize($target-filepath, '/')) - 1"/>
<xsl:variable name="path">
<xsl:for-each select="1 to $count-dirs">../</xsl:for-each>
</xsl:variable>

Related

split xml file with multiple value using xslt

trying to split the xml file having multiple elements into separate xml having different elements.
Input File:
<Person>
<firstname>ABC</firstname>
<lastname>ABC</lastname>
<address>address1</address>
<address>address2</address>
<city>city</city>
<state>state</state>
<currency>currency1</currency>
<currency>currency2</currency>
</Person>
Need to split above file into two files as
Output file-1
<Person>
<firstname>ABC</firstname>
<lastname>ABC</lastname>
<address>address1</address>
<city>city</city>
<state>state</state>
<currency>currency1</currency>
</Person>
Output file -2
<Person>
<firstname>ABC</firstname>
<lastname>ABC</lastname>
<address>address2</address>
<city>city</city>
<state>state</state>
<currency>currency2</currency>
</Person>
Here's a generic solution that outputs N files where N is the maximum number of same-name children elements, where file N contains the Nth instance of each element name if there are at least N, or the first one otherwise:
<xsl:template match="/*">
<xsl:variable name="this" select="."/>
<xsl:variable name="names" select="distinct-values(*/name())"/>
<xsl:for-each select="1 to max(
for $name in $names return count(*[name()=$name]))"/>
<xsl:variable name="n" select="."/>
<xsl:result-document href="file{.}">
<xsl:element name="{name($this)}">
<xsl:for-each-group select="$this/*" group-by="name()">
<xsl:copy-of select="(current-group()[$n], .)[1]"/>
</xsl:for-each-group>
</xsl:element>
</xsl:result-document>
</xsl:for-each>
</xsl:template>
This should do what you want with the example input you have shown, but whether it does the right thing with any other input is anyone's guess, because you have under-specified the requirements.

XSLT: How do I get the value of a variable by concatenating a string and another variable?

I am trying to get the value of a variable by concatenating a string and another variable. But the result is a string with the variable name, not the value. So the following code fails since its trying to evaluate a string against a number. Also where the value should be, there is only the name of the variable.
The aim is to make a scrset for images ranging from 300px to maximum 4200. But stopping the srcset before it reaches the maxWidth value. So if an image has a maxWidth of 2000, then the iteration would stop after outputting 1800.
This is the code I have so far:
<xsl:variable name="count" select="14"/>
<xsl:variable name="maxWidth" select="2200"/> <!-- this value will be dynamic depending on each image (taken from an attribute on the image) -->
<xsl:variable name="loopIndex1" select="300"/>
<xsl:variable name="loopIndex2" select="600"/>
<xsl:variable name="loopIndex3" select="900"/>
<xsl:variable name="loopIndex4" select="1200"/>
<xsl:variable name="loopIndex5" select="1500"/>
<xsl:variable name="loopIndex6" select="1800"/>
<xsl:variable name="loopIndex7" select="2100"/>
<xsl:variable name="loopIndex8" select="2400"/>
<xsl:variable name="loopIndex9" select="2700"/>
<xsl:variable name="loopIndex10" select="3000"/>
<xsl:variable name="loopIndex11" select="3300"/>
<xsl:variable name="loopIndex12" select="3600"/>
<xsl:variable name="loopIndex13" select="3900"/>
<xsl:variable name="loopIndex14" select="4200"/>
<xsl:attribute name="srcset">
<xsl:for-each select="1 to $count">
<xsl:variable name="index" select="position()"/>
<xsl:variable name="source">
<xsl:value-of select="concat('loopIndex', $index)"/>
</xsl:variable>
<xsl:if test="$source < $maxWidth">
http://imagescalerserver.com/?url=http://test.com/1108932.jpg&w=<xsl:value-of select="concat($source, ' ')" /> <xsl:value-of select="$source" />w,
</xsl:if>
</xsl:for-each>
</xsl:attribute>
If I remove the test just to get some output, the output would be:
srcset="
http://imagescalerserver.com/?url=http://test.com/1108932.jpg&w=loopIndex1 loopIndex1w,
http://imagescalerserver.com/?url=http://test.com/1108932.jpg&w=loopIndex2 loopIndex2w,
etc
"
The wanted result is:
srcset="
http://imagescalerserver.com/?url=http://test.com/1108932.jpg&w=300 300w,
http://imagescalerserver.com/?url=http://test.com/1108932.jpg&w=600 600w,
etc
"
I also need to not have the comma after the last item. Meaning if http://imagescalerserver.com/?url=http://test.com/1108932.jpg&w=600 600w, was the last output then the comma at the end would not be there, like this:
http://imagescalerserver.com/?url=http://test.com/1108932.jpg&w=600 600w
Ideally I would like to not have to make the loopIndex variables, but rather just increment the value by 300 for a total of 14 iterations, but since variables cant be changed this is the best I've managed. If there is a better way, I'd appreciate to hear about it.
Declare a single variable <xsl:variable name="loopIndex" select="300, 600, 900, ..., 4200"/> (you need to spell out the ... in your code) and then you can set
<xsl:variable name="source" select="$loopIndex[current()]"/>
inside of the for-each.

Xslt2.0, How to identify the mix of positive and negative numbers inside strings

I have an xml like this (see below).
Using xslt2.0, I need to find whether any one contain a mix of positive and negative numbers ?
Weights: concantenated string of positive/negative numbers.. (separator = ;).
<PriceInfo>
<price>
<date>20160124</date>
<weights>1;2;5;4;</weights>
</price>
<price>
<date>20160125</date>
<weights>1;2;3;4;</weights>
</price>
<price>
<date>20160126</date>
<weights>1;-2;3;4;</weights>
</price>
</PriceInfo>
Thanks
Well, using tokenize you can extract the tokens between ;, you can then check whether they are integers, if so, convert them, and then you can check whether there are any greater than and any smaller than zero:
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="weights">
<xsl:copy>
<xsl:variable name="integers" select="for $token in tokenize(., ';')[. castable as xs:integer] return xs:integer($token)"/>
<xsl:sequence select="(some $i in $integers satisfies $i gt 0) and (some $j in $integers satisfies $j lt 0)"/>
</xsl:copy>
</xsl:template>
</xsl:transform>
I modified the answer from Martin to suit my needs.
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:template match="/">
<xsl:variable name="allWeights" select="string-join(//weights/text(), '')" />
<xsl:variable name="weightTokens" select="for $token in tokenize($allWeights, ';')[. castable as xs:string] return xs:string($token)"/>
<xsl:variable name="isMixOfPositiveNegativeWeights" select="(some $posToken in $weightTokens satisfies matches($posToken, '[0-9].*')) and (some $negToken in $weightTokens satisfies matches($negToken, '-.*'))"/>
<xsl:value-of select="$isMixOfPositiveNegativeWeights" />
</xsl:template>
</xsl:transform>

XSLT template matching on //

The following xslt code produces the output incorrectly. Actually, it should increment the values by 1, But, It produces increment by 2. I need to get this know why. Could anyone let me know why this?
the xml input is
<AAA>
<BBB>cc </BBB>
<BBB>ff </BBB>
<BBB>aa </BBB>
<BBB>fff </BBB>
<BBB>FFF </BBB>
<BBB>Aa </BBB>
<BBB>ccCCC </BBB>
</AAA>
and the xslt input code is
<xsl:template match="/">
<xsl:text>
BBB[</xsl:text>
<xsl:value-of select="position()"/>
<xsl:text>]: </xsl:text>
<xsl:value-of select="."/>
</xsl:template>
It produces the output as follows [wrongly], but it should provide such as [1], [2], [3] etc.
BBB[2]: cc
BBB[4]: ff
BBB[8]: aa
BBB[10]: fff
BBB[12]: FFF
BBB[14]: Aa
BBB[16]: ccCCC
Any idea?
I am pretty sure if you only have <xsl:template match="/"> that then you won't even get the output you say you get.
Assuming you have
<xsl:template match="BBB">
<xsl:text>
BBB[</xsl:text>
<xsl:value-of select="position()"/>
<xsl:text>]: </xsl:text>
<xsl:value-of select="."/>
</xsl:template>
then the result depends on other factors like whether you have <xsl:strip-space elements="*"/> or whether you use
<xsl:template match="AAA">
<xsl:apply-templates select="*"/>
</xsl:template>
Your current result you have suggests you are not stripping white space text nodes and you either rely on built-in templates or you have <xsl:apply-templates/> or <xsl:apply-templates select="node()"/> in the template matching AAA. That way the current node list contains both element node as well as text nodes (between element nodes) resulting in your position results 2, 4, 6, ...
I would fix the code with
<xsl:template match="BBB">
<xsl:text>
BBB[</xsl:text>
<xsl:number/>
<xsl:text>]: </xsl:text>
<xsl:value-of select="."/>
</xsl:template>

Improving the performance of XSL

I am using the below XSL 2.0 code to find the ids of the text nodes that contains the list of indices that i give as input. the code works perfectly but in terms for performance it is taking a long time for huge files. Even for huge files if the index values are small then the result is quick in few ms. I am using saxon9he Java processor to execute the XSL.
<xsl:variable name="insert-data" as="element(data)*">
<xsl:for-each-group
select="doc($insert-file)/insert-data/data"
group-by="xsd:integer(#index)">
<xsl:sort select="current-grouping-key()"/>
<data
index="{current-grouping-key()}"
text-id="{generate-id(
$main-root/descendant::text()[
sum((preceding::text(), .)/string-length(.)) ge current-grouping-key()
][1]
)}">
<xsl:copy-of select="current-group()/node()"/>
</data>
</xsl:for-each-group>
</xsl:variable>
In the above solution if the index value is too huge say 270962 then the time taken for the XSL to execute is 83427ms. In huge files if the index value is huge say 4605415, 4605431 it takes several minutes to execute. Seems the computation of the variable "insert-data" takes time though it is a global variable and computed only once. Should the XSL be addessed or the processor? How can i improve the performance of the XSL.
I'd guess the problem is the generation of text-id, i.e. the expression
generate-id(
$main-root/descendant::text()[
sum((preceding::text(), .)/string-length(.)) ge current-grouping-key()
][1]
)
You are potentially recalculating a lot of sums here. I think the easiest path here would be to invert your approach: recurse across the text nodes in the document, aggregate the string length so far, and output data elements each time a new #index is reached. The following example illustrates the approach. Note that each unique #index and each text node is visited only once.
<xsl:variable name="insert-doc" select="doc($insert-file)"/>
<xsl:variable name="insert-data" as="element(data)*">
<xsl:call-template name="calculate-data"/>
</xsl:variable>
<xsl:key name="index" match="data" use="xsd:integer(#index)"/>
<xsl:template name="calculate-data">
<xsl:param name="text-nodes" select="$main-root//text()"/>
<xsl:param name="previous-lengths" select="0"/>
<xsl:param name="indexes" as="xsd:integer*">
<xsl:perform-sort
select="distinct-values(
$insert-doc/insert-data/data/#index/xsd:integer(.))">
<xsl:sort/>
</xsl:perform-sort>
</xsl:param>
<xsl:if test="$text-nodes">
<xsl:variable name="total-lengths"
select="$previous-lengths + string-length($text-nodes[1])"/>
<xsl:choose>
<xsl:when test="$total-lengths ge number($indexes[1])">
<data
index="{$indexes[1]}"
text-id="{generate-id($text-nodes[1])}">
<xsl:copy-of select="key('index', $indexes[1],
$insert-doc)"/>
</data>
<!-- Recursively move to the next index. -->
<xsl:call-template name="calculate-data">
<xsl:with-param
name="text-nodes"
select="$text-nodes"/>
<xsl:with-param
name="previous-lengths"
select="$previous-lengths"/>
<xsl:with-param
name="indexes"
select="subsequence($indexes, 2)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<!-- Recursively move to the text node. -->
<xsl:call-template name="calculate-data">
<xsl:with-param
name="text-nodes"
select="subsequence($text-nodes, 2)"/>
<xsl:with-param
name="previous-lengths"
select="$total-lengths"/>
<xsl:with-param
name="indexes"
select="$indexes"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>

Resources