CrackedRuby logo

CrackedRuby

Comparison and Equality Operators

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

The == Operator

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

The != Operator

The != operator is the negation of == and returns true when values are not equal.

5 != 3          # => true
5 != 5          # => false
"a" != "A"      # => true

The === Operator (Case Equality)

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"

The eql? Method

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"

The equal? Method

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

Case Equality (===) Behavior by Class

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.