Categories
Uncategorized

Enumerating Enumerable: Enumerable#detect/Enumerable#find

Here’s another article in the Enumerating Enumerable series, in which I attempt to improve upon RubyDoc.org’s documentation for the Enumerable module, which I find rather lacking. If you’ve missed the previous articles in the series, I’ve listed them below:

  1. all?
  2. any?
  3. collect / map
  4. count
  5. cycle

This installment covers a method that goes by two names: detect or find. I personally prefer find, as it’s shorter and the term I tend to use for its function.

Enumerable#detect/Enumerable#find Quick Summary

Graphic representation of the \"detect\" or \"find\" method in Ruby\'s \"Enumerable\" module.

In the simplest possible terms What’s the first item in the collection that meets the given criteria?
Ruby version 1.8 and 1.9
Expects
  • A block containing the criteria.
  • An optional argument containing a proc that calculates a “default” value — that is, the value to return if no item in the collection matches the criteria.
Returns The first item in the collection that matches the criteria, if one exists.
If no such item exists in the collection, detect/find returns:

  • nil is returned if no argument is provided
  • the value of the argument, if one is provided.
RubyDoc.org’s entry Enumerable#detect / Enumerable#find

Enumerable#detect/Enumerable#find and Arrays

When used on an array without an argument, detect/find passes each item from the collection to the block and…

  • If the current item causes the block to return a value that doesn’t evaluate to false, detect/find stops going through collection and returns the item.
  • If no item in the collection causes the block to return a value that doesn’t evaluate to false, detect/find returns nil.

In the examples that follow, I’ll be using the find method. detect does exactly the same thing; it’s just that I prefer find.

# Time to establish my "old fart" credentials
classic_rock_bands = ["AC/DC", "Black Sabbath", "Queen", \
"Ted Nugent and the Amboy Dukes", "Scorpions", "Van Halen"]
=> ["AC/DC", "Black Sabbath", "Queen", "Ted Nugent and the Amboy Dukes",
"Scorpions", "Van Halen"]

# Of the bands in the array, which is the first one
# whose name is longer than 8 characters?
classic_rock_bands.find {|band| band.length > 8}
=> "Black Sabbath"

# Which is the first one whose name is exactly
# 5 characters long?
classic_rock_bands.find {|band| band.length == 5}
=> "AC/DC"

# The order of items in the array can affect "find"'s result.
# Let's put the array into reverse sorted order:
classic_rock_bands.sort!.reverse!
=> ["Van Halen", "Ted Nugent and the Amboy Dukes", "Scorpions", \
"Queen", "Black Sabbath", "AC/DC"]

# Again: which is the first band whose name
# is longer than 8 characters?
=> "Van Halen"

# Again: which is the first band whose name
# is exactly 5 characters?
=> "Queen"

# If no band in the array meets the criteria,
# "find" returns nil.
# There are no bands in the list with names shorter
# than 5 characters...
classic_rock_bands.find {|band| band.length < 5}
=> nil

Using the optional argument is a topic big enough to merit its own section, which appears later in this article.

Enumerable#detect/Enumerable#find and Hashes

When used on a hash and a block is provided, detect/find 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.

When used on a hash without an argument, detect/find passes each item from the collection to the block and…

  • If the current item causes the block to return a value that doesn’t evaluate to false, detect/find stops going through collection and returns the item.
  • If no item in the collection causes the block to return a value that doesn’t evaluate to false, detect/find returns nil.

years_founded = {"AC/DC" => 1973, \
                 "Black Sabbath" => 1968, \
                 "Queen" => 1970, \
                 "Ted Nugent and the Amboy Dukes" => 1967, \
                 "Scorpions" => 1965, \
                 "Van Halen" => 1972}
# Ruby 1.8 re-orders hashes in some mysterious way that is almost never
# the way you entered it. Here's what I got in Ruby 1.8:
=> {"Queen"=>1970, "AC/DC"=>1973, "Black Sabbath"=>1968, "Scorpions"=>1965,
"Ted Nugent and the Amboy Dukes"=>1967, "Van Halen"=>1972}

# Ruby 1.9 preserves hash order so that hashes keep the order in which
# you defined them. Here's what I got in Ruby 1.9:
=> {"AC/DC"=>1973, "Black Sabbath"=>1968, "Queen"=>1970,
"Ted Nugent and the Amboy Dukes"=>1967, "Scorpions"=>1965, "Van Halen"=>1972}

# Which band is the first in the hash to be founded
# in 1970 or later?
years_founded.find {|band| band[1] >= 1970}
# In Ruby 1.8, the result is:
=> ["Queen", 1970]
# In Ruby 1.9, the result is:
=> ["AC/DC", 1973]

# Here's another way of phrasing it:
years_founded.find {|band, year_founded| year_founded >= 1970}

Using Enumerable#detect/Enumerable#find with the Optional Argument

detect/find‘s optional argument lets you specify a proc or lambda whose return value will be the result in cases where no object in the collection matches the criteria.

(Unfortunately, a complete discussion of procs and lambdas is beyond the scope of this article. I highly recommend looking at Eli Bendersky’s very informative article, Understanding Ruby blocks, Procs and methods.)

I think that the optional argument is best explained through examples…

# Once again, the array of classic rock bands
classic_rock_bands = ["AC/DC", "Black Sabbath", "Queen", \
"Ted Nugent and the Amboy Dukes", "Scorpions", "Van Halen"]

# Let's define a proc that will simply returns the band name
# "ABBA", who are most definitely not considered to be
# a classic rock band.
default_band = Proc.new {"ABBA"}

# Procs are objects, so using a proc's name alone isn't sufficient
to invoke its code -- doing so will simply return the proc object.
default_band
=> #<Proc:0x00553f34@(irb):31>
# (The actual value will be different for you, but you get the idea.)

# To call a proc, you have to use its "call" method:
default_band.call
=> "ABBA"

# What we want to do is use "default_band" as a proc that
# provides a default value in the event that detect/find doesn't
# find a value that matches the critera.

# detect/find calls the "call" method of the object you provide
# as the argument if no item in the collection matches the
# criteria in the block.

# There *is* a band in the array that comes after
# "Led Zeppelin" alphabetically: Queen.
classic_rock_bands.find(default_band) {|band| band > "Led Zeppelin"}
=> "Queen"

# The last band in the array, alphabetically speaking,
# is Van Halen. So if we ask detect/find to find a band that
# comes after Van Halen, it won't find one.
# Without the argument, detect/find returns nil,
# but *with* the argument, it will invoke the "call"
# method of the object you provide it.
classic_rock_bands.find(default_band) {|band| band > "Van Halen"}
=> "ABBA"

# Let's try something a little fancier. This time, we'll use a lambda.
# The differences between procs and lambdas are very fine -- I suggest
# you check Eli Bendersky's article for those differences.

# Let's create a lambda that when called,
# returns the name of a randomly-selected pop singer.
random_band = lambda do
    fallback_bands = ["Britney Spears", "Christina Aguilera", "Ashlee Simpson"]
    fallback_bands[rand(fallback_bands.size)]
end

# Let's give it a try...
classic_rock_bands.find(random_band) {|band| band > "Van Halen"}
=> "Britney Spears"

classic_rock_bands.find(random_band) {|band| band > "Van Halen"}
=> "Ashlee Simpson"

classic_rock_bands.find(random_band) {|band| band > "Van Halen"}
=> "Christina Aguilera"

classic_rock_bands.find(random_band) {|band| band > "Led Zeppelin"}
=> "Queen"

To see a “real” application of detect/find's optional argument, see this Ruby Quiz problem.