diff --git a/NEWS.md b/NEWS.md index b30d707b4d..793da5b579 100644 --- a/NEWS.md +++ b/NEWS.md @@ -92,6 +92,10 @@ Outstanding ones only. * File.dirname now accepts an optional argument for the level to strip path components. [[Feature #12194]] +* Integer + + * Integer.try_convert is added. [[Feature #15211]] + * Module * Module#prepend now modifies the ancestor chain if the receiver @@ -191,6 +195,7 @@ Excluding feature bug fixes. [Feature #12194]: https://bugs.ruby-lang.org/issues/12194 [Feature #14256]: https://bugs.ruby-lang.org/issues/14256 [Feature #15198]: https://bugs.ruby-lang.org/issues/15198 +[Feature #15211]: https://bugs.ruby-lang.org/issues/15211 [Feature #16043]: https://bugs.ruby-lang.org/issues/16043 [Feature #16806]: https://bugs.ruby-lang.org/issues/16806 [Feature #17312]: https://bugs.ruby-lang.org/issues/17312 diff --git a/internal/numeric.h b/internal/numeric.h index 32d5bd27fa..82ba4c1cfb 100644 --- a/internal/numeric.h +++ b/internal/numeric.h @@ -77,6 +77,7 @@ VALUE rb_int_lshift(VALUE x, VALUE y); VALUE rb_int_div(VALUE x, VALUE y); int rb_int_positive_p(VALUE num); int rb_int_negative_p(VALUE num); +VALUE rb_check_integer_type(VALUE); VALUE rb_num_pow(VALUE x, VALUE y); VALUE rb_float_ceil(VALUE num, int ndigits); VALUE rb_float_floor(VALUE x, int ndigits); diff --git a/numeric.c b/numeric.c index c9dc24bcf1..345067c6a5 100644 --- a/numeric.c +++ b/numeric.c @@ -5247,6 +5247,12 @@ rb_int_s_isqrt(VALUE self, VALUE num) } } +static VALUE +int_s_try_convert(VALUE self, VALUE num) +{ + return rb_check_integer_type(num); +} + /* * Document-class: ZeroDivisionError * @@ -5473,6 +5479,7 @@ Init_Numeric(void) rb_undef_alloc_func(rb_cInteger); rb_undef_method(CLASS_OF(rb_cInteger), "new"); rb_define_singleton_method(rb_cInteger, "sqrt", rb_int_s_isqrt, 1); + rb_define_singleton_method(rb_cInteger, "try_convert", int_s_try_convert, 1); rb_define_method(rb_cInteger, "to_s", int_to_s, -1); rb_define_alias(rb_cInteger, "inspect", "to_s"); diff --git a/object.c b/object.c index 695a11e366..43c97272dd 100644 --- a/object.c +++ b/object.c @@ -3232,19 +3232,23 @@ rb_check_convert_type_with_id(VALUE val, int type, const char *tname, ID method) #define try_to_int(val, mid, raise) \ convert_type_with_id(val, "Integer", mid, raise, -1) -ALWAYS_INLINE(static VALUE rb_to_integer(VALUE val, const char *method, ID mid)); +ALWAYS_INLINE(static VALUE rb_to_integer_with_id_exception(VALUE val, const char *method, ID mid, int raise)); +/* Integer specific rb_check_convert_type_with_id */ static inline VALUE -rb_to_integer(VALUE val, const char *method, ID mid) +rb_to_integer_with_id_exception(VALUE val, const char *method, ID mid, int raise) { VALUE v; if (RB_INTEGER_TYPE_P(val)) return val; - v = try_to_int(val, mid, TRUE); + v = try_to_int(val, mid, raise); + if (!raise && NIL_P(v)) return Qnil; if (!RB_INTEGER_TYPE_P(v)) { conversion_mismatch(val, "Integer", method, v); } return v; } +#define rb_to_integer(val, method, mid) \ + rb_to_integer_with_id_exception(val, method, mid, TRUE) /** * Tries to convert \a val into \c Integer. @@ -3371,6 +3375,12 @@ rb_Integer(VALUE val) return rb_convert_to_integer(val, 0, TRUE); } +VALUE +rb_check_integer_type(VALUE val) +{ + return rb_to_integer_with_id_exception(val, "to_int", idTo_int, FALSE); +} + int rb_bool_expected(VALUE obj, const char *flagname) { diff --git a/spec/ruby/core/integer/try_convert_spec.rb b/spec/ruby/core/integer/try_convert_spec.rb new file mode 100644 index 0000000000..45c66eec79 --- /dev/null +++ b/spec/ruby/core/integer/try_convert_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +ruby_version_is "3.1" do + describe "Integer.try_convert" do + it "returns the argument if it's an Integer" do + x = 42 + Integer.try_convert(x).should equal(x) + end + + it "returns nil when the argument does not respond to #to_int" do + Integer.try_convert(Object.new).should be_nil + end + + it "sends #to_int to the argument and returns the result if it's nil" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return(nil) + Integer.try_convert(obj).should be_nil + end + + it "sends #to_int to the argument and returns the result if it's an Integer" do + x = 234 + obj = mock("to_int") + obj.should_receive(:to_int).and_return(x) + Integer.try_convert(obj).should equal(x) + end + + it "sends #to_int to the argument and raises TypeError if it's not a kind of Integer" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return(Object.new) + -> { Integer.try_convert obj }.should raise_error(TypeError) + end + + it "does not rescue exceptions raised by #to_int" do + obj = mock("to_int") + obj.should_receive(:to_int).and_raise(RuntimeError) + -> { Integer.try_convert obj }.should raise_error(RuntimeError) + end + end +end diff --git a/test/ruby/test_integer.rb b/test/ruby/test_integer.rb index 2755987276..1cd256a1cf 100644 --- a/test/ruby/test_integer.rb +++ b/test/ruby/test_integer.rb @@ -660,4 +660,21 @@ class TestInteger < Test::Unit::TestCase def o.fdiv(x); 1; end assert_equal(1.0, 1.fdiv(o)) end + + def test_try_convert + assert_equal(1, Integer.try_convert(1)) + assert_equal(1, Integer.try_convert(1.0)) + assert_nil Integer.try_convert("1") + o = Object.new + assert_nil Integer.try_convert(o) + def o.to_i; 1; end + assert_nil Integer.try_convert(o) + o = Object.new + def o.to_int; 1; end + assert_equal(1, Integer.try_convert(o)) + + o = Object.new + def o.to_int; Object.new; end + assert_raise_with_message(TypeError, /can't convert Object to Integer/) {Integer.try_convert(o)} + end end