[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 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]" } ---- [[ [Category Mathematics] ]]