Introduction
Ruby has five distinct equality/comparison operators, each with a specific semantic meaning. Using the wrong one is a common source of bugs. This guide explains each one clearly with examples.
== : Value Equality
== tests whether two objects have the same value. It’s the most commonly used equality check and can be overridden in any class.
# Numbers
1 == 1 # => true
1 == 1.0 # => true (Integer and Float with same value)
1 == "1" # => false (different types)
# Strings
"hello" == "hello" # => true
"hello" == "Hello" # => false (case-sensitive)
# Arrays
[1, 2, 3] == [1, 2, 3] # => true
[1, 2, 3] == [1, 2] # => false
# Hashes
{a: 1} == {a: 1} # => true
{a: 1} == {a: 2} # => false
# nil
nil == nil # => true
nil == false # => false
Custom == in Your Classes
class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
end
def ==(other)
other.is_a?(Point) && x == other.x && y == other.y
end
end
p1 = Point.new(1, 2)
p2 = Point.new(1, 2)
p3 = Point.new(3, 4)
p1 == p2 # => true (same values)
p1 == p3 # => false
p1 == [1, 2] # => false (not a Point)
eql? : Strict Value Equality
eql? is stricter than == — it requires both value and type to match. It’s used by Hash to compare keys.
1.eql?(1) # => true
1.eql?(1.0) # => false (Integer ≠ Float, even with same value)
1.eql?("1") # => false
"hello".eql?("hello") # => true
"hello".eql?("Hello") # => false
:foo.eql?(:foo) # => true
Why eql? Matters for Hash Keys
Hash uses eql? (and hash) to determine key equality:
h = {}
h[1] = "integer one"
h[1.0] = "float one"
puts h.length # => 2 (1 and 1.0 are different keys!)
puts h[1] # => "integer one"
puts h[1.0] # => "float one"
# But:
1 == 1.0 # => true
1.eql?(1.0) # => false (different hash keys)
Implementing eql? in Custom Classes
When you override eql?, you must also override hash to maintain the contract:
class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
end
def ==(other)
other.is_a?(Point) && x == other.x && y == other.y
end
def eql?(other)
other.is_a?(Point) && x.eql?(other.x) && y.eql?(other.y)
end
def hash
[x, y].hash # must be consistent with eql?
end
end
p1 = Point.new(1, 2)
p2 = Point.new(1, 2)
# Now Points work correctly as Hash keys
h = { p1 => "first point" }
puts h[p2] # => "first point" (p1.eql?(p2) is true)
equal? : Object Identity
equal? tests whether two variables refer to the exact same object in memory (same object_id). It should never be overridden.
a = "hello"
b = "hello"
c = a
a.equal?(b) # => false (different objects, same value)
a.equal?(c) # => true (same object)
a.object_id == c.object_id # => true
# Symbols and small integers are always the same object
:foo.equal?(:foo) # => true
1.equal?(1) # => true
<=> : The Spaceship Operator (Comparison)
<=> returns -1, 0, or 1 (or nil if not comparable). It’s the foundation of sorting and the Comparable module.
1 <=> 2 # => -1 (less than)
2 <=> 2 # => 0 (equal)
3 <=> 2 # => 1 (greater than)
1 <=> "a" # => nil (not comparable)
"apple" <=> "banana" # => -1
"banana" <=> "apple" # => 1
"apple" <=> "apple" # => 0
Using Comparable
Include Comparable and define <=> to get <, <=, >, >=, between?, and clamp for free:
class Temperature
include Comparable
attr_reader :degrees
def initialize(degrees)
@degrees = degrees
end
def <=>(other)
degrees <=> other.degrees
end
def to_s
"#{degrees}°"
end
end
temps = [Temperature.new(100), Temperature.new(0), Temperature.new(37)]
puts temps.sort.map(&:to_s).inspect
# => ["0°", "37°", "100°"]
puts temps.min # => 0°
puts temps.max # => 100°
body_temp = Temperature.new(37)
puts body_temp.between?(Temperature.new(36), Temperature.new(38)) # => true
=== : Case Equality (Triple Equals)
=== is used by case/when statements. Its behavior varies by class:
# Integer: same as ==
1 === 1 # => true
1 === 1.0 # => true
# Range: membership test
(1..10) === 5 # => true
(1..10) === 15 # => false
# Regexp: pattern match
/hello/ === "hello world" # => true
/hello/ === "goodbye" # => false
# Class/Module: is_a? check
String === "hello" # => true
Integer === 42 # => true
Integer === "42" # => false
# Proc/Lambda: call the proc
even = ->(n) { n.even? }
even === 4 # => true
even === 3 # => false
How case/when Uses ===
value = 42
case value
when 1..10 # (1..10) === 42 => false
puts "small"
when 11..100 # (11..100) === 42 => true
puts "medium"
when Integer # Integer === 42 => true (not reached)
puts "an integer"
end
# => "medium"
# Pattern matching with Regexp
http_status = "404 Not Found"
case http_status
when /^2\d\d/
puts "Success"
when /^4\d\d/
puts "Client error" # => this matches
when /^5\d\d/
puts "Server error"
end
Custom === for DSLs
class AgeRange
def initialize(min, max)
@min, @max = min, max
end
def ===(age)
age >= @min && age <= @max
end
def to_s
"#{@min}-#{@max}"
end
end
child = AgeRange.new(0, 12)
teen = AgeRange.new(13, 17)
adult = AgeRange.new(18, 64)
senior = AgeRange.new(65, 120)
age = 25
case age
when child then puts "Child"
when teen then puts "Teen"
when adult then puts "Adult" # => matches
when senior then puts "Senior"
end
=~ : Pattern Match
=~ tests a string against a regular expression and returns the position of the match (or nil):
"hello world" =~ /world/ # => 6 (position of match)
"hello world" =~ /xyz/ # => nil
# Sets $~ (MatchData), $1, $2, etc.
if "2026-03-29" =~ /(\d{4})-(\d{2})-(\d{2})/
puts $1 # => "2026"
puts $2 # => "03"
puts $3 # => "29"
end
Quick Reference
| Operator | Tests | Override? | Used by |
|---|---|---|---|
== |
Value equality | Yes | General comparison |
eql? |
Strict value + type | Yes (with hash) |
Hash keys |
equal? |
Object identity | Never | Identity check |
<=> |
Ordering | Yes | Sorting, Comparable |
=== |
Case equality | Yes | case/when |
=~ |
Regex match | Yes | Pattern matching |
Comments