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