Testing Ruby's floats precision

Posted by Ilija Eftimov on July 21, 2015

Float precision in Ruby is a well known quirk. But when testing floats, not many
of us bother to remember this and make their tests respectful to this quirk. In
this post we will see how the popular Ruby testing frameworks help us test Floats
properly.

Ruby Float (im)precision

Float numbers cannot store decimal numbers properly. The reason is that Float
is a binary number format. What do I mean? Well, Ruby always converts Floats from
decimal to binary and vice versa.

Think about this very simple example. Whats the result of 1 divided by 3? Yup, 0.33333333333…
The result of this calculation is 0.333(3), with 3 repeating until infinity.

This same rule, or quirk, applies to binary numbers. When a decimal number is converted
to binary, the resulting binary number can be endless. Mathematically this is all fine.
But in practice, my MacBook Air doesn’t have endless memory. I am running just on
4GBs of RAM, so Ruby must cut off the endless number at some point. Or it will fill
up the whole memory of the computer and it will become useless.
This mechanism of rounding numbers produces a rounding error and that’s exactly
what we have to deal with here.

So, the base rule about this is: do not represent currency (or, money) with Float.

In practice

Take this for an example. Simple calculation. We want to add 0.1 to 0.05, which should
return 0.15. Right? Lets give it a try:

>>0.1+0.05==0.15=>false

Okay, what? Let’s see what’s the result of the addition:

>>0.1+0.05=>0.15000000000000002

You can see that Ruby rounds off the number at the end. Lets print this number with 50 decimal points:

So, assert_in_epsilon is a wrapper for assert_in_delta with a small but
important difference. The delta here is subject to “auto-scaling”. This means that
it will increase for the product of the smaller number from the expected value/actual
value pair and the epsilon.

Also, I guess, it’s called “epsilon” because the greek letter Epsilon is usually used to
denote a small quantity (like a margin of error) or perhaps a number which will be
turned into a zero within some limit.

With RSpec

RSpec provides us the be_within matcher. The same rules apply here as the Minitest
assert_in_delta method. The format is:

expect(<actual>).tobe_within(<delta>).of(<expected>)

Or, in our case:

it"should match within a delta"doexpect(0.10+0.05).tobe_within(0.0000000000000001).of(0.15)end

Conclusion

Since the people behind RSpec and Minitest are awesome, they have provided us these
methods where we can easily smooth out the edges of testing floats. What’s very
important to understand here is that while testing this is pretty easy, it’s
extremely important to know what to use when.

When it comes to money/currency, every sane developer out there will use BigDecimal.
It provides arbitrary-precision floating point decimal arithmetic, which means that
it will always get a correct result for any calculation involving floating point numbers.