ruby-changes:44517
From: nobu <ko1@a...>
Date: Sat, 5 Nov 2016 18:49:45 +0900 (JST)
Subject: [ruby-changes:44517] nobu:r56590 (trunk): numeric.c: round to nearest even
nobu 2016-11-05 18:49:39 +0900 (Sat, 05 Nov 2016) New Revision: 56590 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=56590 Log: numeric.c: round to nearest even * numeric.c (flo_round, int_round): support round-to-nearest-even semantics of IEEE 754 to match sprintf behavior, and add `half:` optional keyword argument for the old behavior. [ruby-core:76273] [Bug #12548] Modified files: trunk/ChangeLog trunk/NEWS trunk/internal.h trunk/lib/rexml/functions.rb trunk/numeric.c trunk/rational.c trunk/test/ruby/test_float.rb trunk/test/ruby/test_integer.rb trunk/test/ruby/test_rational.rb trunk/test/test_mathn.rb Index: NEWS =================================================================== --- NEWS (revision 56589) +++ NEWS (revision 56590) @@ -69,6 +69,9 @@ with all sufficient information, see the https://github.com/ruby/ruby/blob/trunk/NEWS#L69 * Float#ceil, Float#floor, and Float#truncate now take an optional digits, as well as Float#round. [Feature #12245] + * Float#round now takes an optional keyword argument, half option, and + the default behavior is round-to-nearest-even now. [Bug #12548] + * Hash * Hash#transform_values and Hash#transform_values! [Feature #12512] @@ -83,6 +86,9 @@ with all sufficient information, see the https://github.com/ruby/ruby/blob/trunk/NEWS#L86 * Integer#digits for extracting columns of place-value notation [Feature #12447] + * Int#round now takes an optional keyword argument, half option, and the + default behavior is round-to-nearest-even now. [Bug #12548] + * IO * IO#gets, IO#readline, IO#each_line, IO#readlines, IO#foreach now takes @@ -112,6 +118,11 @@ with all sufficient information, see the https://github.com/ruby/ruby/blob/trunk/NEWS#L118 * Support CLOCK_MONOTONIC_RAW_APPROX, CLOCK_UPTIME_RAW, and CLOCK_UPTIME_RAW_APPROX which are introduced by macOS 10.12. +* Rational + + * Rational#round now takes an optional keyword argument, half option, and + the default behavior is round-to-nearest-even now. [Bug #12548] + * Regexp * Regexp#match? [Feature #8110] Index: test/ruby/test_rational.rb =================================================================== --- test/ruby/test_rational.rb (revision 56589) +++ test/ruby/test_rational.rb (revision 56590) @@ -597,17 +597,20 @@ class Rational_Test < Test::Unit::TestCa https://github.com/ruby/ruby/blob/trunk/test/ruby/test_rational.rb#L597 end def test_trunc - [[Rational(13, 5), [ 2, 3, 2, 3]], # 2.6 - [Rational(5, 2), [ 2, 3, 2, 3]], # 2.5 - [Rational(12, 5), [ 2, 3, 2, 2]], # 2.4 - [Rational(-12,5), [-3, -2, -2, -2]], # -2.4 - [Rational(-5, 2), [-3, -2, -2, -3]], # -2.5 - [Rational(-13, 5), [-3, -2, -2, -3]], # -2.6 + [[Rational(13, 5), [ 2, 3, 2, 3, 3, 3]], # 2.6 + [Rational(5, 2), [ 2, 3, 2, 2, 2, 3]], # 2.5 + [Rational(12, 5), [ 2, 3, 2, 2, 2, 2]], # 2.4 + [Rational(-12,5), [-3, -2, -2, -2, -2, -2]], # -2.4 + [Rational(-5, 2), [-3, -2, -2, -2, -2, -3]], # -2.5 + [Rational(-13, 5), [-3, -2, -2, -3, -3, -3]], # -2.6 ].each do |i, a| - assert_equal(a[0], i.floor) - assert_equal(a[1], i.ceil) - assert_equal(a[2], i.truncate) - assert_equal(a[3], i.round) + s = proc {i.inspect} + assert_equal(a[0], i.floor, s) + assert_equal(a[1], i.ceil, s) + assert_equal(a[2], i.truncate, s) + assert_equal(a[3], i.round, s) + assert_equal(a[4], i.round(half: :even), s) + assert_equal(a[5], i.round(half: :up), s) end end Index: test/ruby/test_float.rb =================================================================== --- test/ruby/test_float.rb (revision 56589) +++ test/ruby/test_float.rb (revision 56590) @@ -659,6 +659,48 @@ class TestFloat < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_float.rb#L659 } end + def test_round_half_even + assert_equal(12.0, 12.5.round(half: :even)) + assert_equal(14.0, 13.5.round(half: :even)) + + assert_equal(2.2, 2.15.round(1, half: :even)) + assert_equal(2.2, 2.25.round(1, half: :even)) + assert_equal(2.4, 2.35.round(1, half: :even)) + + assert_equal(-2.2, -2.15.round(1, half: :even)) + assert_equal(-2.2, -2.25.round(1, half: :even)) + assert_equal(-2.4, -2.35.round(1, half: :even)) + + assert_equal(7.1364, 7.13645.round(4, half: :even)) + assert_equal(7.1365, 7.1364501.round(4, half: :even)) + assert_equal(7.1364, 7.1364499.round(4, half: :even)) + + assert_equal(-7.1364, -7.13645.round(4, half: :even)) + assert_equal(-7.1365, -7.1364501.round(4, half: :even)) + assert_equal(-7.1364, -7.1364499.round(4, half: :even)) + end + + def test_round_half_up + assert_equal(13.0, 12.5.round(half: :up)) + assert_equal(14.0, 13.5.round(half: :up)) + + assert_equal(2.2, 2.15.round(1, half: :up)) + assert_equal(2.3, 2.25.round(1, half: :up)) + assert_equal(2.4, 2.35.round(1, half: :up)) + + assert_equal(-2.2, -2.15.round(1, half: :up)) + assert_equal(-2.3, -2.25.round(1, half: :up)) + assert_equal(-2.4, -2.35.round(1, half: :up)) + + assert_equal(7.1365, 7.13645.round(4, half: :up)) + assert_equal(7.1365, 7.1364501.round(4, half: :up)) + assert_equal(7.1364, 7.1364499.round(4, half: :up)) + + assert_equal(-7.1365, -7.13645.round(4, half: :up)) + assert_equal(-7.1365, -7.1364501.round(4, half: :up)) + assert_equal(-7.1364, -7.1364499.round(4, half: :up)) + end + def test_Float assert_in_delta(0.125, Float("0.1_2_5"), 0.00001) assert_in_delta(0.125, "0.1_2_5__".to_f, 0.00001) Index: test/ruby/test_integer.rb =================================================================== --- test/ruby/test_integer.rb (revision 56589) +++ test/ruby/test_integer.rb (revision 56590) @@ -187,13 +187,49 @@ class TestInteger < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_integer.rb#L187 assert_int_equal(11110, 11111.round(-1)) assert_int_equal(11100, 11111.round(-2)) assert_int_equal(+200, +249.round(-2)) - assert_int_equal(+300, +250.round(-2)) + assert_int_equal(+200, +250.round(-2)) assert_int_equal(-200, -249.round(-2)) - assert_int_equal(-300, -250.round(-2)) - assert_int_equal(+30 * 10**70, (+25 * 10**70).round(-71)) - assert_int_equal(-30 * 10**70, (-25 * 10**70).round(-71)) + assert_int_equal(+200, +249.round(-2, half: :even)) + assert_int_equal(+200, +250.round(-2, half: :even)) + assert_int_equal(+300, +349.round(-2, half: :even)) + assert_int_equal(+400, +350.round(-2, half: :even)) + assert_int_equal(+200, +249.round(-2, half: :up)) + assert_int_equal(+300, +250.round(-2, half: :up)) + assert_int_equal(+300, +349.round(-2, half: :up)) + assert_int_equal(+400, +350.round(-2, half: :up)) + assert_int_equal(-200, -250.round(-2)) + assert_int_equal(-200, -249.round(-2, half: :even)) + assert_int_equal(-200, -250.round(-2, half: :even)) + assert_int_equal(-300, -349.round(-2, half: :even)) + assert_int_equal(-400, -350.round(-2, half: :even)) + assert_int_equal(-200, -249.round(-2, half: :up)) + assert_int_equal(-300, -250.round(-2, half: :up)) + assert_int_equal(-300, -349.round(-2, half: :up)) + assert_int_equal(-400, -350.round(-2, half: :up)) + assert_int_equal(+20 * 10**70, (+25 * 10**70).round(-71)) + assert_int_equal(-20 * 10**70, (-25 * 10**70).round(-71)) assert_int_equal(+20 * 10**70, (+25 * 10**70 - 1).round(-71)) assert_int_equal(-20 * 10**70, (-25 * 10**70 + 1).round(-71)) + assert_int_equal(+40 * 10**70, (+35 * 10**70).round(-71)) + assert_int_equal(-40 * 10**70, (-35 * 10**70).round(-71)) + assert_int_equal(+30 * 10**70, (+35 * 10**70 - 1).round(-71)) + assert_int_equal(-30 * 10**70, (-35 * 10**70 + 1).round(-71)) + assert_int_equal(+20 * 10**70, (+25 * 10**70).round(-71, half: :even)) + assert_int_equal(-20 * 10**70, (-25 * 10**70).round(-71, half: :even)) + assert_int_equal(+20 * 10**70, (+25 * 10**70 - 1).round(-71, half: :even)) + assert_int_equal(-20 * 10**70, (-25 * 10**70 + 1).round(-71, half: :even)) + assert_int_equal(+40 * 10**70, (+35 * 10**70).round(-71, half: :even)) + assert_int_equal(-40 * 10**70, (-35 * 10**70).round(-71, half: :even)) + assert_int_equal(+30 * 10**70, (+35 * 10**70 - 1).round(-71, half: :even)) + assert_int_equal(-30 * 10**70, (-35 * 10**70 + 1).round(-71, half: :even)) + assert_int_equal(+30 * 10**70, (+25 * 10**70).round(-71, half: :up)) + assert_int_equal(-30 * 10**70, (-25 * 10**70).round(-71, half: :up)) + assert_int_equal(+20 * 10**70, (+25 * 10**70 - 1).round(-71, half: :up)) + assert_int_equal(-20 * 10**70, (-25 * 10**70 + 1).round(-71, half: :up)) + assert_int_equal(+40 * 10**70, (+35 * 10**70).round(-71, half: :up)) + assert_int_equal(-40 * 10**70, (-35 * 10**70).round(-71, half: :up)) + assert_int_equal(+30 * 10**70, (+35 * 10**70 - 1).round(-71, half: :up)) + assert_int_equal(-30 * 10**70, (-35 * 10**70 + 1).round(-71, half: :up)) assert_int_equal(1111_1111_1111_1111_1111_1111_1111_1110, 1111_1111_1111_1111_1111_1111_1111_1111.round(-1)) assert_int_equal(-1111_1111_1111_1111_1111_1111_1111_1110, (-1111_1111_1111_1111_1111_1111_1111_1111).round(-1)) Index: test/test_mathn.rb =================================================================== --- test/test_mathn.rb (revision 56589) +++ test/test_mathn.rb (revision 56590) @@ -96,17 +96,17 @@ class TestMathn < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/test_mathn.rb#L96 def test_round assert_separately(%w[-rmathn], <<-EOS, ignore_stderr: true) assert_equal( 3, ( 13/5).round) - assert_equal( 3, ( 5/2).round) + assert_equal( 2, ( 5/2).round) assert_equal( 2, ( 12/5).round) assert_equal(-2, (-12/5).round) - assert_equal(-3, ( -5/2).round) + assert_equal(-2, ( -5/2).round) assert_equal(-3, (-13/5).round) assert_equal( 3, ( 13/5).round(0)) - assert_equal( 3, ( 5/2).round(0)) + assert_equal( 2, ( 5/2).round(0)) assert_equal( 2, ( 12/5).round(0)) assert_equal(-2, (-12/5).round(0)) - assert_equal(-3, ( -5/2).round(0)) + assert_equal(-2, ( -5/2).round(0)) assert_equal(-3, (-13/5).round(0)) assert_equal(( 13/5), ( 13/5).round(2)) @@ -115,6 +115,48 @@ class TestMathn < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/test_mathn.rb#L115 assert_equal((-12/5), (-12/5).round(2)) assert_equal(( -5/2), ( -5/2).round(2)) assert_equal((-13/5), (-13/5).round(2)) + + assert_equal( 3, ( 13/5).round(half: :even)) + assert_equal( 2, ( 5/2).round(half: :even)) + assert_equal( 2, ( 12/5).round(half: :even)) + assert_equal(-2, (-12/5).round(half: :even)) + assert_equal(-2, ( -5/2).round(half: :even)) + assert_equal(-3, (-13/5).round(half: :even)) + + assert_equal( 3, ( 13/5).round(0, half: :even)) + assert_equal( 2, ( 5/2).round(0, half: :even)) + assert_equal( 2, ( 12/5).round(0, half: :even)) + assert_equal(-2, (-12/5).round(0, half: :even)) + assert_equal(-2, ( -5/2).round(0, half: :even)) + assert_equal(-3, (-13/5).round(0, half: :even)) + + assert_equal(( 13/5), ( 13/5).round(2, half: :even)) + assert_equal(( 5/2), ( 5/2).round(2, half: :even)) + assert_equal(( 12/5), ( 12/5).round(2, half: :even)) + assert_equal((-12/5), (-12/5).round(2, half: :even)) + assert_equal(( -5/2), ( -5/2).round(2, half: :even)) + assert_equal((-13/5), (-13/5).round(2, half: :even)) + + assert_equal( 3, ( 13/5).round(half: :up)) + assert_equal( 3, ( 5/2).round(half: :up)) + assert_equal( 2, ( 12/5).round(half: :up)) + assert_equal(-2, (-12/5).round(half: :up)) + assert_equal(-3, ( -5/2).round(half: :up)) + assert_equal(-3, (-13/5).round(half: :up)) + + assert_equal( 3, ( 13/5).round(0, half: :up)) + assert_equal( 3, ( 5/2).round(0, half: :up)) + assert_equal( 2, ( 12/5).round(0, half: :up)) + assert_equal(-2, (-12/5).round(0, half: :up)) + assert_equal(-3, ( -5/2).round(0, half: :up)) + assert_equal(-3, (-13/5).round(0, half: :up)) + + assert_equal(( 13/5), ( 13/5).round(2, half: :up)) + assert_equal(( 5/2), ( 5/2).round(2, half: :up)) + assert_equal(( 12/5), ( 12/5).round(2, half: :up)) + assert_equal((-12/5), (-12/5).round(2, half: :up)) + assert_equal(( -5/2), ( -5/2).round(2, half: :up)) + assert_equal((-13/5), (-13/5).round(2, half: :up)) EOS end end Index: numeric.c =================================================================== --- numeric.c (revision 56589) +++ numeric.c (revision 56590) @@ -93,7 +93,7 @@ round(double x) https://github.com/ruby/ruby/blob/trunk/numeric.c#L93 #endif static double -round_to_nearest(double x, double s) +round_half_up(double x, double s) { double f, xs = x * s; @@ -117,12 +117,44 @@ round_to_nearest(double x, double s) https://github.com/ruby/ruby/blob/trunk/numeric.c#L117 return x; } +static double +round_half_even(double x, double s) +{ + double f, d, xs = x * s; + + if (x > 0.0) { + f = floor(xs); + d = xs - f; + if (d > 0.5) + d = 1.0; + else if (d == 0.5 || ((double)((f + 0.5) / s) <= x)) + d = fmod(f, 2.0); + else + d = 0.0; + x = f + d; + } + else if (x < 0.0) { + f = ceil(xs); + d = f - xs; + if (d > 0.5) + d = 1.0; + else if (d == 0.5 || ((double)((f - 0.5) / s) >= x)) + d = fmod(-f, 2.0); + else + d = 0.0; + x = f - d; + } + return x; +} + static VALUE fix_uminus(VALUE num); static VALUE fix_mul(VALUE x, VALUE y); static VALUE fix_lshift(long, unsigned long); static VALUE fix_rshift(long, unsigned long); static VALUE int_pow(long x, unsigned long y); static VALUE int_cmp(VALUE x, VALUE y); +static VALUE int_odd_p(VALUE x); +static VALUE int_even_p(VALUE x); static int int_round_zero_p(VALUE num, int ndigits); VALUE rb_int_floor(VALUE num, int ndigits); VALUE rb_int_ceil(VALUE num, int ndigits); @@ -152,6 +184,38 @@ rb_num_zerodiv(void) https://github.com/ruby/ruby/blob/trunk/numeric.c#L184 rb_raise(rb_eZeroDivError, "divided by 0"); } +enum ruby_num_rounding_mode +rb_num_get_rounding_option(VALUE opts) +{ + static ID round_kwds[1]; + VALUE rounding; + const char *s; + long l; + + if (!NIL_P(opts)) { + if (!round_kwds[0]) { + round_kwds[0] = rb_intern_const("half"); + } + if (!rb_get_kwargs(opts, round_kwds, 0, 1, &rounding)) goto noopt; + if (SYMBOL_P(rounding)) rounding = rb_sym2str(rounding); + s = StringValueCStr(rounding); + l = RSTRING_LEN(rounding); + switch (l) { + case 2: + if (strncasecmp(s, "up", 2) == 0) + return RUBY_NUM_ROUND_HALF_UP; + break; + case 4: + if (strncasecmp(s, "even", 4) == 0) + return RUBY_NUM_ROUND_HALF_EVEN; + break; + } + rb_raise(rb_eArgError, "unknown rounding mode: %"PRIsVALUE, rounding); + } + noopt: + return RUBY_NUM_ROUND_DEFAULT; +} + /* experimental API */ int rb_num_to_uint(VALUE val, unsigned int *ret) @@ -201,7 +265,6 @@ compare_with_zero(VALUE num, ID mid) https://github.com/ruby/ruby/blob/trunk/numeric.c#L265 #define FIXNUM_NEGATIVE_P(num) ((SIGNED_VALUE)(num) < 0) #define FIXNUM_ZERO_P(num) ((num) == INT2FIX(0)) -#if 0 static inline int int_pos_p(VALUE num) { @@ -213,7 +276,6 @@ int_pos_p(VALUE num) https://github.com/ruby/ruby/blob/trunk/numeric.c#L276 } return Qnil; } -#endif static inline int int_neg_p(VALUE num) @@ -1962,11 +2024,27 @@ int_round_zero_p(VALUE num, int ndigits) https://github.com/ruby/ruby/blob/trunk/numeric.c#L2024 return (-0.415241 * ndigits - 0.125 > bytes); } +static SIGNED_VALUE +int_round_half_even(SIGNED_VALUE x, SIGNED_VALUE y) +{ + SIGNED_VALUE z = +(x + y / 2) / y; + if ((z * y - x) * 2 == y) { + z &= ~1; + } + return z * y; +} + +static SIGNED_VALUE +int_round_half_up(SIGNED_VALUE x, SIGNED_VALUE y) +{ + return (x + y / 2) / y * y; +} + /* * Assumes num is an Integer, ndigits <= 0 */ VALUE -rb_int_round(VALUE num, int ndigits) +rb_int_round(VALUE num, int ndigits, enum ruby_num_rounding_mode mode) { VALUE n, f, h, r; @@ -1979,7 +2057,9 @@ rb_int_round(VALUE num, int ndigits) https://github.com/ruby/ruby/blob/trunk/numeric.c#L2057 SIGNED_VALUE x = FIX2LONG(num), y = FIX2LONG(f); int neg = x < 0; if (neg) x = -x; - x = (x + y / 2) / y * y; + x = ROUND_TO(mode, + int_round_half_up(x, y), + int_round_half_even(x, y)); if (neg) x = -x; return LONG2NUM(x); } @@ -1991,7 +2071,11 @@ rb_int_round(VALUE num, int ndigits) https://github.com/ruby/ruby/blob/trunk/numeric.c#L2071 r = rb_int_modulo(num, f); n = rb_int_minus(num, r); r = int_cmp(r, h); - if (FIXNUM_POSITIVE_P(r) || (FIXNUM_ZERO_P(r) && !int_neg_p(num))) { + if (FIXNUM_POSITIVE_P(r) || + (FIXNUM_ZERO_P(r) && + ROUND_TO(mode, + int_pos_p(num), + int_odd_p(rb_int_idiv(n, f))))) { n = rb_int_plus(n, f); } return n; @@ -2109,21 +2193,27 @@ static VALUE https://github.com/ruby/ruby/blob/trunk/numeric.c#L2193 flo_round(int argc, VALUE *argv, VALUE num) { double number, f, x; + VALUE nd, opt; int ndigits = 0; + enum ruby_num_rounding_mode mode; - if (rb_check_arity(argc, 0, 1)) { - ndigits = NUM2INT(argv[0]); + if (rb_scan_args(argc, argv, "01:", &nd, &opt)) { + ndigits = NUM2INT(nd); } + mode = rb_num_get_rounding_option(opt); if (ndigits < 0) { - return rb_int_round(flo_to_i(num), ndigits); + return rb_int_round(flo_to_i(num), ndigits, mode); } number = RFLOAT_VALUE(num); if (ndigits == 0) { - return dbl2ival(round(number)); + x = ROUND_TO(mode, + round(number), round_half_even(number, 1.0)); + return dbl2ival(x); } if (float_invariant_round(number, ndigits, &num)) return num; f = pow(10, ndigits); - x = round_to_nearest(number, f); + x = ROUND_TO(mode, + round_half_up(number, f), round_half_even(number, f)); return DBL2NUM(x / f); } @@ -4862,16 +4952,19 @@ static VALUE https://github.com/ruby/ruby/blob/trunk/numeric.c#L4952 int_round(int argc, VALUE* argv, VALUE num) { int ndigits; + int mode; + VALUE nd, opt; - if (!rb_check_arity(argc, 0, 1)) return num; - ndigits = NUM2INT(argv[0]); + if (!rb_scan_args(argc, argv, "01:", &nd, &opt)) return num; + ndigits = NUM2INT(nd); + mode = rb_num_get_rounding_option(opt); if (ndigits > 0) { return rb_Float(num); } if (ndigits == 0) { return num; } - return rb_int_round(num, ndigits); + return rb_int_round(num, ndigits, mode); } /* Index: lib/rexml/functions.rb =================================================================== --- lib/rexml/functions.rb (revision 56589) +++ lib/rexml/functions.rb (revision 56590) @@ -205,8 +205,8 @@ module REXML https://github.com/ruby/ruby/blob/trunk/lib/rexml/functions.rb#L205 # Now, get the bounds. The XPath bounds are 1..length; the ruby bounds # are 0..length. Therefore, we have to offset the bounds by one. - ruby_start = ruby_start.round - 1 - ruby_length = ruby_length.round + ruby_start = round(ruby_start) - 1 + ruby_length = round(ruby_length) if ruby_start < 0 ruby_length += ruby_start unless infinite_length @@ -376,10 +376,13 @@ module REXML https://github.com/ruby/ruby/blob/trunk/lib/rexml/functions.rb#L376 end def Functions::round( number ) + number = number(number) begin - number(number).round + neg = number.negative? + number = number.abs.round(half: :up) + neg ? -number : number rescue FloatDomainError - number(number) + number end end Index: internal.h =================================================================== --- internal.h (revision 56589) +++ internal.h (revision 56590) @@ -1135,6 +1135,17 @@ VALUE rb_math_sqrt(VALUE); https://github.com/ruby/ruby/blob/trunk/internal.h#L1135 void Init_newline(void); /* numeric.c */ +#ifndef ROUND_DEFAULT +# define ROUND_DEFAULT RUBY_NUM_ROUND_HALF_EVEN +#endif +enum ruby_num_rounding_mode { + RUBY_NUM_ROUND_HALF_UP, + RUBY_NUM_ROUND_HALF_EVEN, + RUBY_NUM_ROUND_DEFAULT = ROUND_DEFAULT +}; +#define ROUND_TO(mode, up, even) \ + ((mode) == RUBY_NUM_ROUND_HALF_EVEN ? even : up) + int rb_num_to_uint(VALUE val, unsigned int *ret); VALUE ruby_num_interval_step_size(VALUE from, VALUE to, VALUE st (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/