Enumerating Enumerable: Enumerable#find_index

by Joey deVilla on August 14, 2008

Enumerating Enumerable

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:

  1. all?
  2. any?
  3. collect / map
  4. count
  5. cycle
  6. detect / find
  7. drop
  8. drop_while
  9. each_cons
  10. each_slice
  11. each_with_index
  12. entries / to_a
  13. find_all / select

Enumerable#find_index Quick Summary

Graphic representation of the "find_index" method in Ruby's "Enumerable" module

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:

  1. A two-element array, with the key as element 0 and its corresponding value as element 1, or
  2. 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:

  1. 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?”
  2. 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.

Previous post:

Next post: