Case Equality Operator in Ruby

Neil Carvalho
This article is also available in: Português

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 in case 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.