“Do not use floating point for currency”
This simple statement is useful for both novice and experienced developers alike. It avoids problems that might otherwise compromise the correctness of the software you’re building.
But behind every piece of wisdom, every history of success, there is hard work from lots of people. Exploring this background is an enriching experience, the kind of thing that elevates the level of knowledge and makes you more confident about what you are doing.
Do not expect to find here a complete guide to floating point or currency handling. There are enough references on each matter separately out there already. The idea is to provide a brief exploration in a top-down fashion, gradually digging each layer of the stack, from the perspective of someone that do not master floating point.
Let’s start with a simple example in irb (Ruby’s Interactive Shell), which happens to be a common source of questions:
0.1 + 0.02 == 0.12
# => false
(0.1 + 0.02) + 0.3 == 0.1 + (0.02 + 0.3)
# => false
Wasn’t it supposed to return true
? What’s going on behind the scenes?
Debugging the results from each side of the first expression.
0.1 + 0.02
# => 0.12000000000000001
0.12
# => 0.12
It looks like there is something wrong with the operation 0.1 + 0.02
. Lets take a closer look, using a more accurate format.
sprintf("%0.50f", 0.12)
# => "0.11999999999999999555910790149937383830547332763672"
sprintf("%0.50f", 0.10 + 0.02)
# => "0.12000000000000000943689570931383059360086917877197"
sprintf("%0.50f", 0.02)
# => "0.02000000000000000041633363423443370265886187553406"
sprintf("%0.50f", 0.10)
# => "0.10000000000000000555111512312578270211815834045410"
So the error happens even before the arithmetic operations. It’s time to dive deeper into the stack.
Conversions
Once the interpreter parses the decimal string 0.1
, for example, it converts that to an internal representation, which we’ll review briefly. This is achieved with the help of C strtod
function, in MRI.
Curiously, this conversion step had already lead to security issues before.
Another conversion takes place when printing a float back to the decimal string. These transformations are not as trivial as one might think. There had been many studies in the development of the algorithms used in order to make it accurately and efficiently.
Since all of this happens transparently, some might expect that all languages work this way by default. It does, but not always for free. For example, in the infant days of the Elixir language, a developer had the following question after typing a decimal string into IEx (Elixir’s Interactive Shell). This had been solved by borrowing functionality already present in Erlang.
A counter intuitive fact is that even if a decimal number is not exactly representable as float, say 0.12
, we are able to convert it back and forth without ever seeing the rounding error due to such carefully designed algorithms.
This automatic rounding behavior hides complexity but may encourage some to operate on floats expecting that the final conversion is going to provide a desired decimal result. But that is not always true due to rounding errors accumulation.
But what’s this internal representation and why does it impose such limitations?
Thinking in terms of binaries
We are not going to dive into the details of the IEEE754 nor explain how to do such conversions. That’s an interesting topic but it has been covered before and would end up being too extensive to fit in this post.
Instead we are going to show some examples that provides a glimpse of the real problem.
For instance, take the decimal number 0.625
. This number represents the sum of fractions 6/10^1 + 2/10^2 + 5/10^3
. The same reasoning can be used to find the binary representation replacing the denominator by powers of two 1/2^1 + 0/2^2 + 1/2^3 = 0.5 + 0.125 = 0.625
, which leads to the binary 0.101
when we place the numerators in a row.
But if we had chosen another number, let’s say the decimal 0.1
, we would find that it’s not possible to write it as a finite sum of fractions whose denominator is a power of 2. E.g. it does not have a finite binary representation.
Since the space to store a double precision float is limited to 64 bits, some sort of rounding would be required. And here we have one of the causes of precision loss.
What type should I use for representing currency?
The two most frequent answers include Integers or BigDecimals as the way to go. I’ve personally used both and it worked fine, but it would be too pretentious to say that one or another is the best for your problem.
Most Rails applications use databases. If that’s your case, it’s important to understand the limitations offered by the types available and the conversions done by the adapter during information storage and retrieval.
For example, Postgres recommends using its numeric type for currency, which Rails maps to BigDecimals. This type also supports the aggregation functions we are used to.
Other databases may require other approaches, like mongodb that does not have a type compatible with BigDecimal. But it doesn’t mean that it’s impossible to use BigDecimals with mongodb either.
Currency in cents, like $19.90 could be represented as the integer 1990
. It avoids precision problems and works well with most databases too. One inconvenience is formatting numbers as currency, but there are gems to make this task easier and coding your own helper methods is also an alternative.
Some may experience performance issues when using BigDecimals, but I’ve never faced such issues personally. One thing to be aware of is that BigDecimals are not as simple as Integers and I like to keep things as simple as possible.
Even floating point may be required under some contexts if numerical methods get involved. The most important thing is to be aware of the limitations and make informed decisions.
So have you ever had problems with currency handling? I’m really willing to hear about how you solve this problem. If you have an experience to share, please, leave a comment below or find me on twitter @rcillo.
References
-
Ruby Float documentation – http://www.ruby-doc.org/core-2.1.2/Float.html
-
Ruby BigDecimal documentation – http://www.ruby-doc.org/stdlib-2.1.2/libdoc/bigdecimal/rdoc/BigDecimal.html
-
How to perform binary division – http://www.exploringbinary.com/binary-division
-
Binary do decimal converter – http://www.binaryconvert.com
-
Good floating point tutorial – http://kipirvine.com/asm/workbook/floating_tut.htm
-
Money in Rails – http://stackoverflow.com/questions/1019939/ruby-on-rails-best-method-of-handling-currency-money
-
Canonical reference – http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
-
Another good reference on floats – http://www.toves.org/books/float
https://github.com/RubyMoney/money-rails makes running with Integers quite easy
Great. Thanks for the tip. It’s always good to have options
Thanks for the tip. It’s always good to have options
I published an article calling attention for the same (initial) point with days of difference.
If you find it worthy: http://blog.iriomk.com/post/98382401723/ruby-numeric-types-dos-and-do-nots