---
title: Case Equality Operator in Ruby
teaser: 'An introduction to the `===` method in Ruby, and how it works in `case` expressions
  and beyond them.

  '
tags: ruby,development
author: Neil Carvalho
published_on: 2022-12-19
---

Take a look at a `case` expression in Ruby:

```rb
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:

```rb
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.

```rb
Enumerable === [1, 2, 3]   # => true
Array === "A string"       # => false
String === "A string"      # => true
```

## `Range`

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

<aside class="info">
Note: this behavior changed on Ruby 2.7. On Ruby 2.6 and below, <code>===</code> behaves as the <code>include?</code> method.
</aside>

```rb
(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?`.

```rb
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?`.

```rb
/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.

```rb
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.

```rb
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:

```rb
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:

```rb
[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`.
