5

Some BigDecimal values can be compared with a Float by eq in Rspec3, but some values can't be.

describe "compare BigDecimal with Float" do
    it { expect("83.79".to_d).to eq(83.79) } # => fail
    it { expect("83.75".to_d).to eq(83.75) } # => succeed
end

To avoid error, I'm using expressions like eq("83.79".to_d).

Why does the first test fail while the second succeeds?

sawa
  • 165,429
  • 45
  • 277
  • 381
ironsand
  • 14,329
  • 17
  • 83
  • 176
  • This isn't a duplicate of the canonical ["floating point is broken"](http://stackoverflow.com/q/588004/479863) question due to the presence of Ruby's BigDecimal and RSpec. – mu is too short Mar 08 '16 at 23:45

2 Answers2

5

You should never try any sort of strict equality testing with floating point values. You always have to deal with inaccurate internal representation issues with Float, so == and != aren't terribly useful.

Consider this:

'83.79'.to_d - 83.79
# => #<BigDecimal:7ff33fcea560,'-0.1E-13',9(36)> 
'83.75'.to_d - 83.75
# => #<BigDecimal:7ff33fcee688,'0.0',9(27)> 

Note that the difference for 83.79 is not quite zero.

If you need to compare floating point values, you always need to use a delta in your comparison; you always want to say:

Are these values within some small amount from each other?

rather than

Are these values equal?

In Rspec terms:

expect('83.75'.to_d).to be_within(1e-12).of(83.75)
expect('83.79'.to_d).to be_within(1e-12).of(83.79)

and choose the delta (1e-12 in this case) to match your requirements.

sawa
  • 165,429
  • 45
  • 277
  • 381
mu is too short
  • 426,620
  • 70
  • 833
  • 800
3

"83.79".to_d is representing the fraction 8379/100 exactly in internal representation because it uses a base 10 (or a power of it), while "83.79".to_f is not because internal representation uses a base 2, so these are not equal.

That's not the same for 83.75 because is is represented exactly in both base 2 and 10 (this is 83 + 1/2 + 1/4).

If you mix big decimals and floats in the same expression, floats are then converted to the nearest big decimal... Thus, you are in fact performing this: 83.79.to_d or put differently "83.79".to_f.to_d
Since "83.79".to_f is not exact, and since big decimal is more accurate than float, there's no reason that it matches "83.79".to_d.

However, if you force the conversion the other way, I would expect that equality holds:

expect("83.79".to_d.to_f).to eq(83.79)

This is because we can reasonnably expect (least astonishment) that conversions to_f will answer the nearest floating point to the exact fraction, be it from an exact big decimal or a string representation.

aka.nice
  • 9,100
  • 1
  • 28
  • 40
  • "big decimal is more accurate than float" – in fact, there are infinitely many `BigDecimal`s but only 2**64 `Float`s, so there are infinitely many more `BigDecimal`s than there are `Float`s. – Jörg W Mittag Mar 09 '16 at 15:44