This article is a response to http://vladzloteanu.wordpress.com/2010/01/06/ruby-on-rails-interview-questions-update/
It is a VERY BAD IDEEA to use floating point arithmetics to deal with currency. In most of the programming languages. Basically, because you’ll end up loosing money :). And this (on the great majority of cases) is not desirable :) .
I’ll show you some magic (that you may try at home):
~$ ruby --version
ruby 1.8.7 (2009-06-12 patchlevel 174) [i486-linux]
So, what just happened?
O.K. Now a little computer science lesson.
Floating Point (vs Fixed Point)
Numbers are in general represented approximately to a fixed number of significant digits and scaled using anexponent. The base for the scaling is normally 2, 10 or 16. The typical number that can be represented exactly is of the form:
- significant digits × baseexponent
The term floating point refers to the fact that the radix point (decimal point, or, more commonly in computers, binary point) can “float”; that is, it can be placed anywhere relative to the significant digits of the number. This position is indicated separately in the internal representation, and floating-point representation can thus be thought of as a computer realization of scientific notation.
- number is converted to scientific notation, and then coded in: sign bit, exponent, significand
- the precision is not fixed
- much larger range of representable numbers (much more than a fixed point representation), at the cost of precision
Decimal to Floating Point conversion
In this representation, a large group of rational numbers can not be represented in binary with a fixed number of digits.
For example, if you would like to convert 0.7 in binary format (the example is from http://sandbox.mc.edu/~bennet/cs110/flt/dtof.html) you’ll end up having an ‘endless’ number (in fact, a repeating fraction, in binary).
0.7 × 2 = 1.4 1 Generate 1 and continue with the rest. 0.4 × 2 = 0.8 0 Generate 0 and continue. 0.8 × 2 = 1.6 1 Generate 1 and continue with the rest. 0.6 × 2 = 1.2 1 Generate 1 and continue with the rest. 0.2 × 2 = 0.4 0 Generate 0 and continue. 0.4 × 2 = 0.8 0 Generate 0 and continue. 0.8 × 2 = 1.6 1 Generate 1 and continue with the rest. 0.6 × 2 = 1.2 1 Generate 1 and continue with the rest. …
The reason why the process seems to continue endlessly is that it does. The number 7/10, which makes a perfectly reasonable decimal fraction, is a repeating fraction in binary, just as the faction 1/3 is a repeating fraction in decimal. (It repeats in binary as well.) We cannot represent this exactly as a floating point number. The closest we can come in four bits is .1011. Since we already have a leading 1, the best eight-bit number we can make is 1.1011.
A good simulator can be fount at http://babbage.cs.qc.edu/IEEE-754/Decimal.html
And now back to our issue..
Ruby floating point values are stored in binary format.
10.12 (decimal) can not be represented exactly in binary:
>> sprintf(“%0.50f”, 10.12)
The default conversion of float to string (e.g., for output) rounds
to 6 (or maybe 7) places and truncates trailing zeros:
>> sprintf(“%0.6f”, a).sub(/0+\Z/,”)
>> sprintf(“%0.50f”, 10.12 * 100 )
>> (10.12 * 100).to_i
A very usefull add/sub/mul/div can be found at http://www.ecs.umass.edu/ece/koren/arith/simulator/FPMul/ . A more detailed explanation from there:
A + 1.0100001111010111000010100011110101110000101000111101 *23 = 10.12 B + 1.1001000000000000000000000000000000000000000000000000 *26 = 100
A*B + 1.1111100111111111111111111111111111111111111111111111|011 *29
Postnormalization StepA*B + 1.1111100111111111111111111111111111111111111111111111|01 *29
Round to ZeroA*B + 1.1111100111111111111111111111111111111111111111111111 *29 = 1011.9999999999999
Wikipedia provides even more examples of accuracy problems:http://en.wikipedia.org/wiki/Floating_point#Accuracy_problems.