Case Equality Operator in Ruby

Neil Carvalho

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"

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"

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?


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


Ranges implement === with the same behavior as cover?.

(1..10) === 3.5 # => true
(5..20) === 2   # => false


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 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 === "" # => false


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 ="")
ip ="")

subnet.include?(ip) # => true
subnet === ip       # => true


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?
      other == self

state =
city =, -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.