5

First, take a specific float f:

f = [64.4, 73.60, 77.90, 87.40, 95.40].sample # take any one of these special Floats
f.to_d.class == (1.to_d * f).class # => true (BigDecimal)

So multiplying by BigDecimal casts f to BigDecimal. Therefore 1.to_d * f (or f * 1.to_d) can be seen as a (poor, but still) form of converting f to BigDecimal. And yet for these specific values we have:

f.to_d == 1.to_d * f # => false (?!)

Isn't this a bug? I'd assume that while multiplying by 1.to_d Ruby should invoke f.to_d internally. But the results differ, i.e. for f = 64.4:

f.to_d # => #<BigDecimal:7f8202038280,'0.644E2',18(36)>
1.to_d * f # => #<BigDecimal:7f82019c1208,'0.6440000000 000001E2',27(45)>

I cannot see why floating-point representation error should be an excuse here, yet it's obviously a cause, somehow. So why is this happening?

PS. I wrote a snippet of code playing around with this issue:

https://github.com/Swarzkopf314/ruby_wtf/blob/master/multiplication_by_unit.rb

2 Answers2

4

So why is this happening?

TL;DR different precisions are used.

Long answer:

64.4.to_d calls bigdecimal/util's Float#to_d:

def to_d(precision=nil)
  BigDecimal(self, precision || Float::DIG)
end

Unless specified, it uses an implicit precision of Float::DIG which is 15 for current implementations:

Float::DIG
#=> 15

So 64.4.to_d is equivalent to:

BigDecimal(64.4, Float::DIG)
#=> #<BigDecimal:7fd7cc0aa838,'0.644E2',18(36)>

BigDecimal#* on the other hand converts a given float argument via:

if (RB_TYPE_P(r, T_FLOAT)) {
    b = GetVpValueWithPrec(r, DBL_DIG+1, 1);
}

DBL_DIG is the C-equivalent of Float::DIG, so it's basically:

BigDecimal(64.4, Float::DIG + 1)
#=> #<BigDecimal:7fd7cc098408,'0.6440000000 000001E2',27(36)>

That said, you can get the expected result if you provide the precision explicitly, either:

f.to_d(16) == 1.to_d * f
#=> true

or:

f.to_d == 1.to_d.mult(f, 15)
#=> true

and of course by explicitly converting f via to_d:

f.to_d == 1.to_d * f.to_d
#=> true

Isn't this a bug?

It looks like one, you should file a bug report.

Note that neither 0.644E2, nor 0.6440000000000001E2 is an exact representation of the given floating point number. As already noted by Eli Sadoff, 64.4's exact value is 64.400000000000005684341886080801486968994140625, so the most exact BigDecimal representation would be:

BigDecimal('64.400000000000005684341886080801486968994140625')
#=> #<BigDecimal:7fd7cc04a0c8,'0.6440000000 0000005684 3418860808 0148696899 4140625E2',54(63)>

IMO, 64.4.to_d should return just that.

Community
  • 1
  • 1
Stefan
  • 109,145
  • 14
  • 143
  • 218
3

This is not a bug. f == f.to_d returns false, so if f == 1.to_d * f is true, then f.to_d == 1.to_d * f must be false because f != f.to_d. The == method for BigDecimal is intended to compare BigDecimals not BigDecimal to float. Sometimes the equality will work, but for some fs the BigDecimal representation is exact whereas the float is not.

Edit: See Is Floating Point Math Broken for more of an explanation.

Community
  • 1
  • 1
Eli Sadoff
  • 7,173
  • 6
  • 33
  • 61
  • 1
    Yeah, but I'd assume that `f.to_d == 1.to_d * f` should *always* hold and yet it doesn't. It's as if `f` had two different representations as `BigDecimal`. For me it's insane that this equality doesn't always hold. – Maciej Satkiewicz Nov 07 '16 at 19:30
  • `64.4` cannot be exactly represented. It instead is represented as `64.400000000000005684341886080801486968994140625`. As there is no exact representation in float whereas there is an exact big decimal representation, there will be a problem. – Eli Sadoff Nov 07 '16 at 19:33
  • You can actually verify this. Set `f = 64.4` and then test `f == 64.400000000000005684341886080801486968994140625`. It will return true. – Eli Sadoff Nov 07 '16 at 19:34
  • I'm well aware of that. But my point is that casting `Float` to `BigDecimal` should be consistent. If one views `1.to_d * f` as a (poor, but still) form of converting `Float` to `BigDecimal`, then it's surprising that it yields different result than `f.to_d`. And this sure can cause a nasty bug. – Maciej Satkiewicz Nov 07 '16 at 19:46