diff --git a/NEWS.md b/NEWS.md
index 230e934ba8..c1cc39327f 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -43,6 +43,13 @@ Note: We're only listing outstanding class updates.
* Range
* 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
@@ -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 #20497]: https://bugs.ruby-lang.org/issues/20497
[Feature #20624]: https://bugs.ruby-lang.org/issues/20624
+[Feature #18368]: https://bugs.ruby-lang.org/issues/18368
diff --git a/range.c b/range.c
index cafe57ab77..3383fdbfae 100644
--- a/range.c
+++ b/range.c
@@ -29,7 +29,7 @@
#include "internal/range.h"
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_succ idSucc
#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
discrete_object_p(VALUE obj)
{
@@ -400,72 +366,123 @@ range_step_size(VALUE range, VALUE args, VALUE eobj)
/*
* call-seq:
- * step(n = 1) {|element| ... } -> self
- * step(n = 1) -> enumerator
+ * step(s = 1) {|element| ... } -> self
+ * 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 + operator:
*
- * With a block given and no argument,
- * calls the block each element of the range; returns +self+:
+ * (0..6).step(2) { puts _1 } #=> 1..5
+ * # Prints: 0, 2, 4, 6
*
- * a = []
- * (1..5).step {|element| a.push(element) } # => 1..5
- * a # => [1, 2, 3, 4, 5]
- * a = []
- * ('a'..'e').step {|element| a.push(element) } # => "a".."e"
- * a # => ["a", "b", "c", "d", "e"]
+ * # Iterate between two dates in step of 1 day (24 hours)
+ * (Time.utc(2022, 2, 24)..Time.utc(2022, 3, 1)).step(24*60*60) { puts _1 }
+ * # Prints:
+ * # 2022-02-24 00:00:00 UTC
+ * # 2022-02-25 00:00:00 UTC
+ * # 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,
- * calls the block with element +0+, element +n+, element 2n, and so on:
+ * If + step decreases the value, iteration is still performed when
+ * step +begin+ is higher than the +end+:
*
- * a = []
- * (1..5).step(2) {|element| a.push(element) } # => 1..5
- * a # => [1, 3, 5]
- * a = []
- * ('a'..'e').step(2) {|element| a.push(element) } # => "a".."e"
- * a # => ["a", "c", "e"]
+ * (0..6).step(-2) { puts _1 }
+ * # Prints nothing
*
- * With no block given, returns an enumerator,
- * which will be of class Enumerator::ArithmeticSequence if +self+ is numeric;
- * otherwise of class Enumerator:
+ * (6..0).step(-2) { puts _1 }
+ * # Prints: 6, 4, 2, 0
*
- * e = (1..5).step(2) # => ((1..5).step(2))
- * e.class # => Enumerator::ArithmeticSequence
- * ('a'..'e').step # => #
+ * (Time.utc(2022, 3, 1)..Time.utc(2022, 2, 24)).step(-24*60*60) { puts _1 }
+ * # Prints:
+ * # 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') #=> #
+ * ('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
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);
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_obj_is_kind_of(step, rb_cNumeric)) {
- step = rb_to_int(step);
- }
- if (rb_equal(step, INT2FIX(0))) {
- 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)) {
+ // This code is allowed to create even beginless ArithmeticSequence, which can be useful,
+ // e.g., for array slicing:
+ // ary[(..-1) % 3]
+ if (step_num_p && ((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,
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);
- VALUE iter[2] = {INT2FIX(1), step};
+ if (NIL_P(b)) {
+ rb_raise(rb_eArgError, "#step iteration for beginless ranges is meaningless");
+ }
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);
do {
rb_yield(LONG2FIX(i));
@@ -473,71 +490,77 @@ range_step(int argc, VALUE *argv, VALUE range)
} while (FIXABLE(i));
b = LONG2NUM(i);
+ /* then switch to Bignum API */
for (;; b = rb_big_plus(b, step))
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 i, unit = FIX2LONG(step);
- if (!EXCL(range))
- end += 1;
- i = FIX2LONG(b);
- while (i < end) {
- rb_yield(LONG2NUM(i));
- if (i + unit < i) break;
- i += unit;
- }
-
- }
- else if (SYMBOL_P(b) && (NIL_P(e) || SYMBOL_P(e))) { /* symbols are special */
- b = rb_sym2str(b);
- if (NIL_P(e)) {
- rb_str_upto_endless_each(b, sym_step_i, (VALUE)iter);
- }
- else {
- rb_str_upto_each(b, rb_sym2str(e), EXCL(range), sym_step_i, (VALUE)iter);
+ if (unit < 0) {
+ if (!EXCL(range))
+ end -= 1;
+ i = FIX2LONG(b);
+ while (i > end) {
+ rb_yield(LONG2NUM(i));
+ i += unit;
+ }
+ } else {
+ if (!EXCL(range))
+ end += 1;
+ i = FIX2LONG(b);
+ while (i < end) {
+ rb_yield(LONG2NUM(i));
+ i += unit;
+ }
}
}
- 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 */
}
- 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 {
- 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)) {
- b = tmp;
- if (NIL_P(e)) {
- rb_str_upto_endless_each(b, step_i, (VALUE)iter);
- }
- else {
- rb_str_upto_each(b, e, EXCL(range), step_i, (VALUE)iter);
+ } else {
+ // Direction of the comparison. We use it as a comparison operator in cycle:
+ // if begin < end, the cycle performs while value < end (iterating forward)
+ // if begin > end, the cycle performs while value > end (iterating backward with
+ // a negative step)
+ dir = r_less(b, e);
+ // 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 {
- if (!discrete_object_p(b)) {
- rb_raise(rb_eTypeError, "can't iterate from %s",
- 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);
- }
+ else
+ for (;; v = rb_funcall(v, id_plus, 1, step))
+ rb_yield(v);
}
return range;
}
@@ -545,29 +568,24 @@ range_step(int argc, VALUE *argv, VALUE range)
/*
* call-seq:
* %(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;
- * returns +self+:
+ * array = [0, 1, 2, 3, 4, 5, 6]
*
- * a = []
- * (1..5).%(2) {|element| a.push(element) } # => 1..5
- * a # => [1, 3, 5]
- * a = []
- * ('a'..'e').%(2) {|element| a.push(element) } # => "a".."e"
- * a # => ["a", "c", "e"]
+ * # slice each second element:
+ * seq = (0..) % 2 #=> ((0..).%(2))
+ * array[seq] #=> [0, 2, 4, 6]
+ * # or just
+ * array[(0..) % 2] #=> [0, 2, 4, 6]
*
- * With no block given, returns an enumerator,
- * which will be of class Enumerator::ArithmeticSequence if +self+ is numeric;
- * otherwise of class Enumerator:
+ * Note that due to operator precedence in Ruby, parentheses are mandatory around range
+ * in this case:
*
- * e = (1..5) % 2 # => ((1..5).%(2))
- * e.class # => Enumerator::ArithmeticSequence
- * ('a'..'e') % 2 # => #
- *
- * Related: Range#step.
+ * (0..7) % 2 #=> ((0..7).%(2)) -- as expected
+ * 0..7 % 2 #=> 0..1 -- parsed as 0..(7 % 2)
*/
static VALUE
range_percent_step(VALUE range, VALUE step)
@@ -2641,6 +2659,7 @@ Init_Range(void)
id_beg = rb_intern_const("begin");
id_end = rb_intern_const("end");
id_excl = rb_intern_const("excl");
+ id_plus = rb_intern_const("+");
rb_cRange = rb_struct_define_without_accessor(
"Range", rb_cObject, range_alloc,
diff --git a/spec/ruby/core/range/step_spec.rb b/spec/ruby/core/range/step_spec.rb
index 64ea3de4ed..f858a6a6bb 100644
--- a/spec/ruby/core/range/step_spec.rb
+++ b/spec/ruby/core/range/step_spec.rb
@@ -10,44 +10,50 @@ describe "Range#step" do
r.step { }.should equal(r)
end
- it "raises TypeError if step" do
- obj = mock("mock")
- -> { (1..10).step(obj) { } }.should raise_error(TypeError)
+ ruby_version_is ""..."3.4" do
+ it "calls #to_int to coerce step to an Integer" do
+ 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
- it "calls #to_int to coerce step to an Integer" do
- obj = mock("Range#step")
- obj.should_receive(:to_int).and_return(1)
+ ruby_version_is "3.4" do
+ it "calls #coerce to coerce step to an Integer" do
+ obj = mock("Range#step")
+ obj.should_receive(:coerce).at_least(:once).and_return([1, 2])
- (1..2).step(obj) { |x| ScratchPad << x }
- ScratchPad.recorded.should eql([1, 2])
- end
+ (1..3).step(obj) { |x| ScratchPad << x }
+ ScratchPad.recorded.should eql([1, 3])
+ end
- it "raises a TypeError if step does not respond to #to_int" do
- obj = mock("Range#step non-integer")
+ it "raises a TypeError if step does not respond to #coerce" do
+ obj = mock("Range#step non-coercible")
- -> { (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 "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)
+ -> { (1..2).step(obj) { } }.should raise_error(TypeError)
+ end
end
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)
end
- it "raises an ArgumentError if step is negative" do
- -> { (-1..1).step(-2) { |x| x } }.should raise_error(ArgumentError)
+ ruby_version_is "3.4" do
+ 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
describe "with inclusive end" do
@@ -78,6 +93,18 @@ describe "Range#step" do
(-2..2).step(1.5) { |x| ScratchPad << x }
ScratchPad.recorded.should eql([-2.0, -0.5, 1.0])
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
describe "and Float values" do
@@ -148,27 +175,114 @@ describe "Range#step" do
end
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
- ("A".."E").step { |x| ScratchPad << x }
- ScratchPad.recorded.should == ["A", "B", "C", "D", "E"]
+ ruby_version_is ""..."3.4" do
+ it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
+ ("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
- 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
+ ruby_version_is "3.4" do
+ it "raises an ArgumentError when not passed a step" do
+ -> { ("A".."E").step { } }.should raise_error(ArgumentError)
+ end
- it "raises a TypeError when passed a Float step" do
- -> { ("A".."G").step(2.0) { } }.should raise_error(TypeError)
- end
+ it "yields String values adjusted by step and less than or equal to end" do
+ ("A".."AAA").step("A") { |x| ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "AA", "AAA"]
+ 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)
+ it "raises a TypeError when passed an incompatible type step" do
+ -> { ("A".."G").step(2) { } }.should raise_error(TypeError)
+ end
- (obj..obj).step { |x| ScratchPad << x }
- ScratchPad.recorded.should == [obj, obj, obj]
+ it "calls #+ on begin and each element returned by #+" 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 "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
@@ -266,18 +380,35 @@ describe "Range#step" do
end
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
- ("A"..."E").step { |x| ScratchPad << x }
- ScratchPad.recorded.should == ["A", "B", "C", "D"]
+ ruby_version_is ""..."3.4" do
+ it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
+ ("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
- 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
+ ruby_version_is "3.4" do
+ it "raises an ArgumentError when not passed a step" do
+ -> { ("A".."E").step { } }.should raise_error(ArgumentError)
+ end
- it "raises a TypeError when passed a Float step" do
- -> { ("A"..."G").step(2.0) { } }.should raise_error(TypeError)
+ it "yields String values adjusted by step and less than or equal to end" do
+ ("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
@@ -351,27 +482,49 @@ describe "Range#step" do
end
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
- eval("('A'..)").step { |x| break if x > "D"; ScratchPad << x }
- ScratchPad.recorded.should == ["A", "B", "C", "D"]
+ ruby_version_is ""..."3.4" do
+ it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
+ eval("('A'..)").step { |x| break if x > "D"; ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "B", "C", "D"]
- ScratchPad.record []
- eval("('A'...)").step { |x| break if x > "D"; ScratchPad << x }
- ScratchPad.recorded.should == ["A", "B", "C", "D"]
+ ScratchPad.record []
+ eval("('A'...)").step { |x| break if x > "D"; ScratchPad << x }
+ 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
- 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"]
+ ruby_version_is "3.4" do
+ it "raises an ArgumentError when not passed a step" do
+ -> { ("A"..).step { } }.should raise_error(ArgumentError)
+ end
- ScratchPad.record []
- eval("('A'...)").step(2) { |x| break if x > "F"; ScratchPad << x }
- ScratchPad.recorded.should == ["A", "C", "E"]
- end
+ it "yields String values adjusted by step" do
+ eval("('A'..)").step("A") { |x| break if x > "AAA"; ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "AA", "AAA"]
- 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)
+ ScratchPad.record []
+ eval("('A'...)").step("A") { |x| break if x > "AAA"; ScratchPad << x }
+ 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
@@ -383,15 +536,24 @@ describe "Range#step" do
describe "returned Enumerator" do
describe "size" do
- 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)
+ ruby_version_is ""..."3.4" do
+ 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
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)
+ ruby_version_is "3.4" do
+ it "does not raise if step is incompatible" do
+ obj = mock("Range#step non-integer")
+ -> { (1..2).step(obj) }.should_not raise_error
+ end
end
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
end
- 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.size.should == nil
+ ruby_version_is ""..."3.4" do
+ 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.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
- 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
+ ruby_version_is "3.4" do
+ it "returns nil with begin and end are String" do
+ ("A".."E").step("A").size.should == nil
+ ("A"..."E").step("A").size.should == nil
+ end
+
+ 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
@@ -470,22 +649,48 @@ describe "Range#step" do
(1..).step(2).take(3).should == [1, 3, 5]
end
- it "returns an instance of Enumerator when begin is not numeric" do
- ("a"..).step.class.should == Enumerator
- ("a"..).step(2).take(3).should == %w[a c e]
+ ruby_version_is ""..."3.4" do
+ it "returns an instance of Enumerator when begin is not numeric" do
+ ("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
context "when range is beginless and endless" do
- it "returns an instance of Enumerator" do
- Range.new(nil, nil).step.class.should == Enumerator
+ ruby_version_is ""..."3.4" do
+ 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
context "when begin and end are not numerics" do
- it "returns an instance of Enumerator" do
- ("a".."z").step.class.should == Enumerator
- ("a".."z").step(3).take(4).should == %w[a d g j]
+ ruby_version_is ""..."3.4" do
+ it "returns an instance of Enumerator" do
+ ("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
diff --git a/test/ruby/test_dir.rb b/test/ruby/test_dir.rb
index 2cc1c3ef4a..b86cf330bc 100644
--- a/test/ruby/test_dir.rb
+++ b/test/ruby/test_dir.rb
@@ -256,7 +256,7 @@ class TestDir < Test::Unit::TestCase
Dir.glob(@root, sort: nil)
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, "*/")))
assert_equal([File.join(@root, '//a')], Dir.glob(@root + '//a'))
diff --git a/test/ruby/test_range.rb b/test/ruby/test_range.rb
index 84b3b205f0..b7541424d1 100644
--- a/test/ruby/test_range.rb
+++ b/test/ruby/test_range.rb
@@ -246,67 +246,138 @@ class TestRange < Test::Unit::TestCase
assert_kind_of(String, (0..1).hash.to_s)
end
- def test_step
- a = []
- (0..10).step {|x| a << x }
- assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], a)
+ def test_step_numeric_range
+ # Fixnums, floats and all other numbers (like rationals) should behave exactly the same,
+ # but the behavior is implemented independently in 3 different branches of code,
+ # so we need to test each of them.
+ %i[to_i to_r to_f].each do |type|
+ conv = type.to_proc
- a = []
- (0..).step {|x| a << x; break if a.size == 10 }
- assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], a)
+ from = conv.(0)
+ to = conv.(10)
+ step = conv.(2)
- a = []
- (0..10).step(2) {|x| a << x }
- assert_equal([0, 2, 4, 6, 8, 10], a)
+ # finite
+ a = []
+ (from..to).step(step) {|x| a << x }
+ assert_equal([0, 2, 4, 6, 8, 10].map(&conv), a)
- a = []
- (0..).step(2) {|x| a << x; break if a.size == 10 }
- assert_equal([0, 2, 4, 6, 8, 10, 12, 14, 16, 18], a)
+ a = []
+ (from...to).step(step) {|x| a << x }
+ assert_equal([0, 2, 4, 6, 8].map(&conv), a)
- assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step)
- assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step(2))
- assert_kind_of(Enumerator::ArithmeticSequence, (0..10).step(0.5))
- assert_kind_of(Enumerator::ArithmeticSequence, (10..0).step(-1))
- assert_kind_of(Enumerator::ArithmeticSequence, (..10).step(2))
- assert_kind_of(Enumerator::ArithmeticSequence, (1..).step(2))
+ # Note: ArithmeticSequence behavior tested in its own test, but we also put it here
+ # to demonstrate the result is the same
+ 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_raise(ArgumentError) { (0..10).step(-1) { } }
- assert_raise(ArgumentError) { (0..10).step(0) }
- assert_raise(ArgumentError) { (0..10).step(0) { } }
- assert_raise(ArgumentError) { (0..).step(-1) { } }
- assert_raise(ArgumentError) { (0..).step(0) }
- assert_raise(ArgumentError) { (0..).step(0) { } }
+ # endless
+ a = []
+ (from..).step(step) {|x| a << x; break if a.size == 5 }
+ assert_equal([0, 2, 4, 6, 8].map(&conv), a)
- a = []
- ("a" .. "z").step(2) {|x| a << x }
- assert_equal(%w(a c e g i k m o q s u w y), a)
+ assert_kind_of(Enumerator::ArithmeticSequence, (from..).step(step))
+ assert_equal([0, 2, 4, 6, 8].map(&conv), (from..).step(step).take(5))
- a = []
- ("a" .. ).step(2) {|x| a << x; break if a.size == 13 }
- assert_equal(%w(a c e g i k m o q s u w y), a)
+ # beginless
+ assert_raise(ArgumentError) { (..to).step(step) {} }
+ 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 = []
- ("a" .. "z").step(2**32) {|x| a << x }
- assert_equal(["a"], a)
+ # negative step
- a = []
- (:a .. :z).step(2) {|x| a << x }
- assert_equal(%i(a c e g i k m o q s u w y), a)
+ a = []
+ (from..to).step(-step) {|x| a << x }
+ assert_equal([], a)
- a = []
- (:a .. ).step(2) {|x| a << x; break if a.size == 13 }
- assert_equal(%i(a c e g i k m o q s u w y), a)
+ a = []
+ (from..-to).step(-step) {|x| a << x }
+ assert_equal([0, -2, -4, -6, -8, -10].map(&conv), a)
- a = []
- (:a .. :z).step(2**32) {|x| a << x }
- assert_equal([:a], a)
+ a = []
+ (from...-to).step(-step) {|x| a << x }
+ 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 = []
(2**32-1 .. 2**32+1).step(2) {|x| a << x }
assert_equal([4294967295, 4294967297], a)
+
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) { } }
+
a = []
(2**32-1 .. ).step(2) {|x| a << x; break if a.size == 2 }
assert_equal([4294967295, 4294967297], a)
@@ -315,58 +386,85 @@ class TestRange < Test::Unit::TestCase
a = []
(max..).step {|x| a << x; break if a.size == 2 }
assert_equal([max, max+1], a)
+
a = []
(max..).step(max) {|x| a << x; break if a.size == 4 }
assert_equal([max, 2*max, 3*max, 4*max], a)
+ end
- o1 = Object.new
- 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)
-
+ def test_step_big_float
a = []
(0x40000000..0x40000002).step(0.5) {|x| a << x }
assert_equal([1073741824, 1073741824.5, 1073741825.0, 1073741825.5, 1073741826], a)
+ end
- o = Object.new
- def o.to_int() 1 end
- assert_nothing_raised("[ruby-dev:34558]") { (0..2).step(o) {|x| } }
+ def test_step_non_numeric_range
+ # finite
+ a = []
+ ('a'..'aaaa').step('a') { a << _1 }
+ assert_equal(%w[a aa aaa aaaa], a)
- o = Object.new
- class << o
- def to_str() "a" end
- def <=>(other) to_str <=> other end
- end
+ assert_kind_of(Enumerator, ('a'..'aaaa').step('a'))
+ assert_equal(%w[a aa aaa aaaa], ('a'..'aaaa').step('a').to_a)
a = []
- (o.."c").step(1) {|x| a << x}
- assert_equal(["a", "b", "c"], a)
+ ('a'...'aaaa').step('a') { a << _1 }
+ 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 = []
- (o..).step(1) {|x| a << x; break if a.size >= 3}
- assert_equal(["a", "b", "c"], a)
+ ('a'...).step('a') { a << _1; break if a.size == 3 }
+ 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
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)
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
a = []
(0..10).each {|x| a << x }
diff --git a/tool/rbs_skip_tests b/tool/rbs_skip_tests
index 56a864d193..4551e276db 100644
--- a/tool/rbs_skip_tests
+++ b/tool/rbs_skip_tests
@@ -16,6 +16,7 @@
#
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_frozen(RBS::CliTest) running tests without Bundler