Preview Ruby 4.1 is a preview. It can change before the final release. Switch to Ruby 4.0 →
Range#step
static VALUE
range_step(int argc, VALUE *argv, VALUE range)
{
VALUE b, e, v, step;
int c, dir;
b = RANGE_BEG(range);
e = RANGE_END(range);
v = b;
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);
// For backward compatibility reasons (conforming to behavior before 3.4), String/Symbol
// supports both old behavior ('a'..).step(1) and new behavior ('a'..).step('a')
// Hence the additional conversion/additional checks.
const VALUE str_b = rb_check_string_type(b);
const VALUE sym_b = SYMBOL_P(b) ? rb_sym2str(b) : Qnil;
if (rb_check_arity(argc, 0, 1))
step = argv[0];
else {
if (b_num_p || !NIL_P(str_b) || !NIL_P(sym_b) || (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()) {
// 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));
}
// ...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);
}
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));
i += unit; /* FIXABLE+FIXABLE never overflow */
} 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: summation is performed in C for performance */
long end = FIX2LONG(e);
long i, unit = FIX2LONG(step);
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 (b_num_p && step_num_p && ruby_float_step(b, e, step, EXCL(range), TRUE)) {
/* done */
}
else if (!NIL_P(str_b) && FIXNUM_P(step)) {
// backwards compatibility behavior for String only, when no step/Integer step is passed
// See discussion in https://bugs.ruby-lang.org/issues/18368
VALUE iter[2] = {INT2FIX(1), step};
if (NIL_P(e)) {
rb_str_upto_endless_each(str_b, step_i, (VALUE)iter);
}
else {
rb_str_upto_each(str_b, e, EXCL(range), step_i, (VALUE)iter);
}
}
else if (!NIL_P(sym_b) && FIXNUM_P(step)) {
// same as above: backward compatibility for symbols
VALUE iter[2] = {INT2FIX(1), step};
if (NIL_P(e)) {
rb_str_upto_endless_each(sym_b, sym_step_i, (VALUE)iter);
}
else {
rb_str_upto_each(sym_b, rb_sym2str(e), EXCL(range), sym_step_i, (VALUE)iter);
}
}
else if (NIL_P(e)) {
// endless range
for (;; v = rb_funcall(v, id_plus, 1, step))
rb_yield(v);
}
else 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;
}
}
}
else if ((dir = r_less(b, e)) == 0) {
if (!EXCL(range)) {
rb_yield(v);
}
}
else if (dir == r_less(b, rb_funcall(b, id_plus, 1, step))) {
// 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)
// 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 (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;
}
}
}
return range;
}
Iterates over the elements of range in steps of s. The iteration is performed by + operator:
(0..6).step(2) { puts _1 } # Prints: 0, 2, 4, 6 # 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
If + step decreases the value, iteration is still performed when step begin is higher than the end:
(0..6).step(-2) { puts _1 } # Prints nothing (6..0).step(-2) { puts _1 } # Prints: 6, 4, 2, 0 (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') #=> #<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:
(Time.utc(2022, 3, 1)..Time.utc(2022, 2, 24)).step { p _1 } # raises: step is required for non-numeric ranges (ArgumentError)
For backward compatibility reasons, String ranges support the iteration both with string step and with integer step. In the latter case, the iteration is performed by calculating the next values with String#succ:
('a'..'e').step(2) { p _1 } # Prints: a, c, e ('a'..'e').step { p _1 } # Default step 1; prints: a, b, c, d, e
Ruby version changes
-
3.4
-
Range#stepnow consistently has a semantics of iterating by using+operator for all types, not only numerics. [Feature #18368]Example(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]
-