Version 4 of Decimal arithmetic

Updated 2010-03-24 12:49:02 by arjen

Sarnold 2005-12-22

Here is a package for decimal arithmetic with math::bignum in pure Tcl.

Usage:

 % namespace import ::math::decimal::*
 % set a [fromstr 0.150]
 % tostr [round $a 1]
 0.2
 % set b [fromstr 23945.59]
 % tostr [- $b $a]
 23945.44
 % set product [* $a $b]
 % tostr $product
 3591.83850
 % tostr [round $product [precision $b]]
 3591.84

The source

 package require math::bignum

 # this line helps when I want to source this file again and again
 catch {namespace delete ::math::decimal}

 # private namespace
 # this software works only with Tcl v8.4 and higher
 # it is using the package math::bignum
 namespace eval ::math::decimal {
     # cached constants
     # 10 as a bignum
     variable Ten
     set Ten [::math::bignum::fromstr 10]
     variable One
     set One [::math::bignum::fromstr 1]
 }




 ################################################################################
 # procedures that handle decimal numbers
 # these procedures are sorted by name (after eventually removing the underscores)
 #
 # Decimal numbers are stored as a list:
 # {"decimal" Mantissa Exponent Power} where "decimal" is a tag
 # Mantissa and Power are bignums
 # Exponent is an integer
 #
 # The resulting value is Mantissa*10^Exponent or Mantissa*Power, both results are good.
 #
 ################################################################################


 #
 # returns the absolute value
 #
 proc ::math::decimal::abs {number} {
     lset number 1 [::math::bignum::abs [lindex $number 1]]
     return $number
 }


 #
 # returns A + B
 #
 proc ::math::decimal::+ {a b} {
     if {[lindex $a 2]<[lindex $b 2]} {
         return [+ $b $a]
     }
     # retrieving parameters from A and B
     foreach {dummy integerA expA powA} $a {break}
     foreach {dummy integerB expB powB} $b {break}
     if {$expA>$expB} {
         set pow [::math::bignum::div $powA $powB]
         lset b 1 [::math::bignum::add [::math::bignum::div $integerA $pow] $integerB]
         return $b
     } elseif {$expA==$expB} {
         # nothing to shift
         lset a 1 [::math::bignum::add $integerA $integerB]
         return $a
     }
     error "internal error"
 }





 #
 # rounds to ceiling the number, with a given precision
 #
 proc ::math::decimal::ceil {number {precision 0}} {
     foreach {dummy integer exp pow} $number {break}
     if {$exp<$precision} {
         error "not enough precision to perform ceil"
     }
     if {$exp==$precision} {
         return $number
     }
     lset number 2 $precision
     variable Ten
     set divider [::math::bignum::pow $Ten [::math::bignum::fromstr [expr {$exp-$precision}]]]
     lset number 3 [::math::bignum::div $pow $divider]
     # saving the sign ...
     set sign [::math::bignum::sign $integer]
     if {$sign} {
         # negative number: return the floor
         lset number 1 [::math::bignum::div $integer $divider]
         return $number
     }
     # decimal part
     foreach {integer remainder} [::math::bignum::divqr $integer $divider] {break}
     # fractional part
     if {![::math::bignum::iszero $remainder]} {
         lset number 1 [::math::bignum::add 1 $integer]
     } else  {
         lset number 1 $integer
     }
     return $number
 }



 #
 # returns 0 if A = B , otherwise returns the sign of A-B
 # IN : a, b (Decimals)
 #
 proc ::math::decimal::compare {a b} {
     if {[lindex $a 2]<[lindex $b 2]} {
         return [expr {-[compare $b $a]}]
     }
     # retrieving data
     foreach {dummy aint aexp apow} $a {break}
     foreach {dummy bint bexp bpow} $b {break}
     if {$aexp==$bexp} {
         return [::math::bignum::compare $aint $bint]
     }
     set diff [expr {$aexp-$bexp}]
     set aint [::math::bignum::div $aint [::math::bignum::div $apow $bpow]]
     if {[::math::bignum::compare $aint $bint]>=0} {
         return 1
     }
     return -1
 }



 #
 # divide A by B and returns the result
 # throw error : when dividing by zero
 #
 proc ::math::decimal::/ {a b} {
     foreach {dummy integerA expA powA} $a {break}
     foreach {dummy integerB expB powB} $b {break}
     # dividing any number by zero throws an error
     if {[::math::bignum::iszero $integerB]} {
         error "divide by zero"
     }
     # aE-ea / bE-eb = (a/b)E(eb-ea) 
     # a/pa / b/pb = (a/b)/(pa/pb)
     # now ea=expA, eb=expB, pa=powA and pb=powB
     if {$expB>$expA} {
         error "not enough precision in the numerator"
     }
     lset a 2 [expr {$expA-$expB}]
     lset a 3 [::math::bignum::div $powA $powB]
     lset a 1 [::math::bignum::div $integerA $integerB]
     return $a
 }





 #
 # procedure floor : rounds to floor with a given precision (number of decimals)
 # returns a Decimal number
 #
 proc ::math::decimal::floor {number {precision 0}} {
     return [opp [ceil [opp $number] $precision]]
 }


 #
 # returns a list formed by an integer and a precision (decimals number)
 # x = A * 10 power (-B) = A/C
 # return [list "decimal" A B C] (where "decimal" is the tag)
 #
 proc ::math::decimal::fromstr {number} {
     set number [string trimleft $number +]
     set sign 0
     if {[string index $number 0]=="-"} {
         set sign 1
         set number [string range $number 1 end]
     }
     # a floating-point number may have a dot
     set tab [split $number .]
     set exp 0
     if {[llength $tab]>2} {error "syntax error in number"}
     if {[llength $tab]==2} {
         set number [join $tab ""]
         if {![string is digit $number]} {
             error "not a number"
         }
         # increment by the number of decimals (after the dot)
         set exp [string length [lindex $tab 1]]
     }
     set number [::math::bignum::fromstr $number]
     ::math::bignum::setsign number $sign
     if {!$exp} {
         variable One
         return [list decimal $number 0 $One]
     }
     variable Ten
     return [list decimal $number $exp [::math::bignum::pow $Ten [::math::bignum::fromstr $exp]]]
 }

 #
 # returns 1 if A is null, 0 otherwise
 #
 proc ::math::decimal::iszero {a} {
     return [::math::bignum::iszero [lindex $a 1]]
 }



 #
 # returns A modulo B (like with fmod() math function)
 #
 proc ::math::decimal::% {a b} {
     set ratio [/ $a $b]
     # examples : fmod(3,2)=1 ratio=1.5
     # fmod(1,2)=1 ratio=0.5
     # ratio>0 and b>0 : get floor(ratio)
     # fmod(-3,-2)=-1 ratio=1.5
     # fmod(-1,-2)=-1 ratio=0.5
     # ratio>0 and b<0 : get floor(ratio)
     # fmod(-3,2)=-1 ratio=-1.5
     # fmod(-1,2)=-1 ratio=-0.5
     # ratio<0 and b>0 : get ceil(ratio)
     # fmod(3,-2)=1 ratio=-1.5
     # fmod(1,-2)=1 ratio=-0.5
     # ratio<0 and b<0 : get ceil(ratio)
     if {[sign $ratio]} {
         set ratio [ceil $ratio]
     } else  {
         set ratio [floor $ratio]
     }
     return [- $a [* $ratio $b]]
 }

 #
 # returns the product of A by B
 #
 proc ::math::decimal::* {a b} {
     foreach {dummy integerA expA powA} $a {break}
     foreach {dummy integerB expB powB} $b {break}
     return [list decimal [::math::bignum::mul $integerA $integerB] \
             [expr {$expA+$expB}] [::math::bignum::mul $powA $powB]]
 }



 #
 # given a decimal number A, returns -A
 #
 proc ::math::decimal::opp {a} {
     set integer [lindex $a 1]
     ::math::bignum::setsign integer [expr {![::math::bignum::sign $integer]}]
     lset a 1 $integer
     return $a
 }

 #
 # returns the precision of some decimal number
 #
 proc ::math::decimal::precision {a} {
     return [lindex $a 2]
 }

 #
 # returns $number rounded to the given precision
 #
 proc ::math::decimal::round {number {precision 0}} {
     if {$precision<0} {
         error "negative second argument, cannot round"
     }
     foreach {dummy integer exp power} $number {break}
     if {$exp<$precision} {
         error "not enough precision to round"
     }
     if {$exp==$precision} {
         return $number
     }
     lset number 2 $precision
     variable Ten
     # divider := 10^(old_precision-new_precision)/2
     #         := $power/10^$precision/2
     #         := $power/$new_power >> 1
     # old_integer/divider := new_integer*2 + carry
     # if carry=1 then increment new_integer
     lset number 3 [set new_power [::math::bignum::pow $Ten [::math::bignum::fromstr $precision]]]
     set divider [::math::bignum::rshift [::math::bignum::div $power $new_power] 1]
     # saving the sign, ...
     set sign [::math::bignum::sign $integer]
     set integer [::math::bignum::abs $integer]
     set integer [::math::bignum::div $integer $divider]
     # first bit 
     set carry [::math::bignum::testbit $integer 0]
     set integer [::math::bignum::rshift $integer 1]
     if {$carry} {
         set integer [::math::bignum::add 1 $integer]
     }
     # ... restore the sign now
     ::math::bignum::setsign integer $sign
     lset number 1 $integer
     return $number
 }


 #
 # gets the sign of a decimal number
 # we keep the bignum convention : 0 for positive, 1 for negative
 #
 proc ::math::decimal::sign {a} {
     return [::math::bignum::sign [lindex $a 1]]
 }





 #
 # substracts B to A
 #
 proc ::math::decimal::- {a b} {
     return [+ $a [opp $b]]
 }

 #
 # converts a decimal number stored as a list to a string
 #
 proc ::math::decimal::tostr {number} {
     foreach {dummy integer exp delta} $number {break}
     if {$exp<0} {
         error "negative decimals number"
     }
     set integer [::math::bignum::tostr $integer]
     set sign ""
     if {[string index $integer 0]=="-"} {
         set sign -
         set integer [string range $integer 1 end]
     }
     set len [string length $integer]
     if {$len<$exp+1} {
         set integer [string repeat 0 [expr {$exp+1-$len}]]$integer
     }
     return $sign[string range $integer 0 end-$exp].[string range $integer end-[incr exp -1] end]
 }
 # exporting public interface
 namespace eval ::math::decimal {
     foreach function {
         + - * / % abs opp precision
         compare fromstr tostr iszero
         round ceil floor
     } {
         namespace export $function
     }
 }

 package provide math::decimal 0.1

AM (24 march 2010) As Tcl 8.5 supports arbitrary-precision integers out-of-the-box, the math::bignum package is not needed anymore. We can get the above functionality more directly. Here is an (independent) implementation of decimal arithmetic. Note that it is not well-tested and for serious work, it should borrow stuff from above.

# decmath.tcl --
#     Simple package for dealing with decimal arithmetic.
#     Numbers are represented as:
#         mantisse * 10**exponent
#
#     Note:
#     Rounding has not been explicitly tested
#

# Decmath --
#     Namespace for the decimal arithmetic procedures
#
namespace eval ::Decmath {
    variable scale 10
    variable maxmantisse [expr {10**$scale}]
    variable bndmantisse [expr {10*$maxmantisse}]

    namespace export + - / * tostr fromstr setScale
}

# setScale --
#     Set the overall scale (maximum number of digits retained)
#
# Arguments:
#     newscale   New scale
#
# Result:
#     None
#
proc ::Decmath::setScale {newscale} {
    variable scale
    variable maxmantisse
    variable bndmantisse

    set scale       $newscale
    set maxmantisse [expr {10**$scale}]
    set bndmantisse [expr {10*$maxmantisse}]
}

# + --
#     Add two numbers
#
# Arguments:
#     a          First operand
#     b          Second operand
#
# Result:
#     Sum of both (rescaled)
#
proc ::Decmath::+ {a b} {
    foreach {ma ea} $a {break}
    foreach {mb eb} $b {break}

    if { $ea > $eb } {
        set ma [expr {$ma * 10 ** ($ea-$eb)}]
        set er $eb
    } else {
        set mb [expr {$mb * 10 ** ($eb-$ea)}]
        set er $ea
    }

    set mr [expr {$ma + $mb}]
    return [Rescale $mr $er]
}

# - --
#     Subtract two numbers (or unary minus)
#
# Arguments:
#     a          First operand
#     b          Second operand (optional)
#
# Result:
#     Sum of both (rescaled)
#
proc ::Decmath::- {a {b {}}} {

    if { $b == {} } {
        return [- {0 0} $a]
    } else {
        lset b 0 [expr {-[lindex $b 0]}]
        return [+ $a $b]
    }
}

# * --
#     Multiply two numbers
#
# Arguments:
#     a          First operand
#     b          Second operand
#
# Result:
#     Product of both (rescaled)
#
proc ::Decmath::* {a b} {
    foreach {ma ea} $a {break}
    foreach {mb eb} $b {break}

    set mr [expr {$ma * $mb}]
    set er [expr {$ea + $eb}]

    return [Rescale $mr $er]
}

# / --
#     Divide two numbers
#
# Arguments:
#     a          First operand
#     b          Second operand
#
# Result:
#     Quotient of both (rescaled)
#
proc ::Decmath::/ {a b} {
    variable scale
    variable maxmantisse

    foreach {ma ea} $a {break}
    foreach {mb eb} $b {break}

    set mr [expr {$ma * $maxmantisse / $mb}]
    set er [expr {$ea - $eb - $scale}]

    return [Rescale $mr $er]
}

# Rescale --
#     Rescale the number (using proper rounding)
#
# Arguments:
#     mantisse   Mantisse of the number
#     exponent   Exponent of the number
#
# Result:
#     Rescaled number (as a list)
#
proc ::Decmath::Rescale {mantisse exponent} {
    variable maxmantisse
    variable bndmantisse

    if { abs($mantisse) <= $maxmantisse } {
        return [list $mantisse $exponent]
    }

    while { abs($mantisse) > $bndmantisse } {
        set  mantisse [expr {$mantisse/10}]
        incr exponent
    }

    set rest [expr {$mantisse % 10}]

    if { $rest > 5 } {
        if { $mantisse > 0 } {
            incr mantisse
        } else {
            incr mantisse -1
        }
    } elseif { $rest == 5 } {
        if { ($mantisse/10) % 2 == 1 } {
            incr mantisse
        }
    }

    return [list $mantisse $exponent]
}

# tostr --
#     Convert number to string
#
# Arguments:
#     number     Number to be converted
#
# Result:
#     Number in the form of a string
#
proc ::Decmath::tostr {number} {

    foreach {mantisse exponent} $number {break}

    if { $exponent > 0 } {
        set string $mantisse[string repeat 0 $exponent]
    } else {
        set digits [string length $mantisse]
        set exponent [expr {-$exponent}]
        if { $digits >= $exponent } {
            set string [string range $mantisse 0 [expr {$digits-$exponent-1}]].[string range $mantisse [expr {$digits-$exponent}] end]
        } else {
            set string 0.[string repeat 0 [expr {$exponent-$digits}]]$mantisse
        }
    }
    return $string
}

# fromstr --
#     Convert string to number
#
# Arguments:
#     string      String to be converted
#
# Result:
#     Number in the form of {mantisse exponent}
#
proc ::Decmath::fromstr {string} {

    set pos [string first . $string]
    if { $pos < 0 } {
        set mantisse $string
        set exponent 0
    } else {
        set fraction [string range $string [expr {$pos+1}] end]
        set mantisse [string trimleft [string map {. ""} $string] 0]
        set exponent [expr {-[string length $fraction]}]
    }

    return [Rescale $mantisse $exponent]
}

# main --
#     Test it
#
namespace import ::Decmath::*

set a [fromstr 1.02]
set b [fromstr 2.1]
set c [fromstr 5.25]
puts "$a + $b = [+ $a $b] -- [tostr [+ $a $b]] -- (expr) [expr {1.02+2.1}]"
puts "$a * $b = [* $a $b] -- [tostr [* $a $b]] -- (expr) [expr {1.02*2.1}]"
puts "$c / $b = [/ $c $b] -- [tostr [/ $c $b]] -- (expr) [expr {5.25/2.1}]"

puts "0.001: [fromstr 0.001] -- [tostr {1 -3}]"

LV So, has anyone considered adding this to tcllib?

AM I have certainly thought about it, but it needs to mature a bit (I concocted my version last night, hardly a mature product :)). It will definitely be useful, but we need to know more about people's needs for this. (Financial computations are one obvious application area, but they pose very strict rules!)