Overview
Ruby provides several operators for comparing values and determining equality. These operators form the foundation of conditional logic, sorting, and many other programming tasks. Ruby's comparison system includes equality operators (==
, !=
, ===
, eql?
, equal?
), relational operators (<
, >
, <=
, >=
), and the spaceship operator (<=>
).
The comparison operators in Ruby work with Ruby's dynamic typing system and can be overridden in custom classes. Most built-in classes implement these operators with sensible default behavior, but understanding the subtle differences between them is crucial for writing correct Ruby code.
# Basic equality comparison
"hello" == "hello" # => true
5 == 5.0 # => true (type coercion)
# Object identity comparison
str1 = "hello"
str2 = "hello"
str1.equal?(str2) # => false (different objects)
# Spaceship operator for sorting
[3, 1, 4, 1, 5].sort # Uses <=> internally
# => [1, 1, 3, 4, 5]
Basic Equality Operators
==
Operator
The The ==
operator checks for value equality and is the most commonly used equality operator. It performs type coercion when appropriate and can be overridden in custom classes.
# Numeric comparisons with type coercion
42 == 42.0 # => true
42 == "42" # => false (no automatic string conversion)
# String comparisons
"Ruby" == "Ruby" # => true
"Ruby" == "ruby" # => false (case sensitive)
# Array comparisons (element-wise)
[1, 2, 3] == [1, 2, 3] # => true
[1, 2, 3] == [1, 2, "3"] # => false
!=
Operator
The The !=
operator is the negation of ==
and returns true
when values are not equal.
5 != 3 # => true
5 != 5 # => false
"a" != "A" # => true
===
Operator (Case Equality)
The The ===
operator is used for case equality and is primarily used internally by case
statements. Its behavior varies significantly between different classes.
# Range case equality
(1..10) === 5 # => true
(1..10) === 15 # => false
# Class case equality
String === "hello" # => true
Integer === 42 # => true
Integer === "42" # => false
# Regex case equality
/\d+/ === "123" # => true
/\d+/ === "abc" # => false
# In case statements
case "hello"
when String then "It's a string"
when Integer then "It's an integer"
end
# => "It's a string"
eql?
Method
The The eql?
method checks for both value and type equality. It's stricter than ==
and doesn't perform type coercion.
1.eql?(1) # => true
1.eql?(1.0) # => false (different types)
"a".eql?("a") # => true
# Used internally by Hash for key comparison
hash = { 1 => "integer", 1.0 => "float" }
hash[1] # => "integer"
hash[1.0] # => "float"
equal?
Method
The The equal?
method checks for object identity - whether two variables reference the exact same object in memory.
str1 = "hello"
str2 = "hello"
str3 = str1
str1.equal?(str2) # => false (different objects)
str1.equal?(str3) # => true (same object)
# Integers and symbols are special cases
1.equal?(1) # => true (same object)
:symbol.equal?(:symbol) # => true (same object)
Relational Operators
Basic Relational Operators
Ruby provides the standard set of relational operators for comparing values.
# Numeric comparisons
5 > 3 # => true
5 < 3 # => false
5 >= 5 # => true
5 <= 4 # => false
# String comparisons (lexicographic)
"apple" < "banana" # => true
"Apple" < "apple" # => true (uppercase comes first)
# Array comparisons (element-wise)
[1, 2] < [1, 3] # => true
[1, 2] < [1, 2, 3] # => true (shorter array is less)
<=>
)
The Spaceship Operator (The spaceship operator returns -1
, 0
, or 1
depending on whether the left operand is less than, equal to, or greater than the right operand. It returns nil
if the values can't be compared.
1 <=> 2 # => -1
2 <=> 2 # => 0
3 <=> 2 # => 1
"a" <=> 5 # => nil (incomparable)
# Used for sorting
numbers = [3, 1, 4, 1, 5, 9]
numbers.sort { |a, b| a <=> b } # => [1, 1, 3, 4, 5, 9]
# Custom sorting with spaceship operator
words = ["apple", "Banana", "cherry"]
words.sort { |a, b| a.downcase <=> b.downcase }
# => ["apple", "Banana", "cherry"]
Advanced Usage Patterns
Custom Class Implementations
When creating custom classes, you often need to implement comparison operators. The Comparable
module can help by providing all comparison operators if you implement <=>
.
class Version
include Comparable
attr_reader :major, :minor, :patch
def initialize(version_string)
@major, @minor, @patch = version_string.split('.').map(&:to_i)
end
def <=>(other)
return nil unless other.is_a?(Version)
[major, minor, patch] <=> [other.major, other.minor, other.patch]
end
def ==(other)
return false unless other.is_a?(Version)
major == other.major && minor == other.minor && patch == other.patch
end
def to_s
"#{major}.#{minor}.#{patch}"
end
end
v1 = Version.new("1.2.3")
v2 = Version.new("1.2.4")
v3 = Version.new("1.2.3")
v1 < v2 # => true
v1 == v3 # => true
v1.eql?(v3) # => false (different objects)
[v2, v1, v3].sort # Uses <=> automatically
Comparison with nil
Comparing with nil
requires careful handling as it can lead to unexpected results or errors.
# Safe nil comparisons
nil == nil # => true
nil != "something" # => true
# Relational operators with nil raise errors
begin
5 > nil
rescue ArgumentError => e
puts e.message # => comparison of Integer with nil failed
end
# Safe nil comparison patterns
def safe_compare(a, b)
return 0 if a.nil? && b.nil?
return -1 if a.nil?
return 1 if b.nil?
a <=> b
end
safe_compare(5, nil) # => 1
safe_compare(nil, 5) # => -1
safe_compare(nil, nil) # => 0
Chained Comparisons and Complex Logic
Ruby's comparison operators can be chained and combined with logical operators for complex conditions.
# Chained comparisons
score = 85
grade = if score >= 90
'A'
elsif score >= 80
'B'
elsif score >= 70
'C'
else
'F'
end
# Complex comparison logic
def categorize_number(n)
case
when n > 0 && n < 10
"single digit positive"
when n >= 10 && n < 100
"double digit positive"
when n == 0
"zero"
when n < 0
"negative"
else
"large positive"
end
end
# Using comparison in functional programming
numbers = [1, -5, 0, 10, 3, -2]
positive = numbers.select { |n| n > 0 }
sorted_abs = numbers.sort_by { |n| n.abs }
Common Pitfalls and Edge Cases
Type Coercion Surprises
Ruby's type coercion in comparisons can sometimes produce unexpected results.
# Numeric type coercion works
1 == 1.0 # => true
1.eql?(1.0) # => false
# String to number coercion doesn't happen automatically
"10" == 10 # => false
"10".to_i == 10 # => true
# Be careful with mixed-type arrays
mixed = [1, "2", 3.0]
# This will raise an error because strings can't be compared to numbers
# mixed.sort # => ArgumentError
Floating Point Precision Issues
Floating point comparisons can be problematic due to precision limitations.
# Floating point precision problems
0.1 + 0.2 == 0.3 # => false
0.1 + 0.2 # => 0.30000000000000004
# Safe floating point comparison
def float_equal?(a, b, epsilon = 1e-10)
(a - b).abs < epsilon
end
float_equal?(0.1 + 0.2, 0.3) # => true
# Using rational numbers for exact arithmetic
require 'rational'
(Rational(1, 10) + Rational(2, 10)) == Rational(3, 10) # => true
Hash Key Comparison Gotchas
Understanding how hashes use comparison for keys is crucial.
# Hash uses eql? and hash for key comparison
hash = {}
hash[1] = "integer"
hash[1.0] = "float"
hash.length # => 2 (different keys because 1.eql?(1.0) is false)
# String keys and symbol keys are different
hash["key"] = "string key"
hash[:key] = "symbol key"
hash.length # => 4
# Mutable objects as keys can cause issues
array_key = [1, 2]
hash[array_key] = "array value"
array_key << 3 # Mutating the key
hash[[1, 2, 3]] # => nil (key not found because hash changed)
Comparison Operator Overriding Issues
When overriding comparison operators, maintaining consistency is important.
class BadComparison
attr_reader :value
def initialize(value)
@value = value
end
# Inconsistent implementation - breaks expected behavior
def ==(other)
value == other.value
end
def <=>(other)
# Inconsistent with ==
value.to_s <=> other.value.to_s
end
end
# This can lead to confusing behavior
a = BadComparison.new(10)
b = BadComparison.new(2)
a == b # => false (comparing numbers)
a > b # => false (comparing strings "10" < "2")
Reference
Equality Operators Quick Reference
Operator | Method | Description | Type Coercion |
---|---|---|---|
== |
#== |
Value equality | Yes |
!= |
#!= |
Value inequality | Yes |
=== |
#=== |
Case equality | Varies by class |
eql? |
#eql? |
Value and type equality | No |
equal? |
#equal? |
Object identity | No |
Relational Operators Quick Reference
Operator | Method | Description | Returns |
---|---|---|---|
< |
#< |
Less than | Boolean |
<= |
#<= |
Less than or equal | Boolean |
> |
#> |
Greater than | Boolean |
>= |
#>= |
Greater than or equal | Boolean |
<=> |
#<=> |
Spaceship operator | -1, 0, 1, or nil |
<=>
Common Return Values for Comparison | Return Value | Meaning |
---|---|---|
a < b |
-1 |
a is less than b |
a == b |
0 |
a equals b |
a > b |
1 |
a is greater than b |
Incomparable | nil |
Cannot compare a and b |
===
) Behavior by Class
Case Equality (Class | Behavior | Example |
---|---|---|
Class |
Instance check | String === "hello" |
Range |
Inclusion check | (1..10) === 5 |
Regexp |
Pattern match | /\d+/ === "123" |
Proc |
Call with argument | proc { |x| x > 0 } === 5 |
Default | Same as == |
5 === 5 |
Implementing Comparable Module
To make a class fully comparable, implement <=>
and include Comparable
:
class MyClass
include Comparable
def <=>(other)
# Return -1, 0, 1, or nil
end
end
This automatically provides: <
, <=
, >
, >=
, and between?
methods.