Overview
Ruby provides multiple comparison methods that serve different purposes in determining how objects relate to each other. The core comparison methods include <=>
(spaceship operator), ==
, eql?
, equal?
, and hash
. These methods form the foundation for sorting algorithms, hash table operations, and equality testing throughout the Ruby ecosystem.
The <=>
method returns -1
, 0
, or 1
when the left operand is less than, equal to, or greater than the right operand, respectively. This method serves as the basis for all other comparison operations when a class includes the Comparable module.
class Version
def initialize(version)
@parts = version.split('.').map(&:to_i)
end
def <=>(other)
@parts <=> other.parts
end
protected
attr_reader :parts
end
v1 = Version.new("1.2.3")
v2 = Version.new("1.2.4")
v1 <=> v2 # => -1
The ==
method tests for value equality and is the most commonly overridden comparison method. Ruby's built-in classes implement ==
to compare meaningful content rather than object identity.
class Person
def initialize(name, age)
@name, @age = name, age
end
def ==(other)
other.is_a?(Person) && @name == other.name && @age == other.age
end
protected
attr_reader :name, :age
end
Hash tables rely on both eql?
and hash
methods working together. Objects that are eql?
must return the same hash value, while objects with different hash values are guaranteed not to be eql?
.
Basic Usage
Ruby's comparison methods follow specific conventions that determine their behavior in different contexts. The ==
method provides value-based equality comparison and is aliased to ===
in most classes except special cases like Module and Regexp.
# String comparison
"hello" == "hello" # => true
"hello" == "HELLO" # => false
# Numeric comparison with type coercion
5 == 5.0 # => true
5.eql?(5.0) # => false
# Array comparison
[1, 2, 3] == [1, 2, 3] # => true
[1, 2, 3] == [1, 3, 2] # => false
The eql?
method provides strict equality without type coercion and is used by Hash for key comparison. This method should return true only when two objects are interchangeable as hash keys.
hash = { 5 => "five", 5.0 => "five point zero" }
hash[5] # => "five"
hash[5.0] # => "five point zero"
hash.size # => 2
# This demonstrates that 5 and 5.0 are not eql?
5.eql?(5.0) # => false
5 == 5.0 # => true
Object identity comparison uses equal?
, which tests whether two variables reference the same object in memory. This method should never be overridden as it provides the foundation for object identity throughout Ruby.
str1 = "hello"
str2 = "hello"
str3 = str1
str1 == str2 # => true
str1.equal?(str2) # => false (different objects)
str1.equal?(str3) # => true (same object)
The Comparable module provides a complete set of comparison operators (<
, <=
, >
, >=
, between?
) when a class defines the <=>
method. This eliminates the need to implement each operator individually.
class Temperature
include Comparable
def initialize(celsius)
@celsius = celsius
end
def <=>(other)
@celsius <=> other.celsius
end
def to_s
"#{@celsius}°C"
end
protected
attr_reader :celsius
end
temp1 = Temperature.new(20)
temp2 = Temperature.new(25)
temp1 < temp2 # => true
temp1 > temp2 # => false
temp2.between?(temp1, Temperature.new(30)) # => true
Advanced Usage
Custom comparison implementations require careful consideration of transitivity, reflexivity, and consistency across related methods. When overriding ==
, ensure the method is symmetric (a == b implies b == a) and transitive (a == b and b == c implies a == c).
class CaseInsensitiveString
def initialize(str)
@str = str
@normalized = str.downcase
end
def ==(other)
case other
when String
@normalized == other.downcase
when CaseInsensitiveString
@normalized == other.normalized
else
false
end
end
def eql?(other)
other.is_a?(CaseInsensitiveString) && @normalized == other.normalized
end
def hash
@normalized.hash
end
def <=>(other)
case other
when String
@normalized <=> other.downcase
when CaseInsensitiveString
@normalized <=> other.normalized
end
end
protected
attr_reader :normalized
end
# Demonstrates symmetric equality
ci_str = CaseInsensitiveString.new("Hello")
ci_str == "hello" # => true
"hello" == ci_str # => true (String#== delegates to ci_str.==)
The Comparable module enables sophisticated sorting and range operations. When implementing <=>
, return nil
for incomparable objects rather than raising an exception, which allows mixed-type collections to handle comparison failures gracefully.
class Priority
include Comparable
LEVELS = { low: 1, medium: 2, high: 3, critical: 4 }
def initialize(level)
@level = level
@value = LEVELS[level]
end
def <=>(other)
return nil unless other.is_a?(Priority)
@value <=> other.value
end
def to_s
@level.to_s
end
protected
attr_reader :value
end
priorities = [Priority.new(:high), Priority.new(:low), Priority.new(:critical)]
sorted = priorities.sort # => [low, high, critical]
# Range operations work automatically
medium_to_high = Priority.new(:medium)..Priority.new(:high)
medium_to_high.include?(Priority.new(:high)) # => true
Comparison methods interact with Ruby's coercion system for numeric types. When implementing comparison methods for numeric-like objects, consider implementing coerce
to enable mixed-type arithmetic and comparison.
class Fraction
include Comparable
def initialize(numerator, denominator)
@num = numerator
@den = denominator
reduce!
end
def <=>(other)
case other
when Fraction
(@num * other.den) <=> (other.num * @den)
when Integer
@num <=> (other * @den)
when Float
to_f <=> other
else
begin
other_num, self_num = other.coerce(self)
self_num <=> other_num
rescue
nil
end
end
end
def coerce(other)
case other
when Integer
[Fraction.new(other, 1), self]
when Float
[other, to_f]
else
super
end
end
def to_f
@num.to_f / @den
end
private
def reduce!
gcd_val = @num.gcd(@den)
@num /= gcd_val
@den /= gcd_val
self
end
protected
attr_reader :num, :den
end
half = Fraction.new(1, 2)
half > 0.4 # => true
half < 1 # => true
0.75 > half # => true
Common Pitfalls
The distinction between ==
, eql?
, and equal?
creates confusion because their behavior varies across Ruby's built-in classes. The ==
method performs type conversion for numeric types but not for other types, while eql?
never performs type conversion.
# Numeric type coercion with ==
1 == 1.0 # => true
1.eql?(1.0) # => false
# No coercion for non-numeric types
"1" == 1 # => false
"1".eql?(1) # => false
# Hash keys use eql? for comparison
hash = {}
hash[1] = "integer one"
hash[1.0] = "float one"
hash.size # => 2 (1 and 1.0 are different keys)
# Array membership uses ==
[1, 2, 3].include?(1.0) # => true
{1 => "one"}.key?(1.0) # => false
Implementing hash
incorrectly breaks hash table behavior. Objects that are eql?
must have identical hash values, but the reverse is not required. Failing to maintain this invariant causes hash lookups to fail unexpectedly.
class BadHashExample
def initialize(value)
@value = value
end
def ==(other)
other.is_a?(BadHashExample) && @value == other.value
end
def eql?(other)
self == other
end
# BAD: hash changes based on mutable state
def hash
@value.hash
end
attr_accessor :value
end
obj1 = BadHashExample.new("test")
obj2 = BadHashExample.new("test")
hash = {}
hash[obj1] = "found"
obj1.eql?(obj2) # => true
hash[obj1] # => "found"
hash[obj2] # => "found" (works initially)
obj1.value = "changed"
hash[obj1] # => nil (broken! hash changed after insertion)
The correct implementation bases hash values on immutable characteristics and remains consistent throughout the object's lifetime in hash collections.
class GoodHashExample
def initialize(id, name)
@id = id # Immutable identifier
@name = name # Mutable data
end
def ==(other)
other.is_a?(GoodHashExample) && @id == other.id
end
def eql?(other)
self == other
end
# GOOD: hash based on immutable identifier
def hash
@id.hash
end
attr_accessor :name
attr_reader :id
end
Comparison methods must handle nil
and incomparable types appropriately. Raising exceptions for incomparable types breaks sorting algorithms, while returning false for nil
comparisons can produce unexpected results.
class RobustComparison
include Comparable
def initialize(value)
@value = value
end
def <=>(other)
return nil unless other.respond_to?(:value) && other.value.respond_to?(:<=>)
# Handle nil values explicitly
return 0 if @value.nil? && other.value.nil?
return -1 if @value.nil?
return 1 if other.value.nil?
@value <=> other.value
end
protected
attr_reader :value
end
# Sorting mixed types handles nil gracefully
items = [RobustComparison.new(3), RobustComparison.new(nil), RobustComparison.new(1)]
sorted = items.sort # Works without exceptions
Inheritance hierarchies require careful comparison design to maintain the Liskov substitution principle. Subclasses should remain comparable with their parent classes while extending comparison behavior appropriately.
class Animal
include Comparable
def initialize(species, age)
@species = species
@age = age
end
def <=>(other)
return nil unless other.is_a?(Animal)
# Compare by species first, then age
result = @species <=> other.species
result == 0 ? @age <=> other.age : result
end
protected
attr_reader :species, :age
end
class Dog < Animal
def initialize(breed, age)
super("Dog", age)
@breed = breed
end
def <=>(other)
# Dogs compare with any Animal
if other.is_a?(Dog)
# Compare dogs by breed, then age
result = @breed <=> other.breed
result == 0 ? super : result
else
super # Use Animal comparison
end
end
protected
attr_reader :breed
end
Reference
Core Comparison Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#<=>(other) |
other (Object) | Integer, nil | Returns -1, 0, 1 for less, equal, greater; nil for incomparable |
#==(other) |
other (Object) | Boolean | Value equality with type coercion for numerics |
#eql?(other) |
other (Object) | Boolean | Strict equality without type coercion, used by Hash |
#equal?(other) |
other (Object) | Boolean | Object identity comparison, should not be overridden |
#hash |
none | Integer | Hash code for use in Hash tables, must be consistent with eql? |
Comparable Module Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#<(other) |
other (Object) | Boolean | Less than comparison via <=> |
#<=(other) |
other (Object) | Boolean | Less than or equal comparison via <=> |
#>(other) |
other (Object) | Boolean | Greater than comparison via <=> |
#>=(other) |
other (Object) | Boolean | Greater than or equal comparison via <=> |
#between?(min, max) |
min, max (Objects) | Boolean | Tests if object falls between min and max inclusive |
#clamp(min, max) |
min, max (Objects) | Object | Returns self constrained to the range min..max |
Hash and Equality Invariants
Rule | Description | Example |
---|---|---|
Hash Consistency | a.eql?(b) implies a.hash == b.hash |
Two equal strings must have same hash |
Hash Immutability | Hash value should not change while object is in Hash | Use immutable fields for hash calculation |
Equality Symmetry | a == b implies b == a |
Must be true in both directions |
Equality Transitivity | a == b && b == c implies a == c |
Equality forms equivalence classes |
Comparison Antisymmetry | (a <=> b) == -(b <=> a) |
Comparison operators are opposites |
Type Coercion Behavior
Method | Numeric Coercion | String Coercion | Other Types |
---|---|---|---|
== |
Yes (1 == 1.0) | No ("1" != 1) | Class-specific |
eql? |
No (1.eql?(1.0) = false) | No | No |
<=> |
Class-specific | No | Class-specific |
Common Return Values
Comparison Result | <=> Return | Boolean Methods |
---|---|---|
Left < Right | -1 | < returns true |
Left == Right | 0 | == returns true |
Left > Right | 1 | > returns true |
Incomparable | nil | Raises ArgumentError |
Implementation Checklist
Component | Required When | Notes |
---|---|---|
<=> method |
Using Comparable | Must return -1, 0, 1, or nil |
== method |
Custom equality | Should be symmetric and transitive |
eql? method |
Used as Hash key | Must be consistent with hash |
hash method |
Used as Hash key | Must remain constant while in Hash |
Type checking | Always | Prevent errors with incompatible types |
Nil handling | When applicable | Decide behavior for nil comparisons |