Version 8 of Fixed-point arithmetic

Updated 2004-08-23 15:24:35

Arjen Markus (10 august 2004) "Real" numbers on computers can cause a lot of agony because they are implemented (most of the time) using binary rather than decimal arithmetic. Thus "obvious" computations like:

   0.1 + 0.1 == 0.2 

fail on most computer systems. There are many ways to solve these problems, though they almost always mean that you need another package or library than the default one - for instance arbitrary precision arithmetic packages.

An important class of applications where these problems are really annoying is that of financial computations. I am no expert in this field, but here is my thought: Why not use fixed-point arithmetic ?

The range for the numbers one deals with is fairly limited, the precision is too, but you do need "decimally" predictable results.

I have an implementation of the basic arithmetical operations in mind that should be easy to use. It is Tcl only, will not imply too much overhead as most will be done using integer computations and ought to be flexible enough for most such applications. I have not coded it yet, just thought it over and I want to use this Wiki page to see if there is any interest in it ...


AM (19 august 2004) I am slowly making progress with this little package. I do have a new addition procedure, but I want to make things more modular to save code and avoid mistakes. That is on its way ...


In answer to the question Why not use fixed-point arithmetic ?, why not use rational numbers, where the numerator and denominator are separately maintained? I believe some Scheme implementations can do that -- escargo 19 Aug 2004 (I was always amazed how close the rational approximation for pi, 355/113, came to the correct value.)

Lars H: Rational arithmetic suffers from the "combinatorial explosion"; the number of digits in the representation is typically doubled with every operation. Unless you want to compute an exact value, you probably don't want that. (As for pi having good rational approximations, there is another side also of that coin. The common proofs that e and pi are transcendental numbers are based on a lemma that non-rational algebraic numbers cannot be that easily approximated using rational numbers.)

AM I agree with Lars here. Furthermore: being able to represent 1/3 exactly does not solve the problem of how to represent it in a decimal way .... It is easier to see the magnitude of 0.33 than of 1740/5742.

escargo 23 Aug 2004 - Good answers. I have used fixed-point arithmetic on processors that had integer multiply/divide but no floating point. One of the problems using it was that the binary point was programmer maintained. You were OK doing addition/subtraction, but when doing multiplication and division, you had to normalize results yourself. There are times when doing decimal arithmetic in BCD really does seem like the best way. (While I think rational numbers have their place, I was interested in arguments for and against them, rather than being solely for them.)


Here is a very first version of such a package ... (It surprised me how difficult it is to adequately insert the decimal point ;)

 # fixedpoint.tcl --
 #    Implement fixed-point arithmetic
 #

 # fixedpoint --
 #    Namespace for the procedures and variables
 #
 namespace eval ::math::fixedpoint {
    variable precision 3
 }

 # fixed --
 #    Convert a numerical value (interpreted as string!) to a fixed-point
 #    number using the current precision
 #
 # Arguments:
 #    strval        String representation of the value
 #
 # Result:
 #    A fixed-point number
 #
 proc ::math::fixedpoint::fixed { strval } {
    variable precision

    if { ![string is double -strict $strval] } {
       return -code error "Argument must be a valid number"
    }

    #
    # Find the decimal point (if any) and the exponent (if any)
    #
    set p [string first . $strval]
    set e [string first e $strval]
    set E [string first e $strval]

    set newval $strval
    set expon  0
    if { $e != -1 } {
       set newval [string range $strval 0 [expr {$e-1}]]
       set expon  [string range $strval [expr {$e+1}] end]
    }
    if { $E != -1 } {
       set newval [string range $strval 0 [expr {$E-1}]]
       set expon  [string range $strval [expr {$E+1}] end]
    }

    if { $p != -1 } {
       set whole [string range $newval 0 [expr {$p-1}]]
       set fract [string range $newval [expr {$p+1}] end]
    }

    set expon [expr {$precision+$expon-[string length $fract]}]

    while { $expon > 0 } {
       set fract "${fract}0"
       incr expon -1
    }
    if { $expon < 0 } {
       set fract [string range $fract 0 end$expon]
       # TODO: insert proper rounding ...
    }

    return [list [expr wide($whole$fract)] $precision]
 }

 # tostring --
 #    Convert a fixed-point number to a string
 #
 # Arguments:
 #    fixedval      Fixed-point number
 #
 # Result:
 #    A string with the decimal representation
 #
 proc ::math::fixedpoint::tostring { fixedval } {

    set m [lindex $fixedval 0]

    if { $m == 0 } {
       return "0."
    }

    set p [lindex $fixedval 1]
    set sign ""

    if { $m < 0 } {
       set sign "-"
       set m [expr {-$m}]
    }
    if { [string length $m] < $p } {
       set m "[string repeat 0 [expr {$p-[string length $m]}]]$m"
       set sign "${sign}0"
    }

    return $sign[string range $m 0 end-$p].[string range $m end-[expr {$p-1}] end]
 }

 # + --
 #    Add two fixed-point numbers
 #
 # Arguments:
 #    op1           First operand
 #    op2           Second operand
 #
 # Result:
 #    The sum of the two
 #
 proc ::math::fixedpoint::+ { op1 op2 } {

    # TODO: handle different precisions
    # TODO: handle overflow
    # TODO: handle unary plus
    set m1 [lindex $op1 0]
    set m2 [lindex $op2 0]
    set p  [lindex $op1 1]
    return [list [expr {$m1+$m2}] $p]
 }

 # - --
 #    Subtract two fixed-point numbers
 #
 # Arguments:
 #    op1           First operand
 #    op2           Second operand
 #
 # Result:
 #    The difference of the two
 #
 proc ::math::fixedpoint::- { op1 op2 } {

    # TODO: handle different precisions
    # TODO: handle overflow
    # TODO: handle unary minus
    set m1 [lindex $op1 0]
    set m2 [lindex $op2 0]
    set p  [lindex $op1 1]
    return [list [expr {$m1-$m2}] $p]
 }

 set tcl_precision 17
 puts [::math::fixedpoint::fixed "1.01"]
 puts [::math::fixedpoint::fixed "1.011e3"]
 puts [::math::fixedpoint::fixed "1.0112222222e3"]
 puts [::math::fixedpoint::tostring [::math::fixedpoint::fixed "1.0112222222e3"]]

 set op1 [::math::fixedpoint::fixed "1.01"]
 set op2 [::math::fixedpoint::fixed "1.021"]
 puts [::math::fixedpoint::tostring [::math::fixedpoint::+ $op1 $op2]]
 puts [expr {1.01+1.021}]
 puts [::math::fixedpoint::tostring [::math::fixedpoint::- $op1 $op2]]
 puts [expr {1.01-1.021}]

[ Category Mathematics ]