CrackedRuby logo

CrackedRuby

Comparison Methods

Ruby's comparison methods enable objects to be compared for equality, ordering, and identity through a consistent API used by sorting, searching, and collection operations.

Core Modules Comparable Module
3.3.2

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