ruby-changes:55215
From: mame <ko1@a...>
Date: Wed, 3 Apr 2019 17:11:48 +0900 (JST)
Subject: [ruby-changes:55215] mame:r67422 (trunk): Introduce beginless range [Feature#14799]
mame 2019-04-03 17:11:41 +0900 (Wed, 03 Apr 2019) New Revision: 67422 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=67422 Log: Introduce beginless range [Feature#14799] Modified files: trunk/NEWS trunk/defs/id.def trunk/doc/syntax/literals.rdoc trunk/parse.y trunk/range.c trunk/spec/ruby/core/range/new_spec.rb trunk/test/ruby/test_array.rb trunk/test/ruby/test_range.rb Index: NEWS =================================================================== --- NEWS (revision 67421) +++ NEWS (revision 67422) @@ -28,6 +28,12 @@ sufficient information, see the ChangeLo https://github.com/ruby/ruby/blob/trunk/NEWS#L28 * Numbered parameter as the default block parameter is introduced as an experimental feature. [Feature #4475] +* A beginless range is experimentally introduced. It might not be as useful + as an endless range, but would be good for DSL purpose. + + ary[..3] # identical to ary[0..3] + where(sales: ..100) + === Core classes updates (outstanding ones only) Enumerable:: Index: range.c =================================================================== --- range.c (revision 67421) +++ range.c (revision 67422) @@ -35,7 +35,7 @@ static VALUE r_cover_p(VALUE, VALUE, VAL https://github.com/ruby/ruby/blob/trunk/range.c#L35 static void range_init(VALUE range, VALUE beg, VALUE end, VALUE exclude_end) { - if ((!FIXNUM_P(beg) || !FIXNUM_P(end)) && !NIL_P(end)) { + if ((!FIXNUM_P(beg) || !FIXNUM_P(end)) && !NIL_P(beg) && !NIL_P(end)) { VALUE v; v = rb_funcall(beg, id_cmp, 1, end); @@ -704,8 +704,8 @@ range_bsearch(VALUE range) https://github.com/ruby/ruby/blob/trunk/range.c#L704 } #if SIZEOF_DOUBLE == 8 && defined(HAVE_INT64_T) else if (RB_TYPE_P(beg, T_FLOAT) || RB_TYPE_P(end, T_FLOAT)) { - int64_t low = double_as_int64(RFLOAT_VALUE(rb_Float(beg))); - int64_t high = double_as_int64(NIL_P(end) ? HUGE_VAL : RFLOAT_VALUE(rb_Float(end))); + int64_t low = double_as_int64(NIL_P(beg) ? -HUGE_VAL : RFLOAT_VALUE(rb_Float(beg))); + int64_t high = double_as_int64(NIL_P(end) ? HUGE_VAL : RFLOAT_VALUE(rb_Float(end))); int64_t mid, org_high; BSEARCH(int64_as_double_to_num); } @@ -726,6 +726,18 @@ range_bsearch(VALUE range) https://github.com/ruby/ruby/blob/trunk/range.c#L726 diff = rb_funcall(diff, '*', 1, LONG2FIX(2)); } } + else if (NIL_P(beg) && is_integer_p(end)) { + VALUE diff = LONG2FIX(-1); + RETURN_ENUMERATOR(range, 0, 0); + while (1) { + VALUE mid = rb_funcall(end, '+', 1, diff); + BSEARCH_CHECK(mid); + if (!smaller) { + return bsearch_integer_range(mid, end, 0); + } + diff = rb_funcall(diff, '*', 1, LONG2FIX(2)); + } + } else { rb_raise(rb_eTypeError, "can't do binary search for %s", rb_obj_classname(beg)); } @@ -770,6 +782,9 @@ range_size(VALUE range) https://github.com/ruby/ruby/blob/trunk/range.c#L782 return DBL2NUM(HUGE_VAL); } } + else if (NIL_P(b)) { + return DBL2NUM(HUGE_VAL); + } return Qnil; } @@ -1230,7 +1245,7 @@ rb_range_beg_len(VALUE range, long *begp https://github.com/ruby/ruby/blob/trunk/range.c#L1245 if (!rb_range_values(range, &b, &e, &excl)) return Qfalse; - beg = NUM2LONG(b); + beg = NIL_P(b) ? 0 : NUM2LONG(b); end = NIL_P(e) ? -1 : NUM2LONG(e); if (NIL_P(e)) excl = 0; origbeg = beg; @@ -1392,6 +1407,12 @@ range_include_internal(VALUE range, VALU https://github.com/ruby/ruby/blob/trunk/range.c#L1407 VALUE rb_str_include_range_p(VALUE beg, VALUE end, VALUE val, VALUE exclusive); return rb_str_include_range_p(beg, end, val, RANGE_EXCL(range)); } + else if (NIL_P(beg)) { + VALUE r = rb_funcall(val, id_cmp, 1, end); + if (NIL_P(r)) return Qfalse; + if (rb_cmpint(r, val, end) <= 0) return Qtrue; + return Qfalse; + } else if (NIL_P(end)) { VALUE r = rb_funcall(beg, id_cmp, 1, val); if (NIL_P(r)) return Qfalse; @@ -1487,7 +1508,7 @@ r_cover_range_p(VALUE range, VALUE beg, https://github.com/ruby/ruby/blob/trunk/range.c#L1508 static VALUE r_cover_p(VALUE range, VALUE beg, VALUE end, VALUE val) { - if (r_less(beg, val) <= 0) { + if (NIL_P(beg) || r_less(beg, val) <= 0) { int excl = EXCL(range); if (NIL_P(end) || r_less(val, end) <= -excl) return Qtrue; @@ -1550,9 +1571,15 @@ range_alloc(VALUE klass) https://github.com/ruby/ruby/blob/trunk/range.c#L1571 * ('a'..'e').to_a #=> ["a", "b", "c", "d", "e"] * ('a'...'e').to_a #=> ["a", "b", "c", "d"] * - * == Endless Ranges + * == Beginless/Endless Ranges + * + * A "beginless range" and "endless range" represents a semi-infinite + * range. Literal notation for a beginless range is: + * + * (..1) + * # or + * (...1) * - * An "endless range" represents a semi-infinite range. * Literal notation for an endless range is: * * (1..) @@ -1564,14 +1591,16 @@ range_alloc(VALUE klass) https://github.com/ruby/ruby/blob/trunk/range.c#L1591 * (1..nil) # or similarly (1...nil) * Range.new(1, nil) # or Range.new(1, nil, true) * - * Endless ranges are useful, for example, for idiomatic slicing of - * arrays: + * Beginless/endless ranges are useful, for example, for idiomatic + * slicing of arrays: * + * [1, 2, 3, 4, 5][...2] # => [1, 2] * [1, 2, 3, 4, 5][2...] # => [3, 4, 5] * * Some implementation details: * - * * +end+ of endless range is +nil+; + * * +begin+ of beginless range and +end+ of endless range are +nil+; + * * +each+ of beginless range raises an exception; * * +each+ of endless range enumerates infinite sequence (may be * useful in combination with Enumerable#take_while or similar * methods); Index: test/ruby/test_array.rb =================================================================== --- test/ruby/test_array.rb (revision 67421) +++ test/ruby/test_array.rb (revision 67422) @@ -42,6 +42,8 @@ class TestArray < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_array.rb#L42 assert_equal([1, 2, 3], x[1..3]) assert_equal([1, 2, 3], x[1,3]) assert_equal([3, 4, 5], x[3..]) + assert_equal([0, 1, 2], x[..2]) + assert_equal([0, 1], x[...2]) x[0, 2] = 10 assert_equal([10, 2, 3, 4, 5], x) @@ -375,8 +377,11 @@ class TestArray < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_array.rb#L377 assert_equal(@cls[10, 11, 12], a[9..11]) assert_equal(@cls[98, 99, 100], a[97..]) + assert_equal(@cls[1, 2, 3], a[..2]) + assert_equal(@cls[1, 2], a[...2]) assert_equal(@cls[10, 11, 12], a[-91..-89]) - assert_equal(@cls[98, 99, 100], a[-3..]) + assert_equal(@cls[1, 2, 3], a[..-98]) + assert_equal(@cls[1, 2], a[...-98]) assert_nil(a[10, -3]) assert_equal [], a[10..7] @@ -462,6 +467,14 @@ class TestArray < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_array.rb#L467 assert_equal(nil, a[10..] = nil) assert_equal(@cls[*(0..9).to_a] + @cls[nil], a) + a = @cls[*(0..99).to_a] + assert_equal(nil, a[..10] = nil) + assert_equal(@cls[nil] + @cls[*(11..99).to_a], a) + + a = @cls[*(0..99).to_a] + assert_equal(nil, a[...10] = nil) + assert_equal(@cls[nil] + @cls[*(10..99).to_a], a) + a = @cls[1, 2, 3] a[1, 0] = a assert_equal([1, 1, 2, 3, 2, 3], a) Index: test/ruby/test_range.rb =================================================================== --- test/ruby/test_range.rb (revision 67421) +++ test/ruby/test_range.rb (revision 67422) @@ -691,6 +691,8 @@ class TestRange < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_range.rb#L691 assert_equal Float::INFINITY, (1...).size assert_equal Float::INFINITY, (1.0...).size + assert_equal Float::INFINITY, (...1).size + assert_equal Float::INFINITY, (...1.0).size assert_nil ("a"...).size end @@ -735,6 +737,7 @@ class TestRange < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_range.rb#L737 assert_equal(1, (0...ary.size).bsearch {|i| ary[i] >= 100 }) assert_equal(1_000_001, (0...).bsearch {|i| i > 1_000_000 }) + assert_equal( -999_999, (...0).bsearch {|i| i > -1_000_000 }) end def test_bsearch_for_float @@ -787,7 +790,8 @@ class TestRange < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_range.rb#L790 assert_in_delta(1.0, (0.0..inf).bsearch {|x| Math.log(x) >= 0 }) assert_in_delta(7.0, (0.0..10).bsearch {|x| 7.0 - x }) - assert_equal(1_000_000.0.next_float, (0.0..).bsearch {|x| x > 1_000_000 }) + assert_equal( 1_000_000.0.next_float, (0.0..).bsearch {|x| x > 1_000_000 }) + assert_equal(-1_000_000.0.next_float, (..0.0).bsearch {|x| x > -1_000_000 }) end def check_bsearch_values(range, search, a) @@ -890,6 +894,7 @@ class TestRange < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_range.rb#L894 assert_equal(bignum + 0, (bignum...bignum+ary.size).bsearch {|i| true }) assert_equal(nil, (bignum...bignum+ary.size).bsearch {|i| false }) assert_equal(bignum * 2 + 1, (bignum...).bsearch {|i| i > bignum * 2 }) + assert_equal(-bignum * 2 + 1, (...-bignum).bsearch {|i| i > -bignum * 2 }) assert_raise(TypeError) { ("a".."z").bsearch {} } end @@ -907,4 +912,8 @@ class TestRange < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_range.rb#L912 assert_equal([1,2,3,4], (1...5).to_a) assert_raise(RangeError) { (1..).to_a } end + + def test_beginless_range_iteration + assert_raise(TypeError) { (..1).each { } } + end end Index: parse.y =================================================================== --- parse.y (revision 67421) +++ parse.y (revision 67422) @@ -922,6 +922,8 @@ static void token_info_warn(struct parse https://github.com/ruby/ruby/blob/trunk/parse.y#L922 %token tNMATCH RUBY_TOKEN(NMATCH) "!~" %token tDOT2 RUBY_TOKEN(DOT2) ".." %token tDOT3 RUBY_TOKEN(DOT3) "..." +%token tBDOT2 RUBY_TOKEN(BDOT2) "(.." +%token tBDOT3 RUBY_TOKEN(BDOT3) "(..." %token tAREF RUBY_TOKEN(AREF) "[]" %token tASET RUBY_TOKEN(ASET) "[]=" %token tLSHFT RUBY_TOKEN(LSHFT) "<<" @@ -968,7 +970,7 @@ static void token_info_warn(struct parse https://github.com/ruby/ruby/blob/trunk/parse.y#L970 %right '=' tOP_ASGN %left modifier_rescue %right '?' ':' -%nonassoc tDOT2 tDOT3 +%nonassoc tDOT2 tDOT3 tBDOT2 tBDOT3 %left tOROP %left tANDOP %nonassoc tCMP tEQ tEQQ tNEQ tMATCH tNMATCH @@ -1996,6 +1998,30 @@ arg : lhs '=' arg_rhs https://github.com/ruby/ruby/blob/trunk/parse.y#L1998 /*% %*/ /*% ripper: dot3!($1, Qnil) %*/ } + | tBDOT2 arg + { + /*%%%*/ + YYLTYPE loc; + loc.beg_pos = @1.beg_pos; + loc.end_pos = @1.beg_pos; + + value_expr($2); + $$ = NEW_DOT2(new_nil(&loc), $2, &@$); + /*% %*/ + /*% ripper: dot2!(Qnil, $2) %*/ + } + | tBDOT3 arg + { + /*%%%*/ + YYLTYPE loc; + loc.beg_pos = @1.beg_pos; + loc.end_pos = @1.beg_pos; + + value_expr($2); + $$ = NEW_DOT3(new_nil(&loc), $2, &@$); + /*% %*/ + /*% ripper: dot3!(Qnil, $2) %*/ + } | arg '+' arg { $$ = call_bin_op(p, $1, '+', $3, &@2, &@$); @@ -8241,15 +8267,16 @@ parser_yylex(struct parser_params *p) https://github.com/ruby/ruby/blob/trunk/parse.y#L8267 pushback(p, c); return warn_balanced('-', "-", "unary operator"); - case '.': + case '.': { + int is_beg = IS_BEG(); SET_LEX_STATE(EXPR_BEG); switch (c = nextc(p)) { case '.': if ((c = nextc(p)) == '.') { - return tDOT3; + return is_beg ? tBDOT3 : tDOT3; } pushback(p, c); - return tDOT2; + return is_beg ? tBDOT2 : tDOT2; case ':': switch (c = nextc(p)) { default: @@ -8275,6 +8302,7 @@ parser_yylex(struct parser_params *p) https://github.com/ruby/ruby/blob/trunk/parse.y#L8302 set_yylval_id('.'); SET_LEX_STATE(EXPR_DOT); return '.'; + } case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': Index: doc/syntax/literals.rdoc =================================================================== --- doc/syntax/literals.rdoc (revision 67421) +++ doc/syntax/literals.rdoc (revision 67422) @@ -334,6 +334,7 @@ its ending value. https://github.com/ruby/ruby/blob/trunk/doc/syntax/literals.rdoc#L334 (1..2) # includes its ending value (1...2) # excludes its ending value (1..) # endless range, representing infinite sequence from 1 to Infinity + (..1) # beginless range, representing infinite sequence from -Infinity to 1 You may create a range of any object. See the Range documentation for details on the methods you need to implement. Index: defs/id.def =================================================================== --- defs/id.def (revision 67421) +++ defs/id.def (revision 67422) @@ -76,6 +76,8 @@ firstline, predefined = __LINE__+1, %[\ https://github.com/ruby/ruby/blob/trunk/defs/id.def#L76 token_ops = %[\ Dot2 .. DOT2 Dot3 ... DOT3 + BDot2 .. BDOT2 + BDot3 ... BDOT3 UPlus +@ UPLUS UMinus -@ UMINUS Pow ** POW Index: spec/ruby/core/range/new_spec.rb =================================================================== --- spec/ruby/core/range/new_spec.rb (revision 67421) +++ spec/ruby/core/range/new_spec.rb (revision 67422) @@ -43,9 +43,25 @@ describe "Range.new" do https://github.com/ruby/ruby/blob/trunk/spec/ruby/core/range/new_spec.rb#L43 end end - describe "endless range" do - it "does not allow range without left boundary" do - -> { Range.new(nil, 1) }.should raise_error(ArgumentError, /bad value for range/) + describe "beginless/endless range" do + ruby_version_is ""..."2.7" do + it "does not allow range without left boundary" do + -> { Range.new(nil, 1) }.should raise_error(ArgumentError, /bad value for range/) + end + end + + ruby_version_is "2.7" do + it "allows beginless left boundary" do + range = Range.new(nil, 1) + range.begin.should == nil + end + + it "distinguishes ranges with included and excluded right boundary" do + range_exclude = Range.new(nil, 1, true) + range_include = Range.new(nil, 1, false) + + range_exclude.should_not == range_include + end end ruby_version_is ""..."2.6" do -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/