summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2021-07-26 11:20:27 -0700
committerJeremy Evans <code@jeremyevans.net>2021-08-06 15:03:51 -0700
commitd16b68cb2204eeb5af8bd39149202b630374c67f (patch)
treed14b41ad62a2ef2a5b2fc40f3917ffebf83d2d80
parent0d3520b063b304708699d3b7ea82b0a5b0279555 (diff)
downloadruby-d16b68cb2204eeb5af8bd39149202b630374c67f.tar.gz
Use Rational for Float#round with ndigits > 14
ndigits higher than 14 can result in values that are slightly too large due to floating point limitations. Converting to rational for the calculation and then back to float fixes these issues. Fixes [Bug #14635] Fixes [Bug #17183] Co-authored by: Yusuke Endoh <mame@ruby-lang.org>
-rw-r--r--internal/rational.h1
-rw-r--r--numeric.c4
-rw-r--r--rational.c6
-rw-r--r--test/ruby/test_numeric.rb8
4 files changed, 19 insertions, 0 deletions
diff --git a/internal/rational.h b/internal/rational.h
index 6bbd2a9810..a9e96742c9 100644
--- a/internal/rational.h
+++ b/internal/rational.h
@@ -39,6 +39,7 @@ VALUE rb_rational_cmp(VALUE self, VALUE other);
VALUE rb_rational_pow(VALUE self, VALUE other);
VALUE rb_rational_floor(VALUE self, int ndigits);
VALUE rb_numeric_quo(VALUE x, VALUE y);
+VALUE rb_flo_round_by_rational(int argc, VALUE *argv, VALUE num);
VALUE rb_float_numerator(VALUE x);
VALUE rb_float_denominator(VALUE x);
diff --git a/numeric.c b/numeric.c
index 5f7c16218a..a1801f9654 100644
--- a/numeric.c
+++ b/numeric.c
@@ -2230,6 +2230,10 @@ flo_round(int argc, VALUE *argv, VALUE num)
frexp(number, &binexp);
if (float_round_overflow(ndigits, binexp)) return num;
if (float_round_underflow(ndigits, binexp)) return DBL2NUM(0);
+ if (ndigits > 14) {
+ /* In this case, pow(10, ndigits) may not be accurate. */
+ return rb_flo_round_by_rational(argc, argv, num);
+ }
f = pow(10, ndigits);
x = ROUND_CALL(mode, round, (number, f));
return DBL2NUM(x / f);
diff --git a/rational.c b/rational.c
index 76a4264e0a..7324f78621 100644
--- a/rational.c
+++ b/rational.c
@@ -1540,6 +1540,12 @@ nurat_round_n(int argc, VALUE *argv, VALUE self)
return f_round_common(argc, argv, self, round_func);
}
+VALUE
+rb_flo_round_by_rational(int argc, VALUE *argv, VALUE num)
+{
+ return nurat_to_f(nurat_round_n(argc, argv, float_to_r(num)));
+}
+
static double
nurat_to_double(VALUE self)
{
diff --git a/test/ruby/test_numeric.rb b/test/ruby/test_numeric.rb
index b5486d387c..0593cb535d 100644
--- a/test/ruby/test_numeric.rb
+++ b/test/ruby/test_numeric.rb
@@ -200,6 +200,14 @@ class TestNumeric < Test::Unit::TestCase
assert_nil(a <=> :foo)
end
+ def test_float_round_ndigits
+ bug14635 = "[ruby-core:86323]"
+ f = 0.5
+ 31.times do |i|
+ assert_equal(0.5, f.round(i+1), bug14635 + " (argument: #{i+1})")
+ end
+ end
+
def test_floor_ceil_round_truncate
a = Class.new(Numeric) do
def to_f; 1.5; end