Project

General

Profile

Actions

Bug #22246

open

Rational#to_f incorrectly rounds when the numerator is a bignum that cannot be represented exactly by a double, and the denominator is a fixnum

Bug #22246: Rational#to_f incorrectly rounds when the numerator is a bignum that cannot be represented exactly by a double, and the denominator is a fixnum

Added by AndrewDile (Andrew Dile) 24 days ago. Updated 10 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:126395]

Description

Rational(65803600513127829623, 10**12).to_f

The bignum numerator cannot be exactly represented by a double, but the code in rb_big_fdiv_double rounds it to a double (dx) before dividing by the fixnum denominator of 10^12, resulting in 65803600.51312783, instead of the correctly rounded value, 65803600.513127826. The isinf(dx) test does not cover this case. Moreover, not all fixnums (in 64-bit systems) can be represented exactly as doubles.

Updated by AndrewDile (Andrew Dile) 20 days ago Actions #1

  • Subject changed from Rational#to.f incorrectly rounds when the numerator is a bignum that cannot be represented exactly by a double, and the denominator is a fixnum to Rational#to_f incorrectly rounds when the numerator is a bignum that cannot be represented exactly by a double, and the denominator is a fixnum

Updated by nobu (Nobuyoshi Nakada) 18 days ago Actions #2 [ruby-core:126453]

$ ruby -e 'x = 65803600513127829623; [65803600.51312783, 65803600.513127826].each {|n| p ((n*10**12).to_i-x).abs}'
3977
4215

Isn't the current result closer?

Updated by AndrewDile (Andrew Dile) 17 days ago Actions #3 [ruby-core:126460]

In your code example, the multiplication of n by 10**12 uses floating point arithmetic, which introduces a rounding error. Instead, it needs to convert n to an exact rational number before doing the multiplication.

x = 65803600513127829623; [65803600.51312783, 65803600.513127826].each {|n| p ((n.to_r*10**12).to_i-x).abs}
3981
3469

Updated by byroot (Jean Boussier) 12 days ago Actions #4

  • Related to Feature #21308: Replacing the Float#to_s (dtoa.c) implementation with a modern algorithm added

Updated by rgburger (Bob Burger) 11 days ago · Edited Actions #6 [ruby-core:126518]

This bug is not related to Feature #21308 except for the same author. The implementation of Rational#to_f does not perform any base conversions.

The bug is caused by the assumption that a floating-point conversion of a bignum numerator followed by a floating-point division by a fixnum denominator produces the correctly rounded result. The relevant code is in rb_big_fdiv_double:

    dx = big2dbl(x);
    if (FIXNUM_P(y)) {
        dy = (double)FIX2LONG(y);
    [...]
    v = rb_flo_div_flo(DBL2NUM(dx), DBL2NUM(dy));

In order for rb_flo_div_flo to produce the correct result, dx and dy must exactly represent x and y.

Updated by byroot (Jean Boussier) 11 days ago Actions #8 [ruby-core:126520]

This bug is not related to Feature #21308 except for the same author.

Yes, I confused it with another ticket, that's why I deleted my comment, sorry.

Updated by byroot (Jean Boussier) 11 days ago Actions #9

  • Related to deleted (Feature #21308: Replacing the Float#to_s (dtoa.c) implementation with a modern algorithm)

Updated by rgburger (Bob Burger) 11 days ago Actions #10 [ruby-core:126521]

Consider the related code in fix_fdiv_double, which demonstrates a check for the denominator y being a fixnum that's exactly represented by a double.

static double
fix_fdiv_double(VALUE x, VALUE y)
{
    if (FIXNUM_P(y)) {
        long iy = FIX2LONG(y);
#if SIZEOF_LONG * CHAR_BIT > DBL_MANT_DIG
        if ((iy < 0 ? -iy : iy) >= (1L << DBL_MANT_DIG)) {
            return rb_big_fdiv_double(rb_int2big(FIX2LONG(x)), rb_int2big(iy));
        }
#endif
        return double_div_double(FIX2LONG(x), iy);
    }

N.B. A simple round-trip check would be simpler and handle all the cases without needing to know anything about mantissa length:
if (iy != (long)(double)iy)

Updated by rgburger (Bob Burger) 11 days ago · Edited Actions #12 [ruby-core:126529]

Here's an example of Rational#to_f giving an incorrect result when the numerator is a bignum and the denominator is a fixnum that can't be exactly represented as a double:
Rational(10**24, 10**17 + 9).to_f should return 10000000.0, but it returns 9999999.999999998.

10**34 = 1000000000000000000000000, but (10**24).to_f.to_i = 999999999999999983222784.

10**17 + 9 = 100000000000000009, but (10**17 + 9).to_f.to_i = 100000000000000016.

Since rb_big_fdiv_double converts both numerator and denominator to doubles, it computes 999999999999999983222784 / 100000000000000016, which gives an incorrect answer.

Updated by rgburger (Bob Burger) 10 days ago Actions #13 [ruby-core:126542]

I found two more cases where rb_big_fdiv_double produces an incorrectly rounded result. I added them to pull request #18599.

assert_equal(3.333333333333333e39, (10**40).fdiv(3), "Bug #22246")
assert_equal(3.333333333333333e39, (10**40).fdiv(3.0), "Bug #22246")
Actions

Also available in: PDF Atom