In Ruby, how do you number marathon finishers from 1 while iterating, and why does each_with_index(1) not do it?
answer
- index starts at zero
- arguments go to each
- Array#each takes no arguments
- Enumerator#with_index(offset)
- each.with_index(1), map.with_index(1)
basics
~10 sCall each.with_index(1): Enumerator#with_index takes a starting offset. each_with_index always counts from 0 and forwards its arguments to each, so on an Array each_with_index(1) raises ArgumentError instead of shifting the index.
solid answer
~30 s`each_with_index` yields each element with a zero-based index and returns the receiver. It has no offset parameter: its arguments are passed straight through to `each`, and since `Array#each` takes none, `finishers.each_with_index(1)` raises `ArgumentError` (given 1, expected 0). To start at 1, call `each` without a block to get an `Enumerator`, then `with_index(1)`: `finishers.each.with_index(1) { |name, place| puts "#{place}. #{name}" }`. `with_index` works on any enumerator, so `map.with_index(1)` builds numbered labels and `each_slice(10).with_index(1)` numbers batches. Either form beats a hand-kept `i += 1` counter, which is easy to forget to increment or to reset.
code
ruby · 17 linesfinishers = ["Abebe", "Chen", "Okafor"]
finishers.each_with_index { |name, i| puts "#{i}. #{name}" }
# 0. Abebe
# 1. Chen
# 2. Okafor
finishers.each.with_index(1) { |name, place| puts "#{place}. #{name}" }
# 1. Abebe
# 2. Chen
# 3. Okafor
finishers.map.with_index(1) { |name, place| "#{place}. #{name}" }
# => ["1. Abebe", "2. Chen", "3. Okafor"]
finishers.each_with_index(1) { |name, i| }
# ArgumentError: wrong number of arguments (given 1, expected 0)go deeper
Recall that each_with_index starts at 0 and that each.with_index(1) is the way to number from 1.
Explain that each_with_index forwards its arguments to each, which is why an offset raises ArgumentError on an Array, and show with_index after map.
Replace hand-kept counters in review, and check hash iterations destructure the pair so an index is not silently bound to the value.
Encourage team idioms that make positions explicit, such as with_index(1) for display numbering, so off-by-one fixes are not scattered through templates.
## Two ways to get an index Ruby gives you the position of each element without a manual counter in two ways: - **`each_with_index`**, defined in the `Enumerable` module, yields `element, index` with the index starting at **0**, and returns the receiver. - **`Enumerator#with_index(offset = 0)`** adds an index to any **enumerator**, starting at `offset`. An enumerator is the object you get when you call an iterator such as `each` or `map` without a block. For a marathon results table, where places start at 1, the second form is the one you want: ```ruby finishers.each.with_index(1) do |name, place| puts "#{place}. #{name}" end ``` ## Why `each_with_index(1)` fails The signature in Ruby's source is `each_with_index(*args)`, and the rdoc says it invokes `self.each` with those arguments. The arguments are **forwarded to `each`**; they are not an offset. So: 1. `finishers.each_with_index(1) { ... }` calls `finishers.each(1)`. 2. `Array#each` is defined with **zero** parameters. 3. Ruby raises `ArgumentError` (wrong number of arguments, given 1, expected 0). The same happens on a `Hash` or an integer `Range`, whose `each` methods also take no arguments. The forwarding exists for classes whose `each` does accept arguments. `IO#each`, for example, is the same method as `each_line` and accepts a separator or a limit. ## `with_index` on other iterators Because `with_index` hangs off the enumerator, it combines with any blockless iterator call: | Expression | What the block receives | Returns | |---|---|---| | `rows.each_with_index` | `row, 0..` | the receiver | | `rows.each.with_index(1)` | `row, 1..` | the receiver (what `each` returns) | | `rows.map.with_index(1)` | `row, 1..` | a new array of block results | | `rows.each_slice(50).with_index(1)` | `batch, 1..` | the receiver | `with_index` converts its offset with `to_int`, so any integer works, including negative ones. `Enumerator#each_with_index` also exists, and like the `Enumerable` version it starts at 0. ## Hashes and destructuring On a `Hash`, `each_with_index` yields **two** values: the `[key, value]` pair and the index. To name the key and value separately you need parentheses in the block parameters: ```ruby splits = {101 => 7530, 205 => 7612} splits.each_with_index do |(bib, secs), i| puts "#{i + 1}. bib #{bib}: #{secs}s" end ``` Writing `|bib, secs, i|` instead binds the whole pair to `bib`, the index to `secs`, and `nil` to `i`, which is a quiet bug rather than an error. ## Why not a counter The hand-written version works but carries bookkeeping that the index methods remove: - `place = 0` has to be declared outside the block. - `place += 1` has to happen on every path, including after a `next`. - The counter outlives the loop and can be reused by mistake. Interviewers ask this question to see whether a candidate reaches for the index methods before inventing a counter, and whether they know the offset lives on `with_index`, not on `each_with_index`. ## Numbering beyond `each` The offset idea generalises. Any iterator that returns an enumerator when called without a block can be numbered from any starting point: 1. `results.each_slice(100).with_index(1)` numbers **pages** of a results table, so the block receives `page_rows, page_number`. 2. `results.each.with_index(101)` continues numbering where a previous page left off. 3. `results.reverse_each.with_index(1)` numbers from the last finisher, which is how a "sweeper" list counting back from the final runner can be printed. In each case the method before `with_index` decides **which elements** are visited and in what order, and `with_index` only decides **what number** travels with them. Keeping those two concerns apart is what makes the code easy to read: the reader sees the traversal first and the numbering second, and neither is hidden in a counter variable. The display number and the array index are also different things. Places on a results page start at 1; `results[i]` is still zero-based. Converting between them in one place, through `with_index(1)`, avoids scattering `+ 1` and `- 1` through templates. ## Summary for the interview Say it in one line: `each_with_index` is zero-based and forwards its arguments to `each`, so the offset form is `each.with_index(1)`. Then show that the same trick works after `map` and `each_slice`, and that a hash's pair needs `(k, v)` destructuring.
- How do you build an array of numbered labels rather than printing them?Call `map` without a block and add the index: `finishers.map.with_index(1) { |name, place| "#{place}. #{name}" }`. `with_index` returns whatever the underlying iterator returns, so after `map` you get a new array of the block's results, while after `each` you get the receiver back.
- When are the arguments to each_with_index actually useful?When the receiver's `each` accepts arguments. `each_with_index(*args)` calls `each(*args)`, so for a class whose `each` takes a separator or a filter, the arguments reach it. `IO#each` is the same method as `each_line`, so an open file can be walked by a custom separator with its line index.
saying these in an interview costs you the question
- each_with_index(1) starts counting at 1
- Ruby cannot offset the index, so a manual counter is required
- with_index works only after each, not after map or each_slice
- Hash#each_with_index passes key, value and index as three separate values
- each.with_index(1) returns an array of the block's results