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.

## Background story

Last week I published a post about migrating a test suite from RSpec to Minitest. What was very interesting is that I got a mention on Twitter from Ryan Davis with an offer for a code review of the migration. Here’s the convo:

Ryan did the review for me, and one of his comments was:

Lets see why…

## 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:

```
>> sprintf("%0.50f", 0.10 + 0.05)
=> "0.15000000000000002220446049250313080847263336181641"
```

Whoa! You can see that the actual result of this addition is quite different from 0.15. Ruby here rounds off the numbers, because the difference is so “microscopic”.

If you are curious, here’s how the numbers 0.10 and 0.05 actually look like with 50 decimal points in Ruby:

```
>> sprintf("%0.50f", 0.10)
=> "0.10000000000000000555111512312578270211815834045410"
>> sprintf("%0.50f", 0.05)
=> "0.05000000000000000277555756156289135105907917022705"
```

## Testing it

Okay, so now when the problem is obvious, how can we test it? The best way to
test this is to use a **delta** number. You can think of this delta number as a
number showing the margin of rounding error.

For example, the delta for the 0.10 + 0.05 operation is approximately 0.0000000000000001.

### With Minitest

Minitest provides us the `assert_in_delta`

and `assert_in_epsilon`

methods.

#### assert_in_delta

It fails unless the expected and the actual values are within **delta** of each
other.

```
def test_precision
assert_in_delta(0.15, 0.10 + 0.05, 0.0000000000000001)
end
```

This means that, while we will expect to get 0.15 as a result, the rounding-off error can be as big as 0.0000000000000001.

If you see the source of this method, it’s quite easy to understand:

```
# File minitest/unit.rb, line 122
def assert_in_delta exp, act, delta = 0.001, msg = nil
n = (exp - act).abs
msg = message(msg) { "Expected #{exp} - #{act} (#{n}) to be < #{delta}" }
assert delta >= n, msg
end
```

The delta must be larger or equal than the absolute value of the result of subtraction of the expected and the actual values.

#### assert_in_epsilon

This method behaves in a different matter than `assert_in_delta`

. For
example, if we use the delta as an epsilon, this test will fail:

```
def test_precision
assert_in_epsilon(0.15, 0.10 + 0.05, 0.0000000000000001)
end
```

```
# Running:
F
Finished in 0.001584s, 1262.6263 runs/s, 1262.6263 assertions/s.
1) Failure:
SomeTest#test_with_epsilon [test.rb:5]:
Expected |0.15 - 0.15000000000000002| (2.7755575615628914e-17) to be <= 1.5e-17.
```

Why this happens is easier to see in the source of the `assert_in_epsilon`

method:

```
# File minitest/unit.rb, line 132
def assert_in_epsilon a, b, epsilon = 0.001, msg = nil
assert_in_delta a, b, [a, b].min * epsilon, msg
end
```

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>).to be_within(<delta>).of(<expected>)`

Or, in our case:

```
it "should match within a delta" do
expect(0.10 + 0.05).to be_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.

As an outro, I’ll leave you with this tweet:

**Liked this article?**
Subscribe to my newsletter and get future articles in your inbox. It's a
short and sweet read, going out to +1000 of other engineers.

I care about your privacy, and will never send you spam. You can unsubscribe at any time.