skip to content

In Ruby, how do you number marathon finishers from 1 while iterating, and why does each_with_index(1) not do it?

level: juniorimportance: must knowfreq 62%

answer

  1. index starts at zero
  2. arguments go to each
  3. Array#each takes no arguments
  4. Enumerator#with_index(offset)
  5. each.with_index(1), map.with_index(1)

basics

~10 s

Call 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 lines
ruby
finishers = ["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

for a junior

Recall that each_with_index starts at 0 and that each.with_index(1) is the way to number from 1.

for a middle

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.

for a senior

Replace hand-kept counters in review, and check hash iterations destructure the pair so an index is not silently bound to the value.

for a principal

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