Skip to content

Commit

Permalink
Introduce beginless range [Feature#14799]
Browse files Browse the repository at this point in the history
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67422 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
  • Loading branch information
mame committed Apr 3, 2019
1 parent dd2479b commit 95f7992
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 19 deletions.
6 changes: 6 additions & 0 deletions NEWS
Expand Up @@ -28,6 +28,12 @@ sufficient information, see the ChangeLog file or Redmine
* 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::
Expand Down
2 changes: 2 additions & 0 deletions defs/id.def
Expand Up @@ -76,6 +76,8 @@ firstline, predefined = __LINE__+1, %[\
token_ops = %[\
Dot2 .. DOT2
Dot3 ... DOT3
BDot2 .. BDOT2
BDot3 ... BDOT3
UPlus +@ UPLUS
UMinus -@ UMINUS
Pow ** POW
Expand Down
1 change: 1 addition & 0 deletions doc/syntax/literals.rdoc
Expand Up @@ -334,6 +334,7 @@ its ending value.
(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.
Expand Down
36 changes: 32 additions & 4 deletions parse.y
Expand Up @@ -922,6 +922,8 @@ static void token_info_warn(struct parser_params *p, const char *token, token_in
%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) "<<"
Expand Down Expand Up @@ -968,7 +970,7 @@ static void token_info_warn(struct parser_params *p, const char *token, token_in
%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
Expand Down Expand Up @@ -1996,6 +1998,30 @@ arg : lhs '=' arg_rhs
/*% %*/
/*% 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, &@$);
Expand Down Expand Up @@ -8241,15 +8267,16 @@ parser_yylex(struct parser_params *p)
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:
Expand All @@ -8275,6 +8302,7 @@ parser_yylex(struct parser_params *p)
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':
Expand Down
49 changes: 39 additions & 10 deletions range.c
Expand Up @@ -35,7 +35,7 @@ static VALUE r_cover_p(VALUE, VALUE, VALUE, VALUE);
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);
Expand Down Expand Up @@ -704,8 +704,8 @@ range_bsearch(VALUE range)
}
#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);
}
Expand All @@ -726,6 +726,18 @@ range_bsearch(VALUE range)
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));
}
Expand Down Expand Up @@ -770,6 +782,9 @@ range_size(VALUE range)
return DBL2NUM(HUGE_VAL);
}
}
else if (NIL_P(b)) {
return DBL2NUM(HUGE_VAL);
}

return Qnil;
}
Expand Down Expand Up @@ -1230,7 +1245,7 @@ rb_range_beg_len(VALUE range, long *begp, long *lenp, long len, int err)

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;
Expand Down Expand Up @@ -1392,6 +1407,12 @@ range_include_internal(VALUE range, VALUE val)
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;
Expand Down Expand Up @@ -1487,7 +1508,7 @@ r_cover_range_p(VALUE range, VALUE beg, VALUE end, VALUE val)
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;
Expand Down Expand Up @@ -1550,9 +1571,15 @@ range_alloc(VALUE klass)
* ('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..)
Expand All @@ -1564,14 +1591,16 @@ range_alloc(VALUE klass)
* (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);
Expand Down
22 changes: 19 additions & 3 deletions spec/ruby/core/range/new_spec.rb
Expand Up @@ -43,9 +43,25 @@
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
Expand Down
15 changes: 14 additions & 1 deletion test/ruby/test_array.rb
Expand Up @@ -42,6 +42,8 @@ def test_0_literal
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)
Expand Down Expand Up @@ -375,8 +377,11 @@ def test_AREF # '[]'

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]
Expand Down Expand Up @@ -462,6 +467,14 @@ def test_ASET # '[]='
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)
Expand Down
11 changes: 10 additions & 1 deletion test/ruby/test_range.rb
Expand Up @@ -691,6 +691,8 @@ def test_size

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

Expand Down Expand Up @@ -735,6 +737,7 @@ def test_bsearch_for_fixnum
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
Expand Down Expand Up @@ -787,7 +790,8 @@ def test_bsearch_for_float
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)
Expand Down Expand Up @@ -890,6 +894,7 @@ def test_bsearch_for_bignum
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
Expand All @@ -907,4 +912,8 @@ def test_to_a
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

1 comment on commit 95f7992

@localhostdotdev
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Please sign in to comment.