Number Manipulation
1.
Roman Numerals to Integer conversion
A question was asked on
XSL-List to
convert Roman numerals into Integers. XSLT provides a direct method to convert
Integers into Roman form, but not vice-versa. To convert Roman numerals into
Integers, we need to write a pretty complex logic ourselves.
Following is a XSLT 1.0 stylesheet which does this (it doesn't
work perfectly as David explains below):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:call-template name="RomanToInteger">
<xsl:with-param name="roman_number" select="'XXI'"/>
<xsl:with-param name="index" select="1" />
</xsl:call-template>
</xsl:template>
<xsl:template name="RomanToInteger">
<xsl:param name="roman_number" />
<xsl:param name="index" />
<xsl:variable name="temp">
<xsl:call-template name="toRoman">
<xsl:with-param name="value" select="$index" />
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<xsl:when test="$temp = $roman_number">
<xsl:value-of select="$index" />
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="RomanToInteger">
<xsl:with-param name="roman_number"
select="$roman_number" />
<xsl:with-param name="index"
select="$index + 1" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="toRoman">
<xsl:param name="value"/>
<xsl:number value="$value" format="I"/>
</xsl:template>
</xsl:stylesheet>
The corressponding
XSLT 2.0
stylesheet is:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:num="http://whatever"
version="2.0" >
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:call-template name="RomanToInteger">
<xsl:with-param name="roman_number" select="'XXI'"/>
<xsl:with-param name="index" select="1" />
</xsl:call-template>
</xsl:template>
<xsl:template name="RomanToInteger">
<xsl:param name="roman_number" />
<xsl:param name="index" />
<xsl:choose>
<xsl:when test="num:toRoman($index) = $roman_number">
<xsl:value-of select="$index" />
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="RomanToInteger">
<xsl:with-param name="roman_number"
select="$roman_number" />
<xsl:with-param name="index"
select="$index + 1" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:function name="num:toRoman" as="xs:string">
<xsl:param name="value" as="xs:integer"/>
<xsl:number value="$value" format="I"/>
</xsl:function>
</xsl:stylesheet>
The idea behind the above technique is :
1) Pass the Roman number to a named template, to be processed. Also pass a
starting integer value 1.
2) The named template converts the passed integer value into Roman form, using
xsl:number instruction. If the Roman number for the passed integer is equal to
the Original Roman number to be converted, the passed integer is the answer.
Otherwise, the template is recursively called, passing to it the integer values
1 greater than the previous iteration.
David Carlisle pointed some important bugs in the above code. He
wrote..
If you put in a string that is not a valid roman numeral, or not of the form
generated by xsl:number you go into an infinte loop. Or if you want numbers in
"Clock" style so give "IIII" rather than "IV" the algorithm fails.
David provided the following solution written in XSLT 2.0:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:num="http://whatever"
version="2.0" >
<xsl:output method="text"/>
<xsl:template match="/">
[<xsl:value-of select="num:RomanToInteger('I')"/>]
[<xsl:value-of select="num:RomanToInteger('II')"/>]
[<xsl:value-of select="num:RomanToInteger('IIII')"/>]
[<xsl:value-of select="num:RomanToInteger('IV')"/>]
[<xsl:value-of select="num:RomanToInteger('MMCCX')"/>]
</xsl:template>
<xsl:function name="num:RomanToInteger" as="xs:integer">
<xsl:param name="r" as="xs:string"/>
<xsl:choose>
<xsl:when test="ends-with($r,'XC')">
<xsl:sequence select="90 +
num:RomanToInteger(substring($r,1,string-length($r)-2))"/>
</xsl:when>
<xsl:when test="ends-with($r,'L')">
<xsl:sequence select="50 +
num:RomanToInteger(substring($r,1,string-length($r)-1))"/>
</xsl:when>
<xsl:when test="ends-with($r,'C')">
<xsl:sequence select="100 +
num:RomanToInteger(substring($r,1,string-length($r)-1))"/>
</xsl:when>
<xsl:when test="ends-with($r,'D')">
<xsl:sequence select="500 +
num:RomanToInteger(substring($r,1,string-length($r)-1))"/>
</xsl:when>
<xsl:when test="ends-with($r,'M')">
<xsl:sequence select="1000 +
num:RomanToInteger(substring($r,1,string-length($r)-1))"/>
</xsl:when>
<xsl:when test="ends-with($r,'IV')">
<xsl:sequence select="4 +
num:RomanToInteger(substring($r,1,string-length($r)-2))"/>
</xsl:when>
<xsl:when test="ends-with($r,'IX')">
<xsl:sequence select="9 +
num:RomanToInteger(substring($r,1,string-length($r)-2))"/>
</xsl:when>
<xsl:when test="ends-with($r,'IIX')">
<xsl:sequence select="8 +
num:RomanToInteger(substring($r,1,string-length($r)-2))"/>
</xsl:when>
<xsl:when test="ends-with($r,'I')">
<xsl:sequence select="1 +
num:RomanToInteger(substring($r,1,string-length($r)-1))"/>
</xsl:when>
<xsl:when test="ends-with($r,'V')">
<xsl:sequence select="5 +
num:RomanToInteger(substring($r,1,string-length($r)-1))"/>
</xsl:when>
<xsl:when test="ends-with($r,'X')">
<xsl:sequence select="10 +
num:RomanToInteger(substring($r,1,string-length($r)-1))"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="0"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>
</xsl:stylesheet>
2.
Generating numbers in a particular way in the output of the XSLT
transform
The following question was asked on XSL-List.
The problem is, how to find the closest preceding-sibling node
with an offset attribute, then count the nodes that don't have the attribute
since that node.
Input :
<top>
<a>
<reg>A1</reg>
<reg>A2</reg>
<reg offset="10">A3</reg>
<reg>A4</reg>
<reg>A5</reg>
<reg offset="24">A6</reg>
<reg>A7</reg>
</a>
<a>
<reg offset="6">A8</reg>
<reg>A9</reg>
</a>
<a>
<reg>A10</reg>
</a>
</top>
Desired output:
A1 : 0
A2 : 1
A3 : 10
A4 : 11
A5 : 12
A6 : 24
A7 : 25
A8 : 6
A9 : 7
A10: 0
The XSLT 2.0 stylesheet which solves this
problem is (tested with Saxon 8.5.1):
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL
xmlns:xs="http://www.w3.org/2001/XMLSche
xmlns:func="http://whatever"
version="2.0">
<xsl:output method="text" />
<xsl:template match="/top">
<xsl:apply-templates select="a" />
</xsl:template>
<xsl:template match="a">
<xsl:for-each select="reg">
<xsl:variable name="n" select="func:getnum(.,
-1)" />
<xsl:value-of select="." /> : <xsl:value-of select="$n" /><xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:function name="func:getnum" as="xs:double">
<xsl:param name="node" as="element()?" />
<xsl:param name="x" as="xs:double" />
<xsl:choose>
<xsl:when test="$node/@offset">
<xsl:sequence select="$node/@offset" />
</xsl:when>
<xsl:when test="$node">
<xsl:sequence select="
if ($node/preceding-sibling::reg
then
1 + $node/preceding-sibling::reg[1
else
1+ func:getnum($node/preceding
/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="$x" />
</xsl:otherwise>
</xsl:choose>
</xsl:function>
</xsl:stylesheet>
(The stylesheet uses an intelligent recursive call)
3. abs() function in XSLT 1.0
The following question was asked on xml-dev list.
How to implement abs() functionality in XSLT 1.0?
I suggested:
Write a named template for this..