Once again, it’s Enumerating Enumerable, my series of articles in which I attempt to outdo Ruby-Doc.org’s documentation of Ruby’s Enumerable
module. In this article, I cover the find_index
method, which was introduced in Ruby 1.9.
In case you missed any of the previous articles, they’re listed and linked below:
- all?
- any?
- collect / map
- count
- cycle
- detect / find
- drop
- drop_while
- each_cons
- each_slice
- each_with_index
- entries / to_a
- find_all / select
Enumerable#find_index Quick Summary
In the simplest possible terms |
What’s the index of the first item in the collection that meets the given criteria? |
Ruby version |
1.9 |
Expects |
A block containing the criteria. |
Returns |
- The index of the item in the collection that matches the criteria, if there is one.
nil , if no item in the collection matches the crtieria.
|
RubyDoc.org’s entry |
Enumerable#find_index |
Enumerable#find_index and Arrays
When used on an array, find_index
passes each item in the array to the given block and either:
- Stops when the current item causes the block to return a value that evaluates to
true
(that is, anything that isn’t false
or nil
) and returns the index of that item, or
- Returns
nil
if there is no item in the array that causes the block to return a value that evaluates to true
.
Some examples:
# How about an array of the name of the first cosmonauts and astronauts,
# listed in the chronological order of the missions?
mission_leaders = ["Gagarin", "Shepard", "Grissom", "Titov", "Glenn", "Carpenter",
"Nikolayev", "Popovich"]
=> ["Gagarin", "Shepard", "Grissom", "Titov", "Glenn", "Carpenter", "Nikolayev",
"Popovich"]
# Yuri Gagarin was the first in space
mission_leaders.find_index{|leader| leader == "Gagarin"}
=> 0
# John Glenn was the fifth
mission_leaders.find_index{|leader| leader == "Glenn"}
=> 4
# And James Tiberius Kirk is not listed in the array
kirk_present = mission_leaders.find_index{|leader| leader == "Kirk"}
=> nil
Enumerable#find_index and Hashes
When used on a hash, find_index
passes each key/value pair in the hash to the block, which you can “catch” as either:
- A two-element array, with the key as element 0 and its corresponding value as element 1, or
- Two separate items, with the key as the first item and its corresponding value as the second item.
As with arrays, find_index
:
- Stops when the current item causes the block to return a value that evaluates to
true
(that is, anything that isn’t false
or nil
) and returns the index of that item, or
- Returns
nil
if there is no item in the array that causes the block to return a value that evaluates to true
.
Some examples:
require 'date'
=> true
# These are the names of the first manned spaceships and their launch dates
launch_dates = {"Kedr" => Date.new(1961, 4, 12),
"Freedom 7" => Date.new(1961, 5, 5),
"Liberty Bell 7" => Date.new(1961, 7, 21),
"Orel" => Date.new(1961, 8, 6),
"Friendship 7" => Date.new(1962, 2, 20),
"Aurora 7" => Date.new(1962, 5, 24),
"Sokol" => Date.new(1962, 8, 11),
"Berkut" => Date.new(1962, 8, 12)}
=> {"Kedr"=>#<Date: 4874803/2,0,2299161>, "Freedom 7"=>#<Date: 4874849/2,0,2299161>,
"Liberty Bell 7"=>#<Date: 4875003/2,0,2299161>, "Orel"=>#<Date: 4875035/2,0,2299161>,
"Friendship 7"=>#<Date: 4875431/2,0,2299161>, "Aurora 7"=>#<Date: 4875617/2,0,2299161>,
"Sokol"=>#<Date: 4875775/2,0,2299161>, "Berkut"=>#<Date: 4875777/2,0,2299161>}
# Where in the list is John Glenn's ship, the Friendship 7?
launch_dates.find_index{|ship, date| ship == "Friendship 7"}
=> 4
# Where in the list is the first mission launched in August 1962?
launch_dates.find_index{|ship, date| date.year == 1962 && date.month == 8}
=> 6
# The same thing, expressed a little differently
launch_dates.find_index{|launch| launch[1].year == 1962 && launch[1].month == 8}
=> 6
Using find_index as a Membership Test
Although Enumerable
has a method for checking whether an item is a member of a collection (the include?
method and its synonym, member?
), find_index
is a more powerful membership test for two reasons:
include?
/member?
only check membership by using the ==
operator, while find_index
lets you define a block to set up all sorts of tests. include?
/member?
asks “Is there an object X in the collection equal to my object Y?” while find_index
can be used to ask “Is there an object X in the collection that matches these criteria?”
include?
/member?
returns true
if there is an object X in the collection that is equal to the given object Y. find_index
goes one step further: not only can it be used to report the equivalent of true
if there is an object X in the collection that is equal to the given object Y, it also reports its location in the collection.
A quick example of this use in action:
# Once again, the mission leaders
mission_leaders = ["Gagarin", "Shepard", "Grissom", "Titov", "Glenn", "Carpenter",
"Nikolayev", "Popovich"]
=> ["Gagarin", "Shepard", "Grissom", "Titov", "Glenn", "Carpenter", "Nikolayev",
"Popovich"]
# Yuri Gagarin is in the list
gagarin_in_list = mission_leaders.find_index {|leader| leader == "Gagarin"}
=> 0
# Captain James T. Kirk is not
kirk_in_list = mission_leaders.find_index {|leader| leader == "Kirk"}
=> nil
# gagarin_in_list is 0, which as a non-false and non-nil value evaluates as true.
# We can use it as both a membership test *and* as his location in the list.
p "Gagarin's there. He's number #{gagarin_in_list + 1} in the list." if gagarin_in_list
"Gagarin's there. He's number 1 in the list."
=> "Gagarin's there. He's number 1 in the list."
# kirk_in_list is nil, which is one of Ruby's two "false" values.
# Let's use it with the "something OR something else" idiom that
# many Ruby programmers like.
kirk_in_list || (p "You only *think* he wasn't there.")
"You only *think* he wasn't there."
=> "You only *think* he wasn't there."
Parts that Haven’t Been Implemented Yet
Ruby-Doc.org’s documentation is generated from the comments in the C implementation of Ruby. It mentions a way of calling find_index
that is just like calling include?
/member?
:
# What the docs say (does not work yet!)
["Alice", "Bob", "Carol"].find_index("Bob")
=> 1
# What happens with the current version of Ruby 1.9
["Alice", "Bob", "Carol"].find_index("Bob")
ArgumentError: wrong number of arguments(1 for 0)
...
Ruby 1.9 is considered to be a work in progress, so I suppose it’ll get implemented in a later release.