Take a look at a case
expression in Ruby:
case x
when 1, 2
"It is either 1 or 2"
when 3..10
"It is a number from 3 to 10"
when BigDecimal
"It is a BigDecimal instance"
when /bot/
"It is a string that matches the /bot/ regular expression"
end
Compared to many languages that have switch statements, case
expressions in Ruby are much more flexible, matching not just on strict equality but also some sort of membership or match: 2
is a member of the range 1..3
; 5
is a member of the class Integer
; "thoughtbot"
matches the regular expression /bot/
.
This power comes from the ===
operator (or case equality operator, threequals, triple equals, you name it). Each argument passed to when
is compared to x
using this operator. Ruby will interpret the case
expression above as if it was:
if 1 === x || 2 === x
"It is either 1 or 2"
elsif 3..10 === x
"It is a number from 3 to 10"
elsif BigDecimal === x
"It is a BigDecimal instance"
elsif /bot/ === x
"It is a string that matches the /bot/ regular expression"
end
The Object
class, which is inherited by most Ruby classes, implements the ===
method as an alias to ==
. The documentation for Object
describes it as:
Case Equality – For class
Object
, effectively the same as calling#==
, but typically overridden by descendants to provide meaningful semantics incase
expressions.
How do descendants override it?
Module
The Module
class, inherited by the Class
class itself, implements ===
as an is_a?
check on the object provided. It will return true
if the class or module is in the ancestor tree of the given object.
Enumerable === [1, 2, 3] # => true
Array === "A string" # => false
String === "A string" # => true
Range
Ranges implement ===
with the same behavior as cover?
.
(1..10) === 3.5 # => true
(5..20) === 2 # => false
Set
A set will respond to ===
by checking whether the provided object is a member of that set. Effectively the same as include?
.
Set[1, 2, 3] === 2 # => true
Set[1, 2, 3] === "😀" # => false
Regexp
Regexp
objects will return true
on ===
when, given a string, the regular expression matches the string. It’s close to the implementation of match?
.
/bot/ === "thoughtbot" # => true
URI::MailTo::EMAIL_REGEXP === "https://thoughtbot.com" # => false
IPAddr
IPAddr
objects respond to ===
as an alias for include?
. They will return true
if the given IP is equal to or is in the range.
subnet = IPAddr.new("192.168.2.0/24")
ip = IPAddr.new("192.168.2.100")
subnet.include?(ip) # => true
subnet === ip # => true
Proc
Proc
objects, including lambdas, have too many ways to be called, and ===
is yet another one.
Prime = ->(n) { n >= 2 && (2..Math.sqrt(n)).none? { |i| n % i == 0 } }
Prime === 3 # => true
Prime === 4 # => false # 4 is not a prime number
Any class you write
Now that you know how case
expressions work under the hood, you can implement the case equality operator in your own classes and match their instances in a meaningful way, such as whether a GPS point is within an area:
class Map::Area
# ...
def ===(other)
if other.respond_to?(:latitude) && other.respond_to?(:longitude)
# Is the given coordinate within this area?
else
other == self
end
end
end
state = Map::Area.new(list_of_gps_coordinates_to_draw_the_area)
city = Map::Point.new(-9.648139, -35.717239)
state === city # => true
Beyond case expressions
You may be wondering that, as case expressions are a code smell that your code has too much knowledge, the ===
method isn’t so useful. But there’s more use of the case equality operator beyond its initial purpose.
The Enumerable
module implements many methods that use it. One interesting example is #grep
. It will match the enumerable elements based on the given pattern using the ===
method, so:
[1, "a", 3].grep(Integer) # => [1, 3]
["foo", "bar", "baz"].grep(/a/) # => ["bar", "baz"]
(1..10).grep(3..8) # => [3, 4, 5, 6, 7, 8]
(1..15).grep(Prime) # => [2, 3, 5, 7, 11, 13]
venues.grep(map_area) # => [venue_1, venue_3, ...]
Other Enumerable
methods that use the case equality operator are: #all?
, #none?
, #one?
, #any?
, #grep_v
, #slice_after
, and #slice_before
.