skip to content

In Ruby, what do (1..10).step(3) and (1..10) % 3 return, and what did Ruby 3.4 change about Range#step?

level: middleimportance: nice to knowfreq 22%

answer

  1. not an Array
  2. Enumerator::ArithmeticSequence
  3. % is step with no default
  4. 0..7 % 2 parses as 0..1
  5. 3.4: step uses + for all types

basics

~10 s

Without a block both return an Enumerator::ArithmeticSequence yielding 1, 4, 7, 10; % is step without a default argument. Since Ruby 3.4, Range#step advances by calling + for every type, not only numbers.

solid answer

~40 s

On a numeric range, `step(3)` without a block returns an `Enumerator::ArithmeticSequence` rather than an Array; it knows its `begin`, `end`, `step` and `size` and yields 1, 4, 7, 10. `%` is the same method without a default step, and it needs parentheses: `0..7 % 2` parses as `0..(7 % 2)`, which is `0..1`. `step(0)` raises `ArgumentError`. Since Ruby 3.4, `Range#step` computes each next value with `+` for every type, so an endless string range stepped by `'b'` yields `"a"`, `"ab"`, `"abb"`. For backward compatibility, String ranges with an Integer step still advance with `String#succ`: `('a'..'e').step(2)` yields a, c, e. Non-numeric ranges have no default step and raise `ArgumentError` without one.

code

ruby · 13 lines
ruby
seq = (1..10).step(3)
seq.class          # => Enumerator::ArithmeticSequence
seq.to_a           # => [1, 4, 7, 10]
seq.size           # => 4

starts = (540...1020) % 15    # 15-minute starts, 09:00 to 16:45
starts.size        # => 32
starts.first(3)    # => [540, 555, 570]

0..7 % 2           # => 0..1, parsed as 0..(7 % 2)
('a'..'e').step(2).to_a       # => ["a", "c", "e"]
('a'..).step('b').take(3)     # => ["a", "ab", "abb"]
(1..10).step(0)    # ArgumentError: step can't be 0

go deeper

for a junior

Recall that step walks a range by increments and that without a block it returns a sequence object, not an Array.

for a middle

Explain ArithmeticSequence, why % needs parentheses, the errors for a zero or missing step, and the String succ exception.

for a senior

Use arithmetic sequences to describe slot grids without allocating arrays, and check stepping code for the 3.4 semantics change.

for a principal

Judge whether stepping non-numeric domain types with + is clearer than an explicit generator in shared code.

## What step returns `Range#step(n)` walks a range in increments of `n`. Called **with a block**, it yields each value and returns the range itself. Called **without a block** on a numeric range, it returns an **`Enumerator::ArithmeticSequence`**, a lazy description of the sequence rather than an Array: - `(1..10).step(3)` describes 1, 4, 7, 10. - `.to_a` materialises it as `[1, 4, 7, 10]`. - `.size` is `4`, computed without iterating. - `.begin`, `.end`, `.step` and `.exclude_end?` read back its parameters, and `.last` is `10`. For a non-numeric range without a block, `step` returns a plain `Enumerator` instead. ## Range#% is step without a default `Range#%` behaves like `step` but always takes the step explicitly: `(1..10) % 3` is the same arithmetic sequence as `(1..10).step(3)`. It exists to read like mathematical notation, and it has one trap: 1. `(0..7) % 2` is the sequence 0, 2, 4, 6. 2. `0..7 % 2` without parentheses parses as `0..(7 % 2)`, because `%` binds tighter than `..`, and evaluates to the plain range `0..1`. ## Errors to expect | Call | Result | |---|---| | `(1..10).step(0)` | `ArgumentError`, `step can't be 0` | | a non-numeric range other than String or Symbol, stepped with no argument | `ArgumentError`, `step is required for non-numeric ranges` | | `(..10).step(2) { }` with a block | `ArgumentError`, `#step iteration for beginless ranges is meaningless` | | `(1..10).step(3)` with no block | `Enumerator::ArithmeticSequence` | A numeric beginless range without a block, such as `(..10) % 2`, still returns an arithmetic sequence; only iterating it fails. ## What Ruby 3.4 changed Before Ruby 3.4, `step` was really about numbers; other types went through `succ`. Ruby 3.4 gave it **one semantics for all types**: each next value is `current + step`. - Any type whose `+` accepts the step can now be stepped, for example a time range stepped by a number of seconds. - An endless String range with a String step concatenates: `('a'..).step('b').take(3)` is `["a", "ab", "abb"]`. - **Kept for compatibility:** a String or Symbol range with an Integer step still advances with `succ`, so `('a'..'e').step(2)` yields `a`, `c`, `e`, and a String range with no step still defaults to 1. - A step that moves away from the end yields nothing: `(0..6).step(-2)` is empty, while `(6..0).step(-2)` yields 6, 4, 2, 0. ## Working with the sequence `Enumerator::ArithmeticSequence` is a subclass of `Enumerator`, so the usual enumerator calls apply: - `first(n)`, `take(n)`, `map` and `each` walk the values lazily from the parameters. - `==` compares the parameters, so `(1..10).step(3) == (1..10) % 3` is `true`. - For **Float** steps, each value is computed as `begin + i * step` rather than by repeated addition, so `(1.0..2.0).step(0.1)` does not drift away from its end through accumulated rounding error. Because the object is only a description, it is cheap to pass around and to reuse, which is the point of returning it instead of an Array. ## A clinic example A clinic's day runs from 9:00 to 17:00, stored as minutes after midnight, and appointments start every 15 minutes: - `starts = (540...1020) % 15` builds the sequence of start minutes without an array. - `starts.size` is `32`, computed from the parameters. - `starts.first(3)` is `[540, 555, 570]`. Using `...` matters here too: the day's end minute, 1020, is not a valid start.

  • Why does ('a'..'e').step(2) yield a, c, e rather than calling 'a' + 2?
    Ruby 3.4 moved `step` to `+` for all types but kept an exception for compatibility: a String or Symbol range with an Integer step still advances with `String#succ`, taking every second value. Only a String step, such as `('a'..).step('b')`, uses `+` on strings.

saying these in an interview costs you the question

  • (1..10).step(3) without a block returns an Array
  • 0..7 % 2 builds the sequence 0, 2, 4, 6
  • step(0) loops forever on the first value
  • Range#step still works only with numbers in Ruby 4.0
  • ('a'..'e').step(2) concatenates strings with +