Make Range#step to consistently use + for iteration (#7444)

Make Range#step to consistently use + for iteration [Feature #18368]

Previously, non-numerics expected step to be integer,
and iterated with begin#succ, skipping over step value
steps. Since this commit, numeric and non-numeric iteration
behaves the same way, by using + operator.
This commit is contained in:
Victor Shepelev 2024-08-18 13:15:18 +03:00 committed by GitHub
parent 4dbf386ca2
commit d450f9d6a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
Notes: git 2024-08-18 10:15:51 +00:00
Merged-By: zverok <zverok.offline@gmail.com>
6 changed files with 662 additions and 351 deletions

View File

@ -43,6 +43,13 @@ Note: We're only listing outstanding class updates.
* Range * Range
* Range#size now raises TypeError if the range is not iterable. [[Misc #18984]] * Range#size now raises TypeError if the range is not iterable. [[Misc #18984]]
* Range#step now consistently has a semantics of iterating by using `+` operator
for all types, not only numerics. [[Feature #18368]]
```ruby
(Time.utc(2022, 2, 24)..).step(24*60*60).take(3)
#=> [2022-02-24 00:00:00 UTC, 2022-02-25 00:00:00 UTC, 2022-02-26 00:00:00 UTC]
```
* RubyVM::AbstractSyntaxTree * RubyVM::AbstractSyntaxTree
@ -168,3 +175,4 @@ See GitHub releases like [GitHub Releases of Logger](https://github.com/ruby/log
[Feature #20443]: https://bugs.ruby-lang.org/issues/20443 [Feature #20443]: https://bugs.ruby-lang.org/issues/20443
[Feature #20497]: https://bugs.ruby-lang.org/issues/20497 [Feature #20497]: https://bugs.ruby-lang.org/issues/20497
[Feature #20624]: https://bugs.ruby-lang.org/issues/20624 [Feature #20624]: https://bugs.ruby-lang.org/issues/20624
[Feature #18368]: https://bugs.ruby-lang.org/issues/18368

309
range.c
View File

@ -29,7 +29,7 @@
#include "internal/range.h" #include "internal/range.h"
VALUE rb_cRange; VALUE rb_cRange;
static ID id_beg, id_end, id_excl; static ID id_beg, id_end, id_excl, id_plus;
#define id_cmp idCmp #define id_cmp idCmp
#define id_succ idSucc #define id_succ idSucc
#define id_min idMin #define id_min idMin
@ -308,40 +308,6 @@ range_each_func(VALUE range, int (*func)(VALUE, VALUE), VALUE arg)
} }
} }
static bool
step_i_iter(VALUE arg)
{
VALUE *iter = (VALUE *)arg;
if (FIXNUM_P(iter[0])) {
iter[0] -= INT2FIX(1) & ~FIXNUM_FLAG;
}
else {
iter[0] = rb_funcall(iter[0], '-', 1, INT2FIX(1));
}
if (iter[0] != INT2FIX(0)) return false;
iter[0] = iter[1];
return true;
}
static int
sym_step_i(VALUE i, VALUE arg)
{
if (step_i_iter(arg)) {
rb_yield(rb_str_intern(i));
}
return 0;
}
static int
step_i(VALUE i, VALUE arg)
{
if (step_i_iter(arg)) {
rb_yield(i);
}
return 0;
}
static int static int
discrete_object_p(VALUE obj) discrete_object_p(VALUE obj)
{ {
@ -400,72 +366,123 @@ range_step_size(VALUE range, VALUE args, VALUE eobj)
/* /*
* call-seq: * call-seq:
* step(n = 1) {|element| ... } -> self * step(s = 1) {|element| ... } -> self
* step(n = 1) -> enumerator * step(s = 1) -> enumerator/arithmetic_sequence
* *
* Iterates over the elements of +self+. * Iterates over the elements of range in steps of +s+. The iteration is performed
* by <tt>+</tt> operator:
* *
* With a block given and no argument, * (0..6).step(2) { puts _1 } #=> 1..5
* calls the block each element of the range; returns +self+: * # Prints: 0, 2, 4, 6
* *
* a = [] * # Iterate between two dates in step of 1 day (24 hours)
* (1..5).step {|element| a.push(element) } # => 1..5 * (Time.utc(2022, 2, 24)..Time.utc(2022, 3, 1)).step(24*60*60) { puts _1 }
* a # => [1, 2, 3, 4, 5] * # Prints:
* a = [] * # 2022-02-24 00:00:00 UTC
* ('a'..'e').step {|element| a.push(element) } # => "a".."e" * # 2022-02-25 00:00:00 UTC
* a # => ["a", "b", "c", "d", "e"] * # 2022-02-26 00:00:00 UTC
* # 2022-02-27 00:00:00 UTC
* # 2022-02-28 00:00:00 UTC
* # 2022-03-01 00:00:00 UTC
* *
* With a block given and a positive integer argument +n+ given, * If <tt> + step</tt> decreases the value, iteration is still performed when
* calls the block with element +0+, element +n+, element <tt>2n</tt>, and so on: * step +begin+ is higher than the +end+:
* *
* a = [] * (0..6).step(-2) { puts _1 }
* (1..5).step(2) {|element| a.push(element) } # => 1..5 * # Prints nothing
* a # => [1, 3, 5]
* a = []
* ('a'..'e').step(2) {|element| a.push(element) } # => "a".."e"
* a # => ["a", "c", "e"]
* *
* With no block given, returns an enumerator, * (6..0).step(-2) { puts _1 }
* which will be of class Enumerator::ArithmeticSequence if +self+ is numeric; * # Prints: 6, 4, 2, 0
* otherwise of class Enumerator:
* *
* e = (1..5).step(2) # => ((1..5).step(2)) * (Time.utc(2022, 3, 1)..Time.utc(2022, 2, 24)).step(-24*60*60) { puts _1 }
* e.class # => Enumerator::ArithmeticSequence * # Prints:
* ('a'..'e').step # => #<Enumerator: ...> * # 2022-03-01 00:00:00 UTC
* # 2022-02-28 00:00:00 UTC
* # 2022-02-27 00:00:00 UTC
* # 2022-02-26 00:00:00 UTC
* # 2022-02-25 00:00:00 UTC
* # 2022-02-24 00:00:00 UTC
*
* When the block is not provided, and range boundaries and step are Numeric,
* the method returns Enumerator::ArithmeticSequence.
*
* (1..5).step(2) # => ((1..5).step(2))
* (1.0..).step(1.5) #=> ((1.0..).step(1.5))
* (..3r).step(1/3r) #=> ((..3/1).step((1/3)))
*
* Enumerator::ArithmeticSequence can be further used as a value object for iteration
* or slicing of collections (see Array#[]). There is a convenience method #% with
* behavior similar to +step+ to produce arithmetic sequences more expressively:
*
* # Same as (1..5).step(2)
* (1..5) % 2 # => ((1..5).%(2))
*
* In a generic case, when the block is not provided, Enumerator is returned:
*
* ('a'..).step('b') #=> #<Enumerator: "a"..:step("b")>
* ('a'..).step('b').take(3) #=> ["a", "ab", "abb"]
*
* If +s+ is not provided, it is considered +1+ for ranges with numeric +begin+:
*
* (1..5).step { p _1 }
* # Prints: 1, 2, 3, 4, 5
*
* For non-Numeric ranges, step absence is an error:
*
* ('a'..'z').step { p _1 }
* # raises: step is required for non-numeric ranges (ArgumentError)
* *
* Related: Range#%.
*/ */
static VALUE static VALUE
range_step(int argc, VALUE *argv, VALUE range) range_step(int argc, VALUE *argv, VALUE range)
{ {
VALUE b, e, step, tmp; VALUE b, e, v, step;
int c, dir;
b = RANGE_BEG(range); b = RANGE_BEG(range);
e = RANGE_END(range); e = RANGE_END(range);
step = (!rb_check_arity(argc, 0, 1) ? INT2FIX(1) : argv[0]);
const VALUE b_num_p = rb_obj_is_kind_of(b, rb_cNumeric);
const VALUE e_num_p = rb_obj_is_kind_of(e, rb_cNumeric);
if (rb_check_arity(argc, 0, 1))
step = argv[0];
else {
if (b_num_p || (NIL_P(b) && e_num_p))
step = INT2FIX(1);
else
rb_raise(rb_eArgError, "step is required for non-numeric ranges");
}
const VALUE step_num_p = rb_obj_is_kind_of(step, rb_cNumeric);
if (step_num_p && b_num_p && rb_equal(step, INT2FIX(0))) {
rb_raise(rb_eArgError, "step can't be 0");
}
if (!rb_block_given_p()) { if (!rb_block_given_p()) {
if (!rb_obj_is_kind_of(step, rb_cNumeric)) { // This code is allowed to create even beginless ArithmeticSequence, which can be useful,
step = rb_to_int(step); // e.g., for array slicing:
} // ary[(..-1) % 3]
if (rb_equal(step, INT2FIX(0))) { if (step_num_p && ((b_num_p && (NIL_P(e) || e_num_p)) || (NIL_P(b) && e_num_p))) {
rb_raise(rb_eArgError, "step can't be 0");
}
const VALUE b_num_p = rb_obj_is_kind_of(b, rb_cNumeric);
const VALUE e_num_p = rb_obj_is_kind_of(e, rb_cNumeric);
if ((b_num_p && (NIL_P(e) || e_num_p)) || (NIL_P(b) && e_num_p)) {
return rb_arith_seq_new(range, ID2SYM(rb_frame_this_func()), argc, argv, return rb_arith_seq_new(range, ID2SYM(rb_frame_this_func()), argc, argv,
range_step_size, b, e, step, EXCL(range)); range_step_size, b, e, step, EXCL(range));
} }
RETURN_SIZED_ENUMERATOR(range, argc, argv, range_step_size); // ...but generic Enumerator from beginless range is useless and probably an error.
if (NIL_P(b)) {
rb_raise(rb_eArgError, "#step for non-numeric beginless ranges is meaningless");
}
RETURN_SIZED_ENUMERATOR(range, argc, argv, 0);
} }
step = check_step_domain(step); if (NIL_P(b)) {
VALUE iter[2] = {INT2FIX(1), step}; rb_raise(rb_eArgError, "#step iteration for beginless ranges is meaningless");
}
if (FIXNUM_P(b) && NIL_P(e) && FIXNUM_P(step)) { if (FIXNUM_P(b) && NIL_P(e) && FIXNUM_P(step)) {
/* perform summation of numbers in C until their reach Fixnum limit */
long i = FIX2LONG(b), unit = FIX2LONG(step); long i = FIX2LONG(b), unit = FIX2LONG(step);
do { do {
rb_yield(LONG2FIX(i)); rb_yield(LONG2FIX(i));
@ -473,71 +490,77 @@ range_step(int argc, VALUE *argv, VALUE range)
} while (FIXABLE(i)); } while (FIXABLE(i));
b = LONG2NUM(i); b = LONG2NUM(i);
/* then switch to Bignum API */
for (;; b = rb_big_plus(b, step)) for (;; b = rb_big_plus(b, step))
rb_yield(b); rb_yield(b);
} }
else if (FIXNUM_P(b) && FIXNUM_P(e) && FIXNUM_P(step)) { /* fixnums are special */ else if (FIXNUM_P(b) && FIXNUM_P(e) && FIXNUM_P(step)) {
/* fixnums are special: summation is performed in C for performance */
long end = FIX2LONG(e); long end = FIX2LONG(e);
long i, unit = FIX2LONG(step); long i, unit = FIX2LONG(step);
if (!EXCL(range)) if (unit < 0) {
end += 1; if (!EXCL(range))
i = FIX2LONG(b); end -= 1;
while (i < end) { i = FIX2LONG(b);
rb_yield(LONG2NUM(i)); while (i > end) {
if (i + unit < i) break; rb_yield(LONG2NUM(i));
i += unit; i += unit;
} }
} else {
} if (!EXCL(range))
else if (SYMBOL_P(b) && (NIL_P(e) || SYMBOL_P(e))) { /* symbols are special */ end += 1;
b = rb_sym2str(b); i = FIX2LONG(b);
if (NIL_P(e)) { while (i < end) {
rb_str_upto_endless_each(b, sym_step_i, (VALUE)iter); rb_yield(LONG2NUM(i));
} i += unit;
else { }
rb_str_upto_each(b, rb_sym2str(e), EXCL(range), sym_step_i, (VALUE)iter);
} }
} }
else if (ruby_float_step(b, e, step, EXCL(range), TRUE)) { else if (b_num_p && step_num_p && ruby_float_step(b, e, step, EXCL(range), TRUE)) {
/* done */ /* done */
} }
else if (rb_obj_is_kind_of(b, rb_cNumeric) ||
!NIL_P(rb_check_to_integer(b, "to_int")) ||
!NIL_P(rb_check_to_integer(e, "to_int"))) {
ID op = EXCL(range) ? '<' : idLE;
VALUE v = b;
int i = 0;
while (NIL_P(e) || RTEST(rb_funcall(v, op, 1, e))) {
rb_yield(v);
i++;
v = rb_funcall(b, '+', 1, rb_funcall(INT2NUM(i), '*', 1, step));
}
}
else { else {
tmp = rb_check_string_type(b); v = b;
if (!NIL_P(e)) {
if (b_num_p && step_num_p && r_less(step, INT2FIX(0)) < 0) {
// iterate backwards, for consistency with ArithmeticSequence
if (EXCL(range)) {
for (; r_less(e, v) < 0; v = rb_funcall(v, id_plus, 1, step))
rb_yield(v);
}
else {
for (; (c = r_less(e, v)) <= 0; v = rb_funcall(v, id_plus, 1, step)) {
rb_yield(v);
if (!c) break;
}
}
if (!NIL_P(tmp)) { } else {
b = tmp; // Direction of the comparison. We use it as a comparison operator in cycle:
if (NIL_P(e)) { // if begin < end, the cycle performs while value < end (iterating forward)
rb_str_upto_endless_each(b, step_i, (VALUE)iter); // if begin > end, the cycle performs while value > end (iterating backward with
} // a negative step)
else { dir = r_less(b, e);
rb_str_upto_each(b, e, EXCL(range), step_i, (VALUE)iter); // One preliminary addition to check the step moves iteration in the same direction as
// from begin to end; otherwise, the iteration should be empty.
if (r_less(b, rb_funcall(b, id_plus, 1, step)) == dir) {
if (EXCL(range)) {
for (; r_less(v, e) == dir; v = rb_funcall(v, id_plus, 1, step))
rb_yield(v);
}
else {
for (; (c = r_less(v, e)) == dir || c == 0; v = rb_funcall(v, id_plus, 1, step)) {
rb_yield(v);
if (!c) break;
}
}
}
} }
} }
else { else
if (!discrete_object_p(b)) { for (;; v = rb_funcall(v, id_plus, 1, step))
rb_raise(rb_eTypeError, "can't iterate from %s", rb_yield(v);
rb_obj_classname(b));
}
if (!NIL_P(e))
range_each_func(range, step_i, (VALUE)iter);
else
for (;; b = rb_funcallv(b, id_succ, 0, 0))
step_i(b, (VALUE)iter);
}
} }
return range; return range;
} }
@ -545,29 +568,24 @@ range_step(int argc, VALUE *argv, VALUE range)
/* /*
* call-seq: * call-seq:
* %(n) {|element| ... } -> self * %(n) {|element| ... } -> self
* %(n) -> enumerator * %(n) -> enumerator or arithmetic_sequence
* *
* Iterates over the elements of +self+. * Same as #step (but doesn't provide default value for +n+).
* The method is convenient for experssive producing of Enumerator::ArithmeticSequence.
* *
* With a block given, calls the block with selected elements of the range; * array = [0, 1, 2, 3, 4, 5, 6]
* returns +self+:
* *
* a = [] * # slice each second element:
* (1..5).%(2) {|element| a.push(element) } # => 1..5 * seq = (0..) % 2 #=> ((0..).%(2))
* a # => [1, 3, 5] * array[seq] #=> [0, 2, 4, 6]
* a = [] * # or just
* ('a'..'e').%(2) {|element| a.push(element) } # => "a".."e" * array[(0..) % 2] #=> [0, 2, 4, 6]
* a # => ["a", "c", "e"]
* *
* With no block given, returns an enumerator, * Note that due to operator precedence in Ruby, parentheses are mandatory around range
* which will be of class Enumerator::ArithmeticSequence if +self+ is numeric; * in this case:
* otherwise of class Enumerator:
* *
* e = (1..5) % 2 # => ((1..5).%(2)) * (0..7) % 2 #=> ((0..7).%(2)) -- as expected
* e.class # => Enumerator::ArithmeticSequence * 0..7 % 2 #=> 0..1 -- parsed as 0..(7 % 2)
* ('a'..'e') % 2 # => #<Enumerator: ...>
*
* Related: Range#step.
*/ */
static VALUE static VALUE
range_percent_step(VALUE range, VALUE step) range_percent_step(VALUE range, VALUE step)
@ -2641,6 +2659,7 @@ Init_Range(void)
id_beg = rb_intern_const("begin"); id_beg = rb_intern_const("begin");
id_end = rb_intern_const("end"); id_end = rb_intern_const("end");
id_excl = rb_intern_const("excl"); id_excl = rb_intern_const("excl");
id_plus = rb_intern_const("+");
rb_cRange = rb_struct_define_without_accessor( rb_cRange = rb_struct_define_without_accessor(
"Range", rb_cObject, range_alloc, "Range", rb_cObject, range_alloc,

View File

@ -10,44 +10,50 @@ describe "Range#step" do
r.step { }.should equal(r) r.step { }.should equal(r)
end end
it "raises TypeError if step" do ruby_version_is ""..."3.4" do
obj = mock("mock") it "calls #to_int to coerce step to an Integer" do
-> { (1..10).step(obj) { } }.should raise_error(TypeError) obj = mock("Range#step")
obj.should_receive(:to_int).and_return(1)
(1..2).step(obj) { |x| ScratchPad << x }
ScratchPad.recorded.should eql([1, 2])
end
it "raises a TypeError if step does not respond to #to_int" do
obj = mock("Range#step non-integer")
-> { (1..2).step(obj) { } }.should raise_error(TypeError)
end
it "raises a TypeError if #to_int does not return an Integer" do
obj = mock("Range#step non-integer")
obj.should_receive(:to_int).and_return("1")
-> { (1..2).step(obj) { } }.should raise_error(TypeError)
end
it "raises a TypeError if the first element does not respond to #succ" do
obj = mock("Range#step non-comparable")
obj.should_receive(:<=>).with(obj).and_return(1)
-> { (obj..obj).step { |x| x } }.should raise_error(TypeError)
end
end end
it "calls #to_int to coerce step to an Integer" do ruby_version_is "3.4" do
obj = mock("Range#step") it "calls #coerce to coerce step to an Integer" do
obj.should_receive(:to_int).and_return(1) obj = mock("Range#step")
obj.should_receive(:coerce).at_least(:once).and_return([1, 2])
(1..2).step(obj) { |x| ScratchPad << x } (1..3).step(obj) { |x| ScratchPad << x }
ScratchPad.recorded.should eql([1, 2]) ScratchPad.recorded.should eql([1, 3])
end end
it "raises a TypeError if step does not respond to #to_int" do it "raises a TypeError if step does not respond to #coerce" do
obj = mock("Range#step non-integer") obj = mock("Range#step non-coercible")
-> { (1..2).step(obj) { } }.should raise_error(TypeError) -> { (1..2).step(obj) { } }.should raise_error(TypeError)
end end
it "raises a TypeError if #to_int does not return an Integer" do
obj = mock("Range#step non-integer")
obj.should_receive(:to_int).and_return("1")
-> { (1..2).step(obj) { } }.should raise_error(TypeError)
end
it "coerces the argument to integer by invoking to_int" do
(obj = mock("2")).should_receive(:to_int).and_return(2)
res = []
(1..10).step(obj) {|x| res << x}
res.should == [1, 3, 5, 7, 9]
end
it "raises a TypeError if the first element does not respond to #succ" do
obj = mock("Range#step non-comparable")
obj.should_receive(:<=>).with(obj).and_return(1)
-> { (obj..obj).step { |x| x } }.should raise_error(TypeError)
end end
it "raises an ArgumentError if step is 0" do it "raises an ArgumentError if step is 0" do
@ -58,8 +64,17 @@ describe "Range#step" do
-> { (-1..1).step(0.0) { |x| x } }.should raise_error(ArgumentError) -> { (-1..1).step(0.0) { |x| x } }.should raise_error(ArgumentError)
end end
it "raises an ArgumentError if step is negative" do ruby_version_is "3.4" do
-> { (-1..1).step(-2) { |x| x } }.should raise_error(ArgumentError) it "does not raise an ArgumentError if step is 0 for non-numeric ranges" do
t = Time.utc(2023, 2, 24)
-> { (t..t+1).step(0) { break } }.should_not raise_error(ArgumentError)
end
end
ruby_version_is ""..."3.4" do
it "raises an ArgumentError if step is negative" do
-> { (-1..1).step(-2) { |x| x } }.should raise_error(ArgumentError)
end
end end
describe "with inclusive end" do describe "with inclusive end" do
@ -78,6 +93,18 @@ describe "Range#step" do
(-2..2).step(1.5) { |x| ScratchPad << x } (-2..2).step(1.5) { |x| ScratchPad << x }
ScratchPad.recorded.should eql([-2.0, -0.5, 1.0]) ScratchPad.recorded.should eql([-2.0, -0.5, 1.0])
end end
ruby_version_is "3.4" do
it "does not iterate if step is negative for forward range" do
(-1..1).step(-1) { |x| ScratchPad << x }
ScratchPad.recorded.should eql([])
end
it "iterates backward if step is negative for backward range" do
(1..-1).step(-1) { |x| ScratchPad << x }
ScratchPad.recorded.should eql([1, 0, -1])
end
end
end end
describe "and Float values" do describe "and Float values" do
@ -148,27 +175,114 @@ describe "Range#step" do
end end
describe "and String values" do describe "and String values" do
it "yields String values incremented by #succ and less than or equal to end when not passed a step" do ruby_version_is ""..."3.4" do
("A".."E").step { |x| ScratchPad << x } it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
ScratchPad.recorded.should == ["A", "B", "C", "D", "E"] ("A".."E").step { |x| ScratchPad << x }
ScratchPad.recorded.should == ["A", "B", "C", "D", "E"]
end
it "yields String values incremented by #succ called Integer step times" do
("A".."G").step(2) { |x| ScratchPad << x }
ScratchPad.recorded.should == ["A", "C", "E", "G"]
end
it "raises a TypeError when passed a Float step" do
-> { ("A".."G").step(2.0) { } }.should raise_error(TypeError)
end
it "calls #succ on begin and each element returned by #succ" do
obj = mock("Range#step String start")
obj.should_receive(:<=>).exactly(3).times.and_return(-1, -1, -1, 0)
obj.should_receive(:succ).exactly(2).times.and_return(obj)
(obj..obj).step { |x| ScratchPad << x }
ScratchPad.recorded.should == [obj, obj, obj]
end
end end
it "yields String values incremented by #succ called Integer step times" do ruby_version_is "3.4" do
("A".."G").step(2) { |x| ScratchPad << x } it "raises an ArgumentError when not passed a step" do
ScratchPad.recorded.should == ["A", "C", "E", "G"] -> { ("A".."E").step { } }.should raise_error(ArgumentError)
end end
it "raises a TypeError when passed a Float step" do it "yields String values adjusted by step and less than or equal to end" do
-> { ("A".."G").step(2.0) { } }.should raise_error(TypeError) ("A".."AAA").step("A") { |x| ScratchPad << x }
end ScratchPad.recorded.should == ["A", "AA", "AAA"]
end
it "calls #succ on begin and each element returned by #succ" do it "raises a TypeError when passed an incompatible type step" do
obj = mock("Range#step String start") -> { ("A".."G").step(2) { } }.should raise_error(TypeError)
obj.should_receive(:<=>).exactly(3).times.and_return(-1, -1, -1, 0) end
obj.should_receive(:succ).exactly(2).times.and_return(obj)
(obj..obj).step { |x| ScratchPad << x } it "calls #+ on begin and each element returned by #+" do
ScratchPad.recorded.should == [obj, obj, obj] start = mock("Range#step String start")
stop = mock("Range#step String stop")
mid1 = mock("Range#step String mid1")
mid2 = mock("Range#step String mid2")
step = mock("Range#step String step")
# Deciding on the direction of iteration
start.should_receive(:<=>).with(stop).at_least(:twice).and_return(-1)
# Deciding whether the step moves iteration in the right direction
start.should_receive(:<=>).with(mid1).and_return(-1)
# Iteration 1
start.should_receive(:+).at_least(:once).with(step).and_return(mid1)
# Iteration 2
mid1.should_receive(:<=>).with(stop).and_return(-1)
mid1.should_receive(:+).with(step).and_return(mid2)
# Iteration 3
mid2.should_receive(:<=>).with(stop).and_return(0)
(start..stop).step(step) { |x| ScratchPad << x }
ScratchPad.recorded.should == [start, mid1, mid2]
end
it "iterates backward if the step is decreasing values, and the range is backward" do
start = mock("Range#step String start")
stop = mock("Range#step String stop")
mid1 = mock("Range#step String mid1")
mid2 = mock("Range#step String mid2")
step = mock("Range#step String step")
# Deciding on the direction of iteration
start.should_receive(:<=>).with(stop).at_least(:twice).and_return(1)
# Deciding whether the step moves iteration in the right direction
start.should_receive(:<=>).with(mid1).and_return(1)
# Iteration 1
start.should_receive(:+).at_least(:once).with(step).and_return(mid1)
# Iteration 2
mid1.should_receive(:<=>).with(stop).and_return(1)
mid1.should_receive(:+).with(step).and_return(mid2)
# Iteration 3
mid2.should_receive(:<=>).with(stop).and_return(0)
(start..stop).step(step) { |x| ScratchPad << x }
ScratchPad.recorded.should == [start, mid1, mid2]
end
it "does no iteration of the direction of the range and of the step don't match" do
start = mock("Range#step String start")
stop = mock("Range#step String stop")
mid1 = mock("Range#step String mid1")
mid2 = mock("Range#step String mid2")
step = mock("Range#step String step")
# Deciding on the direction of iteration: stop > start
start.should_receive(:<=>).with(stop).at_least(:twice).and_return(1)
# Deciding whether the step moves iteration in the right direction
# start + step < start, the direction is opposite to the range's
start.should_receive(:+).with(step).and_return(mid1)
start.should_receive(:<=>).with(mid1).and_return(-1)
(start..stop).step(step) { |x| ScratchPad << x }
ScratchPad.recorded.should == []
end
end end
end end
end end
@ -266,18 +380,35 @@ describe "Range#step" do
end end
describe "and String values" do describe "and String values" do
it "yields String values incremented by #succ and less than or equal to end when not passed a step" do ruby_version_is ""..."3.4" do
("A"..."E").step { |x| ScratchPad << x } it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
ScratchPad.recorded.should == ["A", "B", "C", "D"] ("A"..."E").step { |x| ScratchPad << x }
ScratchPad.recorded.should == ["A", "B", "C", "D"]
end
it "yields String values incremented by #succ called Integer step times" do
("A"..."G").step(2) { |x| ScratchPad << x }
ScratchPad.recorded.should == ["A", "C", "E"]
end
it "raises a TypeError when passed a Float step" do
-> { ("A"..."G").step(2.0) { } }.should raise_error(TypeError)
end
end end
it "yields String values incremented by #succ called Integer step times" do ruby_version_is "3.4" do
("A"..."G").step(2) { |x| ScratchPad << x } it "raises an ArgumentError when not passed a step" do
ScratchPad.recorded.should == ["A", "C", "E"] -> { ("A".."E").step { } }.should raise_error(ArgumentError)
end end
it "raises a TypeError when passed a Float step" do it "yields String values adjusted by step and less than or equal to end" do
-> { ("A"..."G").step(2.0) { } }.should raise_error(TypeError) ("A"..."AAA").step("A") { |x| ScratchPad << x }
ScratchPad.recorded.should == ["A", "AA"]
end
it "raises a TypeError when passed an incompatible type step" do
-> { ("A".."G").step(2) { } }.should raise_error(TypeError)
end
end end
end end
end end
@ -351,27 +482,49 @@ describe "Range#step" do
end end
describe "and String values" do describe "and String values" do
it "yields String values incremented by #succ and less than or equal to end when not passed a step" do ruby_version_is ""..."3.4" do
eval("('A'..)").step { |x| break if x > "D"; ScratchPad << x } it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
ScratchPad.recorded.should == ["A", "B", "C", "D"] eval("('A'..)").step { |x| break if x > "D"; ScratchPad << x }
ScratchPad.recorded.should == ["A", "B", "C", "D"]
ScratchPad.record [] ScratchPad.record []
eval("('A'...)").step { |x| break if x > "D"; ScratchPad << x } eval("('A'...)").step { |x| break if x > "D"; ScratchPad << x }
ScratchPad.recorded.should == ["A", "B", "C", "D"] ScratchPad.recorded.should == ["A", "B", "C", "D"]
end
it "yields String values incremented by #succ called Integer step times" do
eval("('A'..)").step(2) { |x| break if x > "F"; ScratchPad << x }
ScratchPad.recorded.should == ["A", "C", "E"]
ScratchPad.record []
eval("('A'...)").step(2) { |x| break if x > "F"; ScratchPad << x }
ScratchPad.recorded.should == ["A", "C", "E"]
end
it "raises a TypeError when passed a Float step" do
-> { eval("('A'..)").step(2.0) { } }.should raise_error(TypeError)
-> { eval("('A'...)").step(2.0) { } }.should raise_error(TypeError)
end
end end
it "yields String values incremented by #succ called Integer step times" do ruby_version_is "3.4" do
eval("('A'..)").step(2) { |x| break if x > "F"; ScratchPad << x } it "raises an ArgumentError when not passed a step" do
ScratchPad.recorded.should == ["A", "C", "E"] -> { ("A"..).step { } }.should raise_error(ArgumentError)
end
ScratchPad.record [] it "yields String values adjusted by step" do
eval("('A'...)").step(2) { |x| break if x > "F"; ScratchPad << x } eval("('A'..)").step("A") { |x| break if x > "AAA"; ScratchPad << x }
ScratchPad.recorded.should == ["A", "C", "E"] ScratchPad.recorded.should == ["A", "AA", "AAA"]
end
it "raises a TypeError when passed a Float step" do ScratchPad.record []
-> { eval("('A'..)").step(2.0) { } }.should raise_error(TypeError) eval("('A'...)").step("A") { |x| break if x > "AAA"; ScratchPad << x }
-> { eval("('A'...)").step(2.0) { } }.should raise_error(TypeError) ScratchPad.recorded.should == ["A", "AA", "AAA"]
end
it "raises a TypeError when passed an incompatible type step" do
-> { eval("('A'..)").step(2) { } }.should raise_error(TypeError)
-> { eval("('A'...)").step(2) { } }.should raise_error(TypeError)
end
end end
end end
end end
@ -383,15 +536,24 @@ describe "Range#step" do
describe "returned Enumerator" do describe "returned Enumerator" do
describe "size" do describe "size" do
it "raises a TypeError if step does not respond to #to_int" do ruby_version_is ""..."3.4" do
obj = mock("Range#step non-integer") it "raises a TypeError if step does not respond to #to_int" do
-> { (1..2).step(obj) }.should raise_error(TypeError) obj = mock("Range#step non-integer")
-> { (1..2).step(obj) }.should raise_error(TypeError)
end
it "raises a TypeError if #to_int does not return an Integer" do
obj = mock("Range#step non-integer")
obj.should_receive(:to_int).and_return("1")
-> { (1..2).step(obj) }.should raise_error(TypeError)
end
end end
it "raises a TypeError if #to_int does not return an Integer" do ruby_version_is "3.4" do
obj = mock("Range#step non-integer") it "does not raise if step is incompatible" do
obj.should_receive(:to_int).and_return("1") obj = mock("Range#step non-integer")
-> { (1..2).step(obj) }.should raise_error(TypeError) -> { (1..2).step(obj) }.should_not raise_error
end
end end
it "returns the ceil of range size divided by the number of steps" do it "returns the ceil of range size divided by the number of steps" do
@ -431,19 +593,36 @@ describe "Range#step" do
(1.0...6.4).step(1.8).size.should == 3 (1.0...6.4).step(1.8).size.should == 3
end end
it "returns nil with begin and end are String" do ruby_version_is ""..."3.4" do
("A".."E").step(2).size.should == nil it "returns nil with begin and end are String" do
("A"..."E").step(2).size.should == nil ("A".."E").step(2).size.should == nil
("A".."E").step.size.should == nil ("A"..."E").step(2).size.should == nil
("A"..."E").step.size.should == nil ("A".."E").step.size.should == nil
("A"..."E").step.size.should == nil
end
it "return nil and not raises a TypeError if the first element does not respond to #succ" do
obj = mock("Range#step non-comparable")
obj.should_receive(:<=>).with(obj).and_return(1)
enum = (obj..obj).step
-> { enum.size }.should_not raise_error
enum.size.should == nil
end
end end
it "return nil and not raises a TypeError if the first element does not respond to #succ" do ruby_version_is "3.4" do
obj = mock("Range#step non-comparable") it "returns nil with begin and end are String" do
obj.should_receive(:<=>).with(obj).and_return(1) ("A".."E").step("A").size.should == nil
enum = (obj..obj).step ("A"..."E").step("A").size.should == nil
-> { enum.size }.should_not raise_error end
enum.size.should == nil
it "return nil and not raises a TypeError if the first element is not of compatible type" do
obj = mock("Range#step non-comparable")
obj.should_receive(:<=>).with(obj).and_return(1)
enum = (obj..obj).step(obj)
-> { enum.size }.should_not raise_error
enum.size.should == nil
end
end end
end end
@ -470,22 +649,48 @@ describe "Range#step" do
(1..).step(2).take(3).should == [1, 3, 5] (1..).step(2).take(3).should == [1, 3, 5]
end end
it "returns an instance of Enumerator when begin is not numeric" do ruby_version_is ""..."3.4" do
("a"..).step.class.should == Enumerator it "returns an instance of Enumerator when begin is not numeric" do
("a"..).step(2).take(3).should == %w[a c e] ("a"..).step.class.should == Enumerator
("a"..).step(2).take(3).should == %w[a c e]
end
end
ruby_version_is "3.4" do
it "returns an instance of Enumerator when begin is not numeric" do
("a"..).step("a").class.should == Enumerator
("a"..).step("a").take(3).should == %w[a aa aaa]
end
end end
end end
context "when range is beginless and endless" do context "when range is beginless and endless" do
it "returns an instance of Enumerator" do ruby_version_is ""..."3.4" do
Range.new(nil, nil).step.class.should == Enumerator it "returns an instance of Enumerator" do
Range.new(nil, nil).step.class.should == Enumerator
end
end
ruby_version_is "3.4" do
it "raises an ArgumentError" do
-> { Range.new(nil, nil).step(1) }.should raise_error(ArgumentError)
end
end end
end end
context "when begin and end are not numerics" do context "when begin and end are not numerics" do
it "returns an instance of Enumerator" do ruby_version_is ""..."3.4" do
("a".."z").step.class.should == Enumerator it "returns an instance of Enumerator" do
("a".."z").step(3).take(4).should == %w[a d g j] ("a".."z").step.class.should == Enumerator
("a".."z").step(3).take(4).should == %w[a d g j]
end
end
ruby_version_is "3.4" do
it "returns an instance of Enumerator" do
("a".."z").step("a").class.should == Enumerator
("a".."z").step("a").take(4).should == %w[a aa aaa aaaa]
end
end end
end end
end end

View File

@ -256,7 +256,7 @@ class TestDir < Test::Unit::TestCase
Dir.glob(@root, sort: nil) Dir.glob(@root, sort: nil)
end end
assert_equal(("a".."z").step(2).map {|f| File.join(File.join(@root, f), "") }, assert_equal(("a".."z").each_slice(2).map {|f,_| File.join(File.join(@root, f), "") },
Dir.glob(File.join(@root, "*/"))) Dir.glob(File.join(@root, "*/")))
assert_equal([File.join(@root, '//a')], Dir.glob(@root + '//a')) assert_equal([File.join(@root, '//a')], Dir.glob(@root + '//a'))

View File

@ -246,67 +246,138 @@ class TestRange < Test::Unit::TestCase
assert_kind_of(String, (0..1).hash.to_s) assert_kind_of(String, (0..1).hash.to_s)
end end
def test_step def test_step_numeric_range
a = [] # Fixnums, floats and all other numbers (like rationals) should behave exactly the same,
(0..10).step {|x| a << x } # but the behavior is implemented independently in 3 different branches of code,
assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], a) # so we need to test each of them.
%i[to_i to_r to_f].each do |type|
conv = type.to_proc
a = [] from = conv.(0)
(0..).step {|x| a << x; break if a.size == 10 } to = conv.(10)
assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], a) step = conv.(2)
a = [] # finite
(0..10).step(2) {|x| a << x } a = []
assert_equal([0, 2, 4, 6, 8, 10], a) (from..to).step(step) {|x| a << x }
assert_equal([0, 2, 4, 6, 8, 10].map(&conv), a)
a = [] a = []
(0..).step(2) {|x| a << x; break if a.size == 10 } (from...to).step(step) {|x| a << x }
assert_equal([0, 2, 4, 6, 8, 10, 12, 14, 16, 18], a) assert_equal([0, 2, 4, 6, 8].map(&conv), a)
assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step) # Note: ArithmeticSequence behavior tested in its own test, but we also put it here
assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step(2)) # to demonstrate the result is the same
assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step(0.5)) assert_kind_of(Enumerator::ArithmeticSequence, (from..to).step(step))
assert_kind_of(Enumerator::ArithmeticSequence, (10..0).step(-1)) assert_equal([0, 2, 4, 6, 8, 10].map(&conv), (from..to).step(step).to_a)
assert_kind_of(Enumerator::ArithmeticSequence, (..10).step(2)) assert_kind_of(Enumerator::ArithmeticSequence, (from...to).step(step))
assert_kind_of(Enumerator::ArithmeticSequence, (1..).step(2)) assert_equal([0, 2, 4, 6, 8].map(&conv), (from...to).step(step).to_a)
assert_raise(ArgumentError) { (0..10).step(-1) { } } # endless
assert_raise(ArgumentError) { (0..10).step(0) } a = []
assert_raise(ArgumentError) { (0..10).step(0) { } } (from..).step(step) {|x| a << x; break if a.size == 5 }
assert_raise(ArgumentError) { (0..).step(-1) { } } assert_equal([0, 2, 4, 6, 8].map(&conv), a)
assert_raise(ArgumentError) { (0..).step(0) }
assert_raise(ArgumentError) { (0..).step(0) { } }
a = [] assert_kind_of(Enumerator::ArithmeticSequence, (from..).step(step))
("a" .. "z").step(2) {|x| a << x } assert_equal([0, 2, 4, 6, 8].map(&conv), (from..).step(step).take(5))
assert_equal(%w(a c e g i k m o q s u w y), a)
a = [] # beginless
("a" .. ).step(2) {|x| a << x; break if a.size == 13 } assert_raise(ArgumentError) { (..to).step(step) {} }
assert_equal(%w(a c e g i k m o q s u w y), a) assert_kind_of(Enumerator::ArithmeticSequence, (..to).step(step))
# This is inconsistent, but so it is implemented by ArithmeticSequence
assert_raise(TypeError) { (..to).step(step).to_a }
a = [] # negative step
("a" .. "z").step(2**32) {|x| a << x }
assert_equal(["a"], a)
a = [] a = []
(:a .. :z).step(2) {|x| a << x } (from..to).step(-step) {|x| a << x }
assert_equal(%i(a c e g i k m o q s u w y), a) assert_equal([], a)
a = [] a = []
(:a .. ).step(2) {|x| a << x; break if a.size == 13 } (from..-to).step(-step) {|x| a << x }
assert_equal(%i(a c e g i k m o q s u w y), a) assert_equal([0, -2, -4, -6, -8, -10].map(&conv), a)
a = [] a = []
(:a .. :z).step(2**32) {|x| a << x } (from...-to).step(-step) {|x| a << x }
assert_equal([:a], a) assert_equal([0, -2, -4, -6, -8].map(&conv), a)
a = []
(from...).step(-step) {|x| a << x; break if a.size == 5 }
assert_equal([0, -2, -4, -6, -8].map(&conv), a)
assert_kind_of(Enumerator::ArithmeticSequence, (from..to).step(-step))
assert_equal([], (from..to).step(-step).to_a)
assert_kind_of(Enumerator::ArithmeticSequence, (from..-to).step(-step))
assert_equal([0, -2, -4, -6, -8, -10].map(&conv), (from..-to).step(-step).to_a)
assert_kind_of(Enumerator::ArithmeticSequence, (from...-to).step(-step))
assert_equal([0, -2, -4, -6, -8].map(&conv), (from...-to).step(-step).to_a)
assert_kind_of(Enumerator::ArithmeticSequence, (from...).step(-step))
assert_equal([0, -2, -4, -6, -8].map(&conv), (from...).step(-step).take(5))
# zero step
assert_raise(ArgumentError) { (from..to).step(0) {} }
assert_raise(ArgumentError) { (from..to).step(0) }
# default step
a = []
(from..to).step {|x| a << x }
assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(&conv), a)
assert_kind_of(Enumerator::ArithmeticSequence, (from..to).step)
assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(&conv), (from..to).step.to_a)
# default + endless range
a = []
(from..).step {|x| a << x; break if a.size == 5 }
assert_equal([0, 1, 2, 3, 4].map(&conv), a)
assert_kind_of(Enumerator::ArithmeticSequence, (from..).step)
assert_equal([0, 1, 2, 3, 4].map(&conv), (from..).step.take(5))
# default + beginless range
assert_kind_of(Enumerator::ArithmeticSequence, (..to).step)
# step is not numeric
to = conv.(5)
val = Struct.new(:val)
a = []
assert_raise(TypeError) { (from..to).step(val.new(step)) {|x| a << x } }
assert_kind_of(Enumerator, (from..to).step(val.new(step)))
assert_raise(TypeError) { (from..to).step(val.new(step)).to_a }
# step is not numeric, but coercible
val = Struct.new(:val) do
def coerce(num) = [self.class.new(num), self]
def +(other) = self.class.new(val + other.val)
def <=>(other) = other.is_a?(self.class) ? val <=> other.val : val <=> other
end
a = []
(from..to).step(val.new(step)) {|x| a << x }
assert_equal([from, val.new(conv.(2)), val.new(conv.(4))], a)
assert_kind_of(Enumerator, (from..to).step(val.new(step)))
assert_equal([from, val.new(conv.(2)), val.new(conv.(4))], (from..to).step(val.new(step)).to_a)
end
end
def test_step_numeric_fixnum_boundary
a = [] a = []
(2**32-1 .. 2**32+1).step(2) {|x| a << x } (2**32-1 .. 2**32+1).step(2) {|x| a << x }
assert_equal([4294967295, 4294967297], a) assert_equal([4294967295, 4294967297], a)
zero = (2**32).coerce(0).first zero = (2**32).coerce(0).first
assert_raise(ArgumentError) { (2**32-1 .. 2**32+1).step(zero) } assert_raise(ArgumentError) { (2**32-1 .. 2**32+1).step(zero) }
assert_raise(ArgumentError) { (2**32-1 .. 2**32+1).step(zero) { } } assert_raise(ArgumentError) { (2**32-1 .. 2**32+1).step(zero) { } }
a = [] a = []
(2**32-1 .. ).step(2) {|x| a << x; break if a.size == 2 } (2**32-1 .. ).step(2) {|x| a << x; break if a.size == 2 }
assert_equal([4294967295, 4294967297], a) assert_equal([4294967295, 4294967297], a)
@ -315,58 +386,85 @@ class TestRange < Test::Unit::TestCase
a = [] a = []
(max..).step {|x| a << x; break if a.size == 2 } (max..).step {|x| a << x; break if a.size == 2 }
assert_equal([max, max+1], a) assert_equal([max, max+1], a)
a = [] a = []
(max..).step(max) {|x| a << x; break if a.size == 4 } (max..).step(max) {|x| a << x; break if a.size == 4 }
assert_equal([max, 2*max, 3*max, 4*max], a) assert_equal([max, 2*max, 3*max, 4*max], a)
end
o1 = Object.new def test_step_big_float
o2 = Object.new
def o1.<=>(x); -1; end
def o2.<=>(x); 0; end
assert_raise(TypeError) { (o1..o2).step(1) { } }
assert_raise(TypeError) { (o1..).step(1) { } }
class << o1; self; end.class_eval do
define_method(:succ) { o2 }
end
a = []
(o1..o2).step(1) {|x| a << x }
assert_equal([o1, o2], a)
a = []
(o1...o2).step(1) {|x| a << x }
assert_equal([o1], a)
assert_nothing_raised("[ruby-dev:34557]") { (0..2).step(0.5) {|x| } }
a = []
(0..2).step(0.5) {|x| a << x }
assert_equal([0, 0.5, 1.0, 1.5, 2.0], a)
a = []
(0..).step(0.5) {|x| a << x; break if a.size == 5 }
assert_equal([0, 0.5, 1.0, 1.5, 2.0], a)
a = [] a = []
(0x40000000..0x40000002).step(0.5) {|x| a << x } (0x40000000..0x40000002).step(0.5) {|x| a << x }
assert_equal([1073741824, 1073741824.5, 1073741825.0, 1073741825.5, 1073741826], a) assert_equal([1073741824, 1073741824.5, 1073741825.0, 1073741825.5, 1073741826], a)
end
o = Object.new def test_step_non_numeric_range
def o.to_int() 1 end # finite
assert_nothing_raised("[ruby-dev:34558]") { (0..2).step(o) {|x| } } a = []
('a'..'aaaa').step('a') { a << _1 }
assert_equal(%w[a aa aaa aaaa], a)
o = Object.new assert_kind_of(Enumerator, ('a'..'aaaa').step('a'))
class << o assert_equal(%w[a aa aaa aaaa], ('a'..'aaaa').step('a').to_a)
def to_str() "a" end
def <=>(other) to_str <=> other end
end
a = [] a = []
(o.."c").step(1) {|x| a << x} ('a'...'aaaa').step('a') { a << _1 }
assert_equal(["a", "b", "c"], a) assert_equal(%w[a aa aaa], a)
assert_kind_of(Enumerator, ('a'...'aaaa').step('a'))
assert_equal(%w[a aa aaa], ('a'...'aaaa').step('a').to_a)
# endless
a = [] a = []
(o..).step(1) {|x| a << x; break if a.size >= 3} ('a'...).step('a') { a << _1; break if a.size == 3 }
assert_equal(["a", "b", "c"], a) assert_equal(%w[a aa aaa], a)
assert_kind_of(Enumerator, ('a'...).step('a'))
assert_equal(%w[a aa aaa], ('a'...).step('a').take(3))
# beginless
assert_raise(ArgumentError) { (...'aaa').step('a') {} }
assert_raise(ArgumentError) { (...'aaa').step('a') }
# step is not provided
assert_raise(ArgumentError) { ('a'...'aaaa').step }
# step is incompatible
assert_raise(TypeError) { ('a'...'aaaa').step(1) {} }
assert_raise(TypeError) { ('a'...'aaaa').step(1).to_a }
# step is compatible, but shouldn't convert into numeric domain:
a = []
(Time.utc(2022, 2, 24)...).step(1) { a << _1; break if a.size == 2 }
assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 24, 0, 0, 1)], a)
a = []
(Time.utc(2022, 2, 24)...).step(1.0) { a << _1; break if a.size == 2 }
assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 24, 0, 0, 1)], a)
a = []
(Time.utc(2022, 2, 24)...).step(1r) { a << _1; break if a.size == 2 }
assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 24, 0, 0, 1)], a)
# step decreases the value
a = []
(Time.utc(2022, 2, 24)...).step(-1) { a << _1; break if a.size == 2 }
assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 23, 23, 59, 59)], a)
a = []
(Time.utc(2022, 2, 24)...Time.utc(2022, 2, 23, 23, 59, 57)).step(-1) { a << _1 }
assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 23, 23, 59, 59),
Time.utc(2022, 2, 23, 23, 59, 58)], a)
a = []
(Time.utc(2022, 2, 24)..Time.utc(2022, 2, 23, 23, 59, 57)).step(-1) { a << _1 }
assert_equal([Time.utc(2022, 2, 24), Time.utc(2022, 2, 23, 23, 59, 59),
Time.utc(2022, 2, 23, 23, 59, 58), Time.utc(2022, 2, 23, 23, 59, 57)], a)
# step decreases, but the range is forward-directed:
a = []
(Time.utc(2022, 2, 24)...Time.utc(2022, 2, 24, 01, 01, 03)).step(-1) { a << _1 }
assert_equal([], a)
end end
def test_step_bug15537 def test_step_bug15537
@ -392,26 +490,6 @@ class TestRange < Test::Unit::TestCase
assert_equal(4, (1.0...5.6).step(1.5).to_a.size) assert_equal(4, (1.0...5.6).step(1.5).to_a.size)
end end
def test_step_with_succ
c = Struct.new(:i) do
def succ; self.class.new(i+1); end
def <=>(other) i <=> other.i;end
end.new(0)
result = []
(c..c.succ).step(2) do |d|
result << d.i
end
assert_equal([0], result)
result = []
(c..).step(2) do |d|
result << d.i
break if d.i >= 4
end
assert_equal([0, 2, 4], result)
end
def test_each def test_each
a = [] a = []
(0..10).each {|x| a << x } (0..10).each {|x| a << x }

View File

@ -16,6 +16,7 @@
# #
test_replicate(EncodingTest) the method was removed in 3.3 test_replicate(EncodingTest) the method was removed in 3.3
test_step(RangeTest) the method protocol was changed in 3.4
test_collection_install(RBS::CliTest) running tests without Bundler test_collection_install(RBS::CliTest) running tests without Bundler
test_collection_install_frozen(RBS::CliTest) running tests without Bundler test_collection_install_frozen(RBS::CliTest) running tests without Bundler