How to get a number with unique digits in XSLT 1.0? - loops

I have below variable.
<xsl:variable name="number" select="56568"/>
Output needed : 568
I need to get a output which contains only unique digits in the number.
Any idea how to achieve this in XSLT 1.0 ?
Thanks

I don't think there is an easy way to do this - unless your processor supports some extension functions. Without it, you would have to use a recursive named template:
<xsl:template name="distinct-characters">
<xsl:param name="input"/>
<xsl:param name="output"/>
<xsl:choose>
<xsl:when test="not($input)">
<xsl:value-of select="$output"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="char" select="substring($input, 1, 1)" />
<!-- recursive call -->
<xsl:call-template name="distinct-characters">
<xsl:with-param name="input" select="substring($input, 2)"/>
<xsl:with-param name="output">
<xsl:value-of select="$output"/>
<xsl:if test="not(contains($output, $char))">
<xsl:value-of select="$char"/>
</xsl:if>
</xsl:with-param>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Example of call:
<output>
<xsl:call-template name="distinct-characters">
<xsl:with-param name="input" select="56568"/>
</xsl:call-template>
</output>
Result:
<output>568</output>
Demo: http://xsltransform.net/a9Gix1
Added:
Revisiting this after 4 years, there is a shorter and more efficient way. Instead of iterating over every character in the input, it iterates only over each distinct character:
<xsl:template name="distinct-characters">
<xsl:param name="input"/>
<xsl:if test="$input">
<xsl:variable name="char" select="substring($input, 1, 1)" />
<xsl:value-of select="$char"/>
<!-- recursive call -->
<xsl:call-template name="distinct-characters">
<xsl:with-param name="input" select="translate($input, $char, '')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
Demo: http://xsltransform.net/a9Gix1/1

Related

Calculating the sum of values from sibling elements using XSLT

I am working on an automatic transform of measurements using XSLT. The transform of single measurements from one system (e.q. imperial) to another (e.g. metric) is working fine. But imperial measurements may take the form '5 ft 10 in' and I would want to convert this into a single metric value.
In my XML model for I accommodate such combined measurements by allowing either a single value or multiple child nodes. So when I find that my has child nodes, I need to convert each one of those child elements to metric units and then add up the values to get one single metric result.
I am struggling to find the best way to process multiple child nodes and add up the resulting values. In an iterative language I would just process from the first to the next and update a global variable, but in XSLT I don't know if there is such a thing as a global variable that can be updated from subsequent calls to the same template.
Here is a (simplified) transform - this one handles only [ft_i] and [in_i] to m.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output method="xml" encoding="UTF-8"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*,node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement">
<xsl:copy>
<xsl:choose>
<xsl:when test="measurement">
<xsl:apply-templates select="*"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="normalise">
<xsl:with-param name="val" as="xs:double" select="number(text())"/>
<xsl:with-param name="unitin" select="#ucum"/>
<xsl:with-param name="count" as="xs:integer" select="1"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
</xsl:template>
<xsl:template name="normalise">
<xsl:param name="val" as="xs:double"/>
<xsl:param name="unitin"/>
<xsl:param name="count" as="xs:integer"/>
<xsl:choose>
<xsl:when test="$unitin eq '[ft_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.3048"/>
</xsl:when>
<xsl:when test="$unitin eq '[in_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.0254"/>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
A simple test file:
<topic>
<p>This piece is
<measurement>
<measurement unit="ft"
ucum=" [ft_i]">10</measurement>
<measurement unit="in"
ucum="[in_i]">2</measurement>
</measurement>
long
</p>
</topic>
The transform gives this:
<topic>
<p>This piece is
<measurement>
<measurement ucum="m"
unit="m">3.048</measurement>
<measurement ucum="m"
unit="m">0.0508</measurement>
</measurement>
long
</p>
</topic>
Obviously, I would want to see this:
<topic>
<p>This piece is
<measurement ucum="m"
unit="m">3.0988</measurement>
long
</p>
</topic>
I could use an xsl:for-each on the child measurement nodes but how do I add the separate values to a global value which can then be output from the main template ?
Assuming that some values and attributes are constant the most simple approach would be using a complex XPath-2.0 expression. Then you can reduce your measurement template to:
<xsl:template match="measurement">
<measurement ucum="m" unit="m">
<xsl:copy-of select="sum(for $x in measurement return if (normalize-space($x/#ucum)= '[ft_i]') then xs:double(normalize-space($x))*0.3048 else xs:double(normalize-space($x))*0.0254)" />
</measurement>
</xsl:template>
It assumes that the attributes stay the same and that there are only two units. But you could easily extend it.
Building templates based on your approach, the following solution is also possible:
<xsl:template match="measurement">
<xsl:copy>
<xsl:variable name="summary">
<xsl:for-each select="measurement">
<val>
<xsl:call-template name="normalise">
<xsl:with-param name="val" as="xs:double" select="number(.)"/>
<xsl:with-param name="unitin" select="#ucum"/>
<xsl:with-param name="count" as="xs:integer" select="1"/>
</xsl:call-template>
</val>
</xsl:for-each>
</xsl:variable>
<xsl:copy-of select="$summary/val[1]/#*" />
<xsl:copy-of select="sum($summary/val)" />
</xsl:copy>
</xsl:template>
<xsl:template name="normalise">
<xsl:param name="val" as="xs:double"/>
<xsl:param name="unitin"/>
<xsl:param name="count" as="xs:integer"/>
<xsl:choose>
<xsl:when test="normalize-space($unitin) = '[ft_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.3048"/>
</xsl:when>
<xsl:when test="normalize-space($unitin) = '[in_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.0254"/>
</xsl:when>
</xsl:choose>
</xsl:template>
It is more flexible and uses a two-stage approach with the variable.
The result is the same. I guess if you combine both, you'll find a good way fitting your needs.
Assuming you can have more than just 2 units, it would be convenient to store them in a variable and retrieve the corresponding conversion factor using a key:
XSLT 2.0
<xsl:stylesheet version="2.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:key name="unit" match="unit" use="#name" />
<xsl:variable name="units">
<unit name="ft" factor=".3048"/>
<unit name="in" factor=".0254"/>
<!-- add more units here ... -->
</xsl:variable>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[measurement]">
<measurement ucum="m" unit="m">
<xsl:value-of select="sum(measurement/(. * key('unit', #unit, $units)/#factor))"/>
</measurement>
</xsl:template>
</xsl:stylesheet>
Demo: https://xsltfiddle.liberty-development.net/6rewNxT
No need for multi-pass processing -- XPath 2.0 is strong enough for this kind of problem:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[*]">
<measurement ucum="m" unit="m">
<xsl:value-of select=
"sum(measurement/(. * (if(#ucum eq '[ft_i]') then 0.3048 else 0.0254)))"/>
</measurement>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<topic>
<p>This piece is
<measurement>
<measurement unit="ft"
ucum="[ft_i]">10</measurement>
<measurement unit="in"
ucum="[in_i]">2</measurement>
</measurement>
long
</p>
</topic>
The wanted, correct result is produced:
<topic>
<p>This piece is
<measurement ucum="m" unit="m">3.0988</measurement>
long
</p>
</topic>
II. More Generic Solution
One can produce a more generic solution, where the conversion is isolated into a separate XSLT <xsl:function>. Do note that this solution will work even in cases when the conversion logic is so complex that it cannot be expressed in a single XPath expression as was possible in the solution above. If the conversion is not just a simple multiplication, then it is not possible in general to present it as a table and use xsl:key:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:f="my:f" exclude-result-prefixes="f">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[*]">
<measurement ucum="m" unit="m">
<xsl:value-of select="sum(measurement/f:convert(#ucum, .))"/>
</measurement>
</xsl:template>
<xsl:function name="f:convert">
<xsl:param name="pFromUnit"/>
<xsl:param name="pValue"/>
<xsl:value-of select=
"$pValue * (if($pFromUnit eq '[ft_i]') then 0.3048 else 0.0254)"/>
</xsl:function>
</xsl:stylesheet>
When this transformation is applied on the same XML document (above), the same correct, wanted result is produced again:
<topic>
<p>This piece is
<measurement ucum="m" unit="m">3.0988</measurement>
long
</p>
</topic>

XSLT sort array values

I'm new on XSLT. I want to sort the following xml segment by XSLT. Does anyone know how to sort the inner array values?
<Coordinates>
<Array n="155" type="real">1909.090909090909 1894.7368421052631 1777.7777777777778 1923.076923076923 2000.0 3191.528925619835 3771.025641025641 4609.022727272727 6931.111111111111 7394.8611111111095 3149.4444444444443 4173.596491228071 6090.740740740741 7578.214285714285 7261.25 3369.7478260869566 5986.1621621621625 6515.15625 7138.875000000002 8225.714285714286 3224.5867768595035 6915.27027027027 8103.548387096775 6897.741935483871 8485.166666666666 3662.2988505747126 7968.2307692307695 7770.882352941177 6628.548387096775 8864.642857142857 3429.8863636363635 5785.285714285715 6576.428571428572 6791.8 8715.625 4015.2450980392155 7127.045454545455 6171.041666666667 9326.95652173913 10307.827586206897 3577.6136363636365 6820.000000000001 6308.913043478261 8907.0 9448.392857142857 3452.926829268293 7172.5 7280.0 8653.125 9277.692307692309 3108.3333333333335 4601.282051282052 6530.538461538462 8847.368421052632 8147.105263157895 3258.360655737705 7630.833333333335 6814.333333333333 5513.35294117647 9727.894736842105 2781.2500000000005 6974.21875 7172.5 6459.318181818182 8599.722222222223 3512.0454545454545 5203.5 7422.5 9705.454545454546 9217.631578947368 3608.7820512820513 6385.952380952381 9302.35294117647 6647.857142857143 8054.285714285715 ...
</Array>
</Coordinates>
I want to sort the real values by ascending order.
Thanks in advance.
If you are using the Xalan-J processor, you can take advantage of the EXSLT str:tokenize() extension function and do simply:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:str="http://exslt.org/strings"
extension-element-prefixes="str">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Array">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:for-each select="str:tokenize(., ' ')">
<xsl:sort select="." data-type="number" order="ascending"/>
<xsl:value-of select="."/>
<xsl:text> </xsl:text>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Otherwise you will have to tokenize the values using a recursive named template, then convert the result to a node-set before you can sort it:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Array">
<xsl:variable name="values">
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="."/>
</xsl:call-template>
</xsl:variable>
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:for-each select="exsl:node-set($values)/value">
<xsl:sort select="." data-type="number" order="ascending"/>
<xsl:value-of select="."/>
<xsl:text> </xsl:text>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="text"/>
<xsl:param name="delimiter" select="' '"/>
<xsl:variable name="token" select="substring-before(concat($text, $delimiter), $delimiter)" />
<xsl:if test="$token">
<value>
<xsl:value-of select="$token"/>
</value>
</xsl:if>
<xsl:if test="contains($text, $delimiter)">
<!-- recursive call -->
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
One solution is to form a variable where each tag contains value. And then you can sort it by method , which works for number in xslt 1.0 too. As i can see, your values here are separated by a space.
ok, here is my solution. credit for a part goes to Does XSLT have a Split() function?
<xsl:template match="/">
<xsl:variable name="raw-data">
<xsl:value-of select="Coordinates/Array"/>
</xsl:variable>
<xsl:variable name="ids">
<xsl:if test="$raw-data">
<xsl:call-template name="output-tokens">
<xsl:with-param name="list" select="$raw-data" />
</xsl:call-template>
</xsl:if>
</xsl:variable>
<xsl:for-each select="msxsl:node-set($ids)/id">
<xsl:sort data-type="number" order="descending" select="."/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:template>
<xsl:template name="output-tokens">
<xsl:param name="list" />
<xsl:variable name="newlist" select="concat(normalize-space($list), ' ')" />
<xsl:variable name="first" select="substring-before($newlist, ' ')" />
<xsl:variable name="remaining" select="substring-after($newlist, ' ')" />
<id>
<xsl:value-of select="$first" />
</id>
<xsl:if test="$remaining">
<xsl:call-template name="output-tokens">
<xsl:with-param name="list" select="$remaining" />
</xsl:call-template>
</xsl:if>
</xsl:template>
p.s. edited - now with sort

Compare XSL values in array

I'm not sure how to title this post..
I want to compare pubDate between rows
Desired Output: Only print the pubDate when it is different from the previous row's pubDate. (my substring function returns Dec 2014, Jan 2015, etc)
Currently I have it set to print the pubDate every time. I just can't figure out how to compare the value between rows... please help!
Code excerpt:
<xsl:template match="item">
<xsl:variable name="Rows" select="channel/item"/>
<xsl:variable name="RowCount" select="count($Rows)"/>
<xsl:variable name="item_link" select="link"/>
<xsl:variable name="item_title" select="description"/>
<xsl:value-of select="$Rows"/>
<xsl:variable name="CurPosition" select="position()" />
<xsl:variable name="PrevPosition" select="position()-1" />
<xsl:variable name="RssFeedLink" select="$rss_WebPartID" />
<xsl:variable name="CurrentElement" select="concat($RssFeedLink,$CurPosition)" />
<xsl:if test="not(substring(pubDate,3,9) = 0)">
<div align="center">
<hr/>
<xsl:value-of select="substring(pubDate,3,9)" disable-output-escaping="yes"/>
</div>
</xsl:if>
<li>
<a href="{$item_link}" title="{$item_title}">
<xsl:variable name="SafeHtml">
<xsl:call-template name="GetSafeHtml">
<xsl:with-param name="Html" select="title"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$SafeHtml" disable-output-escaping="yes"/>
</a>
</li>
Here is the full code for anybody who wants it:
<xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema"
version="1.0" exclude-result-prefixes="xsl ddwrt msxsl rssaggwrt"
xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime"
xmlns:rssaggwrt="http://schemas.microsoft.com/WebParts/v3/rssagg/runtime"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:rssFeed="urn:schemas-microsoft-com:sharepoint:RSSAggregatorWebPart">
<xsl:param name="rss_FeedLimit">30</xsl:param>
<xsl:param name="rss_ExpandFeed">true</xsl:param>
<xsl:param name="rss_LCID">1033</xsl:param>
<xsl:param name="rss_WebPartID">RSS_Viewer_WebPart</xsl:param>
<xsl:param name="rss_alignValue">left</xsl:param>
<xsl:param name="rss_IsDesignMode">True</xsl:param>
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="Rows" select="channel/item"/>
<xsl:variable name="RowCount" select="count($Rows)"/>
<div>
<xsl:apply-templates select="rss/channel"/>
</div>
</xsl:template>
<xsl:template match="rss/channel">
<xsl:variable name="link" select="link"/>
<xsl:variable name="description" select="description"/>
<ul>
<xsl:apply-templates select="item"/>
</ul>
</xsl:template>
<xsl:template match="item">
<xsl:variable name="Rows" select="channel/item"/>
<xsl:variable name="RowCount" select="count($Rows)"/>
<xsl:variable name="item_link" select="link"/>
<xsl:variable name="item_title" select="description"/>
<xsl:value-of select="$Rows"/>
<xsl:variable name="CurPosition" select="position()" />
<xsl:variable name="RssFeedLink" select="$rss_WebPartID" />
<xsl:variable name="CurrentElement" select="concat($RssFeedLink,$CurPosition)" />
<xsl:if test="not(substring(pubDate,3,9) = 0)">
<div align="center">
<hr/>
<xsl:value-of select="substring(pubDate,3,9)" disable-output-escaping="yes"/>
</div>
</xsl:if>
<li>
<a href="{$item_link}" title="{$item_title}">
<xsl:variable name="SafeHtml">
<xsl:call-template name="GetSafeHtml">
<xsl:with-param name="Html" select="title"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$SafeHtml" disable-output-escaping="yes"/>
</a>
</li>
</xsl:template>
<xsl:template name="GetSafeHtml">
<xsl:param name="Html"/>
<xsl:choose>
<xsl:when test="$rss_IsDesignMode = 'True'">
<xsl:value-of select="$Html"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="rssaggwrt:MakeSafe($Html)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
If you want to check the value of previous "row" you can use the preceding-sibling axis operator.
So, instead of writing this....
<xsl:if test="not(substring(pubDate,3,9) = 0)">
Write this...
<xsl:if test="not(substring(pubDate,3,9) = 0) and not(pubDate = preceding-sibling::item[1]/pubDate)">
Note the use of the [1] here. Doing preceding-sibling will return all nodes preceding the current one, and you only want the first one.
Also note, you might be tempted to write this...
<xsl:if test="not(substring(pubDate,3,9) = 0) and pubDate != preceding-sibling::item[1]/pubDate">
But this will not work for the first item element, because for that expression to be true the preceding sibling would have to exist.
Finally, this will only really work if the item elements are in pub-date order. A better approach would be to consider this a grouping problem, and group the items by date. For XSLT 1.0 you would use a technique called Muenchian Grouping.

xslt single output from a loop

I have a source file that looks like this
<company>
<ids>
<id provider="AAA">123456</id>
</ids>
<businessDetails>
<blocks>
<block channel="google.com"/>
<block channel="nokia.com"/>
<block channel="bing.com"/>
</blocks>
<registration>
<registrationNumber>1352045555</registrationNumber>
<registrationDate>1983-09-17</registrationDate>
</registration>
</businessDetails>
</company>
I need to loop through block to see if I should send the registration number. A part of my xslt looks like this
<xsl:choose>
<xsl:when test="s2:businessDetails/s2:blocks">
<xsl:for-each select="s2:businessDetails/s2:blocks/s2:block">
<xsl:variable name="var:v1" select="userCSharp:LogicalEq(string(./#channel="google.com") , "true")"/>
<xsl:variable name="var:v2" select="userCSharp:LogicalEq(string(./#channel="bing.com") , "true")"/>
<xsl:choose>
<xsl:when test="($var:v1 or $var:v2)='true'">
</xsl:when>
<xsl:otherwise>
<ns0:companyOrgNumber>
<xsl:value-of select="../../s2:registration/s2:registrationNumber/text()"/>
</ns0:companyOrgNumber>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:if test="s2:businessDetails/s2:registration/s2:registrationNumber">
<ns0:companyOrgNumber>
<xsl:value-of select="s2:businessDetails/s2:registration/s2:registrationNumber/text()"/>
</ns0:companyOrgNumber>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
The problem is when I have blocks, but none of them match var:v1 or var:v2 I get several in the destination file. I only want to have one if all block are false.
It looks like you should do this instead:
<xsl:if test="s2:businessDetails/s2:blocks[not(s2:block[#channel='google.com']) and not(s2:block[#channel='bing.com'])]">
<ns0:companyOrgNumber>
<xsl:value-of select="s2:businessDetails/s2:registration/s2:registrationNumber/text()"/>
</ns0:companyOrgNumber>
</xsl:if>
You don't really need to loop through it- Just check for the absence of those nodes.

How to emulate C enumeration in XSLT with optional values

I'm trying to make a XSLT conversion that generates C code, the following XML should be converted:
<enum name="anenum">
<enumValue name="a"/>
<enumValue name="b"/>
<enumValue name="c" data="10"/>
<enumValue name="d" />
<enumValue name="e" />
</enum>
It should convert to some C code as following:
enum anenum {
a = 0,
b = 1,
c = 10,
d = 11,
e = 12
}
or alternatively (as the C preprocessor will handle the summation):
enum anenum {
a = 0,
b = 1,
c = 10,
d = c+1,
e = c+2
}
The core of my XSLT looks like:
<xsl:for-each select="enumValue">
<xsl:value-of select="name"/>
<xsl:text> = </xsl:text>
<xsl:choose>
<xsl:when test="string-length(#data)>0">
<xsl:value-of select="#data"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="position()-1"/>
</xsl:otherwise>
</xsl:choose>
<xsl:text>,
(for simplicity I skip some of the 'no comma at the last element' code)
This example will not generate the correct values for d and e
I've been trying to get it working for the variable d and e, but so far I'm unsuccessful.
Using constructions like:
<xsl:when test="string-length(preceding-sibling::enumValue[1]/#datavalue)>0">
<xsl:value-of select="preceding-sibling::enumValue/#data + 1"/>
</xsl:when>
...only work for the first one after the specified value (in this case d).
Who can help me? I'm probably thinking too much in a procedural way...
A nonrecursive solution, using keys:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output method="text"/>
<xsl:key name="koffsetEnums" match="enumValue[#data]"
use="generate-id()"/>
<xsl:template match="enum">
enum <xsl:value-of select="#name"/> {
<xsl:apply-templates select="enumValue"/>
}
</xsl:template>
<xsl:template match="enumValue">
<xsl:value-of select="concat(#name, ' = ')"/>
<xsl:variable name="voffsetValueId" select=
"generate-id((. | preceding-sibling::enumValue)
[#data][last()]
)"/>
<xsl:choose>
<xsl:when test="not($voffsetValueId)">
<xsl:value-of select="concat(position(),'
')"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vinitOffset" select=
"key('koffsetEnums', $voffsetValueId)/#data"
/>
<xsl:value-of select=
"$vinitOffset
+
count(preceding-sibling::enumValue)
-
count(key('koffsetEnums', $voffsetValueId)/preceding-sibling::enumValue)
"
/>
<xsl:text>
</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
When the above transformation is applied on the originally provided XML document:
<enum name="anenum">
<enumValue name="a"/>
<enumValue name="b"/>
<enumValue name="c" data="10"/>
<enumValue name="d" />
<enumValue name="e" />
</enum>
the required result is produced:
enum anenum {
a = 1
b = 2
c = 10
d = 11
e = 12
}
A better solution with keys, avoiding most use of the preceding-sibling axis:
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output method="text"/>
<!-- -->
<xsl:key name="ksimpleEnValues" match="enumValue[not(#data)]"
use="generate-id(preceding-sibling::enumValue[#data][1])"/>
<!-- -->
<xsl:template match="enum">
enum <xsl:value-of select="#name"/>
{
<xsl:apply-templates select=
"key('ksimpleEnValues', '')
"/>
<xsl:apply-templates select="enumValue[#data]"/>
}
</xsl:template>
<!-- -->
<xsl:template match="enumValue">
<xsl:param name="pOffset" select="0"/>
<xsl:value-of select=
"concat(#name, ' = ', position()+$pOffset,'
')"/>
</xsl:template>
<!-- -->
<xsl:template match="enumValue[#data]">
<xsl:value-of select=
"concat(#name, ' = ', #data,'
')"/>
<!-- -->
<xsl:apply-templates select=
"key('ksimpleEnValues', generate-id())">
<xsl:with-param name="pOffset" select="#data"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
when applied on the originally-provided XML document:
<enum name="anenum">
<enumValue name="a"/>
<enumValue name="b"/>
<enumValue name="c" data="10"/>
<enumValue name="d" />
<enumValue name="e" />
</enum>
Produces the wanted result:
enum anenum
{
a = 1
b = 2
c = 10
d = 11
e = 12
}
Explanation:
The key named ksimpleEnValues indexes all enumValue elements that do not have the data attribute. The indexing is done by the generate-id() value of the first preceding enumValue element that has a data attribute.
Thus key('ksimpleEnValues', someId) is the nodeset containing all enumValue elements following the enumValue that has its generate-id() equal to someId, and all these enumValue elements are preceding the next enumValue with a data attribute, if such exists.
key('ksimpleEnValues', '') is the node-set of all enumValue elements that do not have a preceding enumValue element with a data attribute.
The template that matches enumValue takes an optional parameter $pOffset, in which the value of the data attribute from the immediate preceding enumValue element with this attribute, will be passed, otherwise the default value for $pOffset is 0.
The template matching enumValue elements that have a data attribute produces its enum-value (#name = #data) and then applies templates to all enumValue elements between itself and the next (if such exists) enumValue with a data attribute. The value of the data attribute is passed as the $pOffset parameter and it will be added to the relative position of each selected enumValue element when producing the output from its processing.
You can't change "variables" in xsl but you can use recursion. Don't use preceding-sibling predicates unless absolutely urgent as they will kill your performance.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:template match="/" >
<xsl:call-template name="printEnum">
<xsl:with-param name="value" select="0"/>
<xsl:with-param name="position" select="1"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="printEnum">
<xsl:param name="position"/>
<xsl:param name="value" select="0"/>
<xsl:variable name="node" select="/enum/enumValue[$position]"/>
<xsl:variable name="enumValue">
<xsl:choose>
<xsl:when test="$node/#data">
<xsl:value-of select="$node/#data"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value + 1"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:value-of select="concat($node/#name, ' = ', $enumValue, ' , ')"/>
<xsl:if test="/enum/enumValue[$position + 1]">
<xsl:call-template name="printEnum">
<xsl:with-param name="value" select="$enumValue"/>
<xsl:with-param name="position" select="$position + 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

Resources