Recipe 3.3. Converting from Roman Numerals to Numbers
Problem
You need to convert a Roman numeral to a
number.
Solution
Roman numbers do not use a place value system; instead, the number is
composed by adding or subtracting the fixed value of the specified
Roman numeral characters. If the following character has a lower or
equal value, you add; otherwise, you subtract:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:math="http://www.ora.com/XSLTCookbook/math">
<ckbk:romans>
<ckbk:roman value="1">i</ckbk:roman>
<ckbk:roman value="1">I</ckbk:roman>
<ckbk:roman value="5">v</ckbk:roman>
<ckbk:roman value="5">V</ckbk:roman>
<ckbk:roman value="10">x</ckbk:roman>
<ckbk:roman value="10">X</ckbk:roman>
<ckbk:roman value="50">l</ckbk:roman>
<ckbk:roman value="50">L</ckbk:roman>
<ckbk:roman value="100">c</ckbk:roman>
<ckbk:roman value="100">C</ckbk:roman>
<ckbk:roman value="500">d</ckbk:roman>
<ckbk:roman value="500">D</ckbk:roman>
<ckbk:roman value="1000">m</ckbk:roman>
<ckbk:roman value="1000">M</ckbk:roman>
</ckbk:romans>
<xsl:variable name="ckbk:roman-nums" select="document('')/*/*/ckbk:roman"/>
<xsl:template name="ckbk:roman-to-number">
<xsl:param name="roman"/>
<xsl:variable name="valid-roman-chars">
<xsl:value-of select="document('')/*/ckbk:romans"/>
</xsl:variable>
<xsl:choose>
<!-- returns true if there are any non-Roman characters in the string -->
<xsl:when test="translate($roman,$valid-roman-chars,'')">NaN</xsl:when>
<xsl:otherwise>
<xsl:call-template name="ckbk:roman-to-number-impl">
<xsl:with-param name="roman" select="$roman"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="ckbk:roman-to-number-impl">
<xsl:param name="roman"/>
<xsl:param name="value" select="0"/>
<xsl:variable name="len" select="string-length($roman)"/>
<xsl:choose>
<xsl:when test="not($len)">
<xsl:value-of select="$value"/>
</xsl:when>
<xsl:when test="$len = 1">
<xsl:value-of select="$value + $ckbk:roman-nums[. = $roman]/@value"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="roman-num"
select="$ckbk:roman-nums[. = substring($roman, 1, 1)]"/>
<xsl:choose>
<xsl:when test="$roman-num/following-sibling::ckbk:roman =
substring($roman, 2, 1)">
<xsl:call-template name="ckbk:roman-to-number-impl">
<xsl:with-param name="roman" select="substring($roman,2,$len - 1)"/>
<xsl:with-param name="value" select="$value - $roman-num/@value"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="ckbk:roman-to-number-impl">
<xsl:with-param name="roman" select="substring($roman,2,$len - 1)"/>
<xsl:with-param name="value" select="$value + $roman-num/@value"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Discussion
The xsl:number
element
provides a convenient way to convert numbers to Roman numerals;
however, for converting from Roman numerals to numbers, you are on
your own. The recursive template shown earlier is straightforward and
much like that already found in Jeni Tennison's
XSLT and XPath on the Edge (M&T Books,
2001).
There are two small caveats, but they should not cause trouble in
most cases. The first is that the previous solution will not work
with Roman numerals using mixed case (e.g., IiI).
Such odd strings would hardly appear in reasonable data source, but
this code will neither reject such input nor arrive at the
"correct" value. Adding code to
convert to one case allows the code to reject or correctly process
these mixed Romans.
The second caveat relates to the fact that there is no standard Roman
representation for numbers higher than 1,000. Saxon and Xalan keep
stringing Ms together, but another processor might
do something else.
If for some reason you object to storing data about Roman numerals in
the stylesheet, then the following XPath 1.0 decodes a Roman numeral:
<xsl:variable name="roman-value"
select="($c = 'i' or $c = 'I') * 1 +
($c = 'v' or $c = 'V') * 5 +
($c = 'x' or $c = 'X') * 10 +
($c = 'l' or $c = 'L') * 50 +
($c = 'c' or $c = 'C') * 100 +
($c = 'd' or $c = 'D') * 500 +
($c = 'm' or $c = 'M') * 1000)"/>
In XSLT 2.0, you can use a nested if-then-else expression or use
a lookup within a sequence:
<xsl:variable name="roman-value" select="(1,5,10,50,100,500,1000)
[index-of(('I', 'V', 'X', 'L', 'C', 'D', 'M'),upper-case($c))]"/>
 |