From: Tomasz Wegrzanowski Date: 2010-07-25T12:25:45+09:00 Subject: [ruby-core:31476] [Bug #3589] Converting Bignums to Float for equality checks is wrong Issue #3589 has been updated by Tomasz Wegrzanowski. > > Integer 10 means exactly 10, not everything that would end up as 10 if rounded. 10 == 10.2 #=> false > but 10 == 10.0. 100000000000000000000000 should == 100000000000000000000000.0 > You know, if you think it should not, you have to persuade us. > # Assigning this to matz because it turned out to be a design matter. To me 100000000000000000000000.0 means 100000000000000000000000.to_f, or "floating point number closest to 100000000000000000000000". This doesn't have to be exactly 100000000000000000000000. Some arguments follow. == Argument from other languages == This treatment of float equality seem to be unique to Ruby. Compare with Python: print(100000000000000000000000 == 100000000000000000000000.0) #=> False print( 99999999999999991611392 == 100000000000000000000000.0) #=> True print(Fraction(1,3) == 1.0/3.0) #=> False print(Fraction(1,4) == 1.0/4.0) #=> True With Perl it's more complicated, as Perl's standard number type switches between native int, native float, and decimal the way Ruby switches between fixint and bigint - so 1000000000000000000.0 in Perl is decimal, not float and so exact. Still, it doesn't follow "equal if converts". This is the same regardless of $x being decimal or float internally. my $x = 1000000000000000000; my $a = Math::BigInt->new('1000000000000000000'); my $b = Math::BigInt->new('1000000000000000001'); print($x == $a->numify() ? "equal" : "not"); #=> equal print($x == $b->numify() ? "equal" : "not"); #=> equal print($x == $a ? "equal" : "not"); #=> equal print($x == $b ? "equal" : "not"); #=> not A counterexample to this would be C, which is quite explicit that operations involving different numeric types involve implicit conversion. It doesn't have bignums, but comparing float vs int or double vs long long will convert and lose precision before comparison. It also loses precision this way when comparing integers of different signedness etc. - this mess is a good example of what we shouldn't do ;-) == Argument from sort == What would you guess this code to print? # Build array of bignums, add extra floats big = 10**50 values = (1..10).map{|x| big + x} values += (1..10).map{ big.to_f } # Make sure it's sorted values.shuffle! values.sort! # Just cleanup for printing values.reject!{|x| x.is_a?(Float)} values.map!{|x| x - big} p values If you guessed [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], you'd be wrong most of the time. sort relies on transitivity of <=>, so if <=> says big.to_f equals both big+1 and big+7, it naturally assumes big+1 <=> big+7 will also be 0. It's not a bug in sort - it's how <=> works for everything except floats. == Argument from mathematics == Ruby has a few equality-like relations - ==, eql?, equal?. They differ, but they're all (almost) mathematical equivalence relations. For all three, these can be expected: * a == a * a == b iff b == a * if a == b and b == c then a == c I can think of one good exception to a == a - with things like float NaNs and sql nulls - but these really are more techniques for handling errors than real values. I really cannot think of a single case where it would make sense to make equality either non-symmetric, or non-transitive. Similar reasoning applies to <=> - normally it defines partial order between objects: * a >= b and b >= a only if a == b * if a >= b and b >= c then a >= c Transitivity of equality, and transitivity of partial ordering seem to be violated only in one case in Ruby - for comparisons between floats and other numeric types. (and this causes sort to break). Can you think of any other type that does something like that? == Argument from rationals == When you have mixed type operations, and one type is bigger, the most obvious thing to do is simply converting argument of smaller type to bigger type. For example you can define pretty much every Rational vs Integer operation as: class Rational def something(x) x = x.to_r if x.is_a?(Integer) ... And you can do this for Complex operations and non-complex arguments etc. You never do it the other way around - converting to smaller type. This would be obviously wrong: class Integer def something(x) x = x.to_i if x.is_a?(Rational) ... So why, if Rational can represent every floating point value (except nans/infinities/negative zero that is), do rational vs float operations downconvert to float, instead of upconverting to rational? I can think of no other such case anywhere. With bignum vs float, neither is strictly wider than other, but both can be represented as rationals. This doesn't mean I want bignum + float to return rationals, downconversion before returning is perfectly fine. I just want it to be pretty much equivalent to this: class Integer def +(x) if x.is_a?(Float) and x.finite? return (self.to_r + x.to_r).to_f ... == Argument from IEEE 754 == This .to_r/.to_f above might be puzzling, but look at this. All basic floating point operations are defined by IEEE 754 standard as mathematically equivalent to this: def +(x) return (self.to_real + x.to_real).to_f(rounding_mode) end Where .to_real means conversion to actual mathematical real number with potentially infinite precision, all extra bits being 0s. Of course it's not implemented like that - but the result is guaranteed to be exactly the same as if it was. == Other inaccuracies == I'm not really terribly bothered by that, only by equality and <=>, but a lot of operations involving floats and other types downconvert to float too early and lose precision. puts(Rational(15,11)*11.0 == 15.0) #=> false puts((Rational(15,11) * 11.0.to_r).to_f == 15.0) # => true puts(100000000000000000000000 - 100000000000000000000000.0) #=> 0 puts((100000000000000000000000 - 100000000000000000000000.0.to_i).to_f) #=> 8388608.0 Such precision loss never happens for anything that involves only floats, or only non-floats. And it really wouldn't be that difficult to avoid it if we cared, but if even I don't, I doubt others will. ---------------------------------------- http://redmine.ruby-lang.org/issues/show/3589 ---------------------------------------- http://redmine.ruby-lang.org