Know Ruby: with_index
Have you ever used with_index
?
Not each_with_index
which is similar but slightly different.
Did you know that you can do map.with_index
?
If you’ve written code like this:
i = 0
lines.map do |line|
i += 1
"#{i}) #{line}"
end
or
lines.each_with_index do |line, i|
puts "#{i + 1}) #{line}"
end
then keep reading.
What does it do?
Adding with_index
to an enumeration lets you enumerate that enumeration.
Say that ten times fast.
A quick example will clarify that a bit.
Let’s say I have a list of three, I don’t know, famous Martians.
martians = ["Marvin", "J'onn J'onzz", "Mark Watney"]
I’ll list them along with their current position in the array.
> martians
* .each
* .with_index(1) do |martian, i|
* puts "#{i}) #{martian}"
> end
1) Marvin
2) J'onn J'onzz
3) Mark Watney
=> ["Marvin", "J'onn J'onzz", "Mark Watney"]
As I mentioned earlier, with_index
isn’t limited to each
.
I could replace each
with map
in the example above.
> martians
* .map
* .with_index(1) do |martian, i|
* "#{i}) #{martian}"
> end
=> ["1) Marvin", "2) J'onn J'onzz", "3) Mark Watney"]
You probably noticed that I’m passing 1
to with_index
.
It accepts an integer offset defaulted to 0
.
I’ve found this to be handy when generating user-facing information.
They usually don’t want their lists to be zero-indexed.
No more having to do i + 1
inside the block.
It’s also useful when you have a dynamic list that starts after some hard-coded entries.
Can’t I just use each_with_index
?
Sure, in fact, sometimes it’s better. Deciding which to use comes down to two considerations.
In the previous section, I passed an offset of 1
to with_index
.
You can’t do this with each_with_index
.
Both each
and with_index
take arguments and each_with_index
forwards them to each
.
At this point, you’re probably thinking, “I have never in my life seen someone pass an argument to each
.”
Me either but it can be done.
On IO#each
and StringIO#each
you can pass a line separator, a line limit, or both.
The Matrix#each
lets you provide a which
to select the types of elements you want to iterate.
With Prime#each
you can pass an upper bound or even your own prime generator.
> Prime.each_with_index(10) do |prime, i|
* puts "#{i}. #{prime}"
> end
0. 2
1. 3
2. 5
3. 7
=> Prime
Tell me that’s not confusing.
The other thing to know is that each_with_index
is faster than each.with_index
.
Calculating -------------------------------------
each_with_index 636.248k (± 5.0%) i/s - 3.173M in 5.000131s
each.with_index 465.312k (± 7.6%) i/s - 2.317M in 5.009064s
Comparison:
each_with_index: 636247.9 i/s
each.with_index: 465312.3 i/s - 1.37x slower
Any time you see two methods smashed together (e.g. reverse_each
), it’s going to be faster.
Chances are you don’t need the speed but if you do well, there it is.
Halt, and be fricasseed.
Another neat feature of with_index
is that you can keep chaining them.
Remember those famous Martians?
Let’s say I want to sort them and print their initial and final position in the list.
> ["Marvin", "J'onn J'onzz", "Mark Watney"]
* .map.with_index(1).to_a # add the initial position
* .sort
* .each.with_index(1) do |(martian, initial), final|
* puts "#{martian} moved from #{initial} to #{final}."
> end
J'onn J'onzz moved from 2 to 1.
Mark Watney moved from 3 to 2.
Marvin moved from 1 to 3.
=> [["J'onn J'onzz", 2], ["Mark Watney", 3], ["Marvin", 1]]
If you chain like this don’t forget to destructure the arguments (see the parentheses in the block arguments).
While fun, I don’t usually need to write code like that.
I have gotten some use out of chaining with_index
and with_object
.
Oh yeah, if you didn’t know, with_object
is also its own thing.
Anyway, that’s a story for another Know Ruby.