In Ruby, what do (1..10).step(3) and (1..10) % 3 return, and what did Ruby 3.4 change about Range#step?
answer
- not an Array
- Enumerator::ArithmeticSequence
- % is step with no default
- 0..7 % 2 parses as 0..1
- 3.4: step uses + for all types
basics
~10 sWithout 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 sOn 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 linesseq = (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 0go deeper
Recall that step walks a range by increments and that without a block it returns a sequence object, not an Array.
Explain ArithmeticSequence, why % needs parentheses, the errors for a zero or missing step, and the String succ exception.
Use arithmetic sequences to describe slot grids without allocating arrays, and check stepping code for the 3.4 semantics change.
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 +