Make your own free website on Tripod.com

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/Transform"
                      xmlns:xs="http://www.w3.org/2001/XMLSchema"
                      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>&#xa;</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[1]/@offset)
                    then
                      1 + $node/preceding-sibling::reg[1]/@offset
                    else
                      1+ func:getnum($node/preceding-sibling::reg[1], -1)"
      />
    </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..

Here is the code:

<xsl:template name="abs">
  <xsl:param name="n" />

  <xsl:choose>
     <xsl:when test="$n &gt;= 0">
       <xsl:value-of select="$n" />
     </xsl:when>
     <xsl:otherwise>
       <xsl:value-of select="0 - $n" />
     </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Call this as:

1)
<xsl:call-template name="abs">
   <xsl:with-param name="n" select="3" />
</xsl:call-template>

Will produce output: 3

2)
<xsl:call-template name="abs">
  <xsl:with-param name="n" select="-10" />
</xsl:call-template>

Will produce output: 10

David Carlisle suggested:

or even just select="number(translate(number(.),'-',''))"