Fixed-point arithmetic

Difference between version 28 and 29 - Previous - Next
[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.)

[KBK] ''2005-12-23'' Hmm, how'd I miss this page the first time around?  Anyway, one comment about rationals and the combinatorial explosion; it is real.  The simplest example is (1 + sqrt(5))/2 = 1.618.  The convergents for the continued fraction representation of this number are ratios of successive Fibonacci
numbers:

   2/1, 3/2, 5/3, 8/5, 13/8, ...

and numerator and denominator grow exponentially.  Any quadratic surd that is not an integer has a similar problem.  (Incidentally, approximating surds by Newton-Raphson iteration yields precisely the same convergents.)

The common case for fixed point is in financial calculations, which have well-defined rounding conventions (and numbers of decimal digits to retain) in accounting practice.  
----
Here is a second version of such a package ... (It surprised me how difficult it is to adequately insert the decimal point ;) 

(More modular in set-up than the first one, but still it lacks proper rounding - I want to get a look at the source code on paper before moving on to tackle that problem ;)
-- [AM] 6 september 2004

----

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

 # fixedpoint --
 #    Namespace for the procedures and variables
 #
 namespace eval ::math::fixedpoint {
    variable std_precision 3
    variable scale {1 10 100 1000 10000 100000 1000000
                    100000000 1000000000 10000000000}
    variable scale_current
    variable rounding_method

 }

 # setprecision --
 #    Set the standard precision for computations
 #
 # Arguments:
 #    newvalue      Integer between 0 and 9, giving the number
 #                  of decimals to work with (optional)
 # Result:
 #    Returns the standard precision (also if no argument given)
 # Side effect:
 #    The variable std_precision is set along with several others
 #
 proc ::math::fixedpoint::setprecision { {newvalue {}} } {
    variable std_precision
    variable scale
    variable scale_current

    if { $newvalue == {} } {
       return $std_precision
    }
    if { $newvalue < 0 || $newvalue > 9 } {
       return -code error "Number of decimals must be between 0 and 9"
    }

    set scale_current [expr {wide([lindex $scale $std_precision])}]

    return $std_precision
 }

 # setrounding --
 #    Set the rounding method
 #
 # Arguments:
 #    newname       Name of method (optional):
 #                  round-up, round-to-zero, floor, ceil, round-to-even
 # Result:
 #    Returns the rouding method (also if name not given)
 # Side effect:
 #    The variable rounding_method is set
 #
 proc ::math::fixedpoint::setrounding { {newname {}} } {
    variable rounding_method

    if { $newname == {} } {
       return $rounding_method
    }
    switch -- $newname {
    "round-up" -
    "round-to-zero" -
    "floor" -
    "ceil" -
    "round-to-even" {
       set rounding_method $newname
    }
    default {
       return -code error "Rounding method must be one of:
    round-up, round-to-zero, floor, ceil or round-to-even"
    }
    }

    return $rounding_method
 }

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

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

    if { $precision == {} } {
       set precision $std_precision
    }

    #
    # 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]
 }

 # SplitNumber --
 #    Split the encoded number in an integral and a fractional part
 #
 # Arguments:
 #    fixed          Fixed-point number
 #    int            Name of variable to hold integral part
 #    fract          Name of variable to hold fractional part
 #    scale_factor   Name of variable to hold scale factor
 #
 # Result:
 #    int and fract are set
 #
 proc ::math::fixedpoint::SplitNumber { fixed int fract scale_factor} {
    variable scale

    upvar 1 $int          Int
    upvar 1 $fract        Fract
    upvar 1 $scale_factor ScaleFactor

    set m [lindex $fixed 0]
    set p [lindex $scale [lindex $fixed 1]]

    set Int         [expr {($m>=0)? $m/$p : -(-$m/$p)}]
    set Fract       [expr {($m>=0)?$m%$p:(-(-$m%$p))}]
    set ScaleFactor $p
 }

 # RoundOff --
 #    Round off to the given precision
 #
 # Arguments:
 #    int            Name of variable holding integral part
 #    fract          Name of variable holding fractional part
 #    scale_factor   Scale factor
 #
 # Result:
 #    int and fract are set
 #
 proc ::math::fixedpoint::RoundOff { int fract scale_factor } {
    variable rounding_method
    variable std_precision
    variable scale

    upvar 1 $int          Int
    upvar 1 $fract        Fract

    if { abs($Fract) > 0.5*$scale_factor } {
       if { $Fract >= 0 } {
          set Fract [expr {int($Fract/$scale_factor+0.5)}]
       } else {
          set Fract [expr {int($Fract/$scale_factor-0.5)}]
       }
    } else {
       set Fract [expr {int($Fract/$scale_factor)}]
    }
 }

 # + --
 #    Add two fixed-point numbers
 #
 # Arguments:
 #    op1           First operand
 #    op2           Second operand
 #
 # Result:
 #    The sum of the two
 #
 proc ::math::fixedpoint::+ { op1 {op2 {}} } {
    variable std_precision
    variable scale
    variable scale_current
    if { [llength $op2] == 0 } {
       return $op1
    }

    # TODO: handle different precisions
    # TODO: handle overflow

    SplitNumber $op1 I1 F1 p1
    SplitNumber $op2 I2 F2 p2
    if { $p1 > $p2 } {
       set F2 [expr {$F2*$p1/$p2}]
    }
    if { $p1 < $p2 } {
       set F1 [expr {$F1*$p2/$p1}]
       set p1 $p2
   }

    set I [expr {$I1+$I2}]
    set F [expr {$F1+$F2}]

    if { $p1 > $scale_current } {
       set reduce [expr {$p1/$scale_current}]
       RoundOff I F $reduce
       # TODO: rounding
    }
    if { $p1 < $scale_current } {
       set enlarge [expr {$scale_current/$p1}]
       set F [expr {$F*$enlarge}]
    }
    set I [expr {wide($I)*$scale_current+$F}]

    return [list $I $std_precision]
 }

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

    if { $op2 == {} } {
       foreach {m p} $op1 {break}
       return [list [expr {-$m}] $p]
    } else {
       foreach {m p} $op2 {break}
       return [+ $op1 [list [expr {-$m}] $p]]
    }
 }

 # * --
 #    Multiply two fixed-point numbers
 #
 # Arguments:
 #    op1           First operand
 #    op2           Second operand
 #
 # Result:
 #    The product of the two
 #
 proc ::math::fixedpoint::* { op1 op2 } {
    variable std_precision
    variable scale
    variable scale_current

    # TODO: handle different precisions
    # TODO: handle overflow

    SplitNumber $op1 I1 F1 p1
    SplitNumber $op2 I2 F2 p2
    if { $p1 > $p2 } {
       set F2 [expr {$F2*$p1/$p2}]
    }
    if { $p1 < $p2 } {
       set F1 [expr {$F1*$p2/$p1}]
       set p1 $p2
   }

    set I [expr {$I1*$I2}]
    set F [expr {$I1*$F2+$I2*$F1+($F1*$F2)/$p1}]

    if { $p1 > $scale_current } {
       set reduce [expr {$p1/$scale_current}]
       RoundOff I F $reduce
       # TODO: rounding
    }
    if { $p1 < $scale_current } {
       set enlarge [expr {$scale_current/$p1}]
       set F [expr {$F*$enlarge}]
    }
    set I [expr {wide($I)*$scale_current+$F}]

    return [list $I $std_precision]
 }

 # /--
 #    Divide two fixed-point numbers
 #
 # Arguments:
 #    op1           First operand
 #    op2           Second operand
 #
 # Result:
 #    The quotient of the two
 #
 proc ::math::fixedpoint::/ { op1 op2 } {
    variable std_precision
    variable scale
    variable scale_current

    # TODO: handle different precisions
    # TODO: handle overflow

    SplitNumber $op1 I1 F1 p1
    SplitNumber $op2 I2 F2 p2
    if { $p1 > $p2 } {
       set F2 [expr {$F2*$p1/$p2}]
    }
    if { $p1 < $p2 } {
       set F1 [expr {$F1*$p2/$p1}]
       set p1 $p2
    }

    set I  [expr {($I1*$p1+$F1)/($I2*$p1+$F2)}]
    set F  [expr {($p1*($I1*$p1+$F1-$I*($I2*$p1+$F2)))/($I2*$p1+$F2)}]

    if { $p1 > $scale_current } {
       set reduce [expr {$p1/$scale_current}]
       RoundOff I F $reduce
       # TODO: rounding
    }
    if { $p1 < $scale_current } {
       set enlarge [expr {$scale_current/$p1}]
       set F [expr {$F*$enlarge}]
    }
    set I [expr {wide($I)*$scale_current+$F}]

    return [list $I $std_precision]
 }


 # initialisation --
 #
 namespace eval ::math::fixedpoint {
    setprecision 3
    setrounding  round-up
 }

 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}]

 puts [::math::fixedpoint::tostring [::math::fixedpoint::+ $op1 {10210 2}]]

 puts "Sums:"
 puts "Fixed -- floating-point"
 foreach {x1 x2} {1.01 1.021 1.1 1.2 2.3 4.5 1.99 2.01 20.3 0.001} {
    set op1 [::math::fixedpoint::fixed $x1]
    set op2 [::math::fixedpoint::fixed $x2]
    puts "$x1+$x2: \
  [::math::fixedpoint::tostring [::math::fixedpoint::+ $op1 $op2]] \
 -- [expr {$x1+$x2}]"
 }

 puts "Differences:"
 puts "Fixed -- floating-point"
 foreach {x1 x2} {1.01 1.021 1.1 1.2 2.3 4.5 1.99 2.01 20.3 0.001} {
    set op1 [::math::fixedpoint::fixed $x1]
    set op2 [::math::fixedpoint::fixed $x2]
    puts "$x1-$x2: \
  [::math::fixedpoint::tostring [::math::fixedpoint::- $op1 $op2]] \
 -- [expr {$x1-$x2}]"
 }

 puts "Products:"
 puts "Fixed -- floating-point"
 foreach {x1 x2} {1.01 1.021 1.1 1.2 2.3 4.5 1.99 2.01 20.3 0.001} {
    set op1 [::math::fixedpoint::fixed $x1]
    set op2 [::math::fixedpoint::fixed $x2]
    puts "$x1*$x2: \
  [::math::fixedpoint::tostring [::math::fixedpoint::* $op1 $op2]] \
 -- [expr {$x1*$x2}]"
 }
 puts "Quotients:"
 puts "Fixed -- floating-point"
 foreach {x1 x2} {1.021 1.01 1.1 1.2 2.3 4.5 1.99 2.01 20.03 0.001} {
    set op1 [::math::fixedpoint::fixed $x1]
    set op2 [::math::fixedpoint::fixed $x2]
    puts "$x1/$x2: \
  [::math::fixedpoint::tostring [::math::fixedpoint::/ $op1 $op2]] \
 -- [expr {$x1/$x2}]"
 }

----
[AM] (20 december 2005) Here is a much simpler approach (it is also less complete, but I know how to proceed):

 # decimal.tcl --
 #     Dealing with decimal arithmetic
 #
 #     Note:
 #     - This is a limited implementation
 #     - After using "fixed-precision" all existing "decimal numbers"
 #       become invalid, so use it at initialisation only to control
 #       the precision (and the range)
 #

 # namespace --
 #     Define the DecimalArithmetic namespace
 #
 package require Tcl 8.4

 namespace eval ::DecimalArithmetic {
     variable _precision_
     variable _maxvalue_
     variable _tofrac_
     namespace export fixed-precision + < > >= == <= < != \
                      tostring fromstring
 }

 # fixed-precision --
 #     Set the precision and compute the auxiliary parameters
 # Arguments:
 #     prec        Number of digits in a fraction
 # Result:
 #     None
 # Side effect:
 #     Auxiliary parameter set - see also the note at the start
 #
 proc ::DecimalArithmetic::fixed-precision {prec} {
     variable _precision_
     variable _maxvalue_
     variable _tofrac_

     set _precision_ $prec
     set _maxvalue_  [expr {pow(10.0,18-$prec)}]
     set _tofrac_    [expr "wide(1[string repeat 0 $prec])"]
 }

 # comparisons --
 #     Define the comparison procedures
 #
 namespace eval ::DecimalArithmetic {
     foreach op {< > >= == <= < !=} {
         proc $op {op1 op2} [string map [list OP $op] \
             {expr {$op1 OP $op2}}]
     }
 }

 # default precision --
 #     Define the comparison procedures
 #
 namespace eval ::DecimalArithmetic {
     fixed-precision 8
 }

 # + --
 #     Add two decimal numbers
 # Arguments:
 #     op1         First operand
 #     op2         Second operand
 # Result:
 #     Sum of the two or Inf
 # Note:
 #     Take care not to overflow the range!
 #
 proc ::DecimalArithmetic::+ {op1 op2} {
     variable _maxvalue_

     if { $op1 == "Inf" || $op2 == "Inf" } {
         return "Inf"
     }
     if { abs(double($op1)+double($op2)) > $_maxvalue_ } {
         return "Inf"
     }

     return [expr {$op1+$op2}]
 }

 # tostring --
 #     Convert a decimal number to a string ("1.001" for instance)
 # Arguments:
 #     number      Number to be converted
 # Result:
 #     Decimal representation of the number
 #
 proc ::DecimalArithmetic::tostring {number} {
     variable _precision_

     if { $number == "Inf" } {
         return "Inf"
     }

     return "[string range $number 0 end-$_precision_].[string range $number end-[expr {$_precision_-1}] end]"
 }

 # fromstring --
 #     Convert a string to a decimal number ("1.001" for instance)
 # Arguments:
 #     string      String to be converted
 # Result:
 #     Decimal number equivalent to the string (in current precision)
 #
 proc ::DecimalArithmetic::fromstring {string} {
     variable _precision_
     variable _maxvalue_
     variable _tofrac_

     if { ! [string is double $string] } {
         return "Inf"
     }

     foreach {int frac} [split $string "."] {break}

     if { $int > $_maxvalue_ } {
         return "Inf"
     }

     set frac [string range "$frac[string repeat 0 $_precision_]" 0 [expr {$_precision_-1}]]
     set frac [string triml $frac 0]
     if { $frac == "" } {
         set frac "0"
     }
     set frac [expr {wide($frac)}]

     if { $int >= 0 } {
         return [expr {wide($int)*$_tofrac_+$frac}]
     } else {
         return [expr {wide($int)*$_tofrac_-$frac}]
     }
 }

 # main --
 #     Test the procedures ...
 #
 namespace import ::DecimalArithmetic::*

 set x   0.0
 set dx  1.01
 set y   [fromstring 0.0]
 set dy  [fromstring 1.01]

 set tcl_precision 17
 for { set i 0 } { $i < 10 } { incr i } {
    set x [expr {$x+$dx}]
    set y [+ $y $dy]
    puts "$x -- [tostring $y]"
 }
======

----
''[escargo] 20 Dec 2005'' - It is the normalizing of the decimal point (or binary point) after
multiplication and division that are the true tests of fixed-point arithmetic.  And fixed-point
does not imply decimal; it can just as easily be a binary point as a decimal point.

[AM] Hm, the above strategy makes it possible to work with any radix ... Though I can not foresee the usefulness of a radix like 17, it would work ... The main reason for doing it like
this is that it is much simpler than my first idea.

''[escargo]'' - I guess using a DecimalArithmetic namespace made me think that nondecimal radix (or
binary radix) was less supported.  (I used to use some computers that used a radix 40 character set;
it allowed for 26 letters, 10 digits, a space character and three other characters (",", "$", ".",
if memory serves) in the character set.  This let them pack 3 characters in a 16-bit word.)
----
[Sarnold] What if I want 3.2571% of 17000.00$ rounded to 2 decimals? Could the above scripts handle this?

[AM] It should be possible (rounding is not supported yet, but there is no essential limitation) - as long as you stay within the limits of the precision.

[beernutmark] 2011-05-02 - [Decimal Arithmetic Package for tcl 8.5] handles this easily with 8 rounding modes to choose from. 
 puts "[::Decimal::tostr [::Decimal::round_half_even [::Decimal::/ [::Decimal::* [::Decimal::fromstr 3.2571] [::Decimal::fromstr 17000.00]] $::Decimal::onehundred] 2]]"

553.71

<<categories>> Mathematics | Currency and Finance