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:
- all?
- any?
- collect / map
- count
- 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
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:
- 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.
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.