Comparable and Enumerable in Ruby

· 7 min read · Updated March 30, 2026 · beginner
ruby modules comparable enumerable

Ruby gives you two powerful modules for free: Comparable and Enumerable. They let you add comparison operations and iteration methods to any class you write, with almost no code. Understanding how they work will make you a better Ruby developer, whether you’re building custom objects or working with the built-in collections.

What Comparable Gives You

Comparable is a mixin module that adds comparison operators to a class. Once you include it, you get <, <=, >, >=, ==, between?, and clamp for free. The only thing you have to implement yourself is the spaceship operator, <=>.

The spaceship operator is the backbone of comparison in Ruby. It takes two values and returns:

  • -1 when the left side is smaller
  • 0 when both sides are equal
  • 1 when the left side is larger
  • nil when the values cannot be compared
5 <=> 3    # => 1
5 <=> 5    # => 0
3 <=> 5    # => -1
"apple" <=> "banana"  # => -1

Notice that strings compare alphabetically. Ruby core classes like Numeric, String, and Time already include Comparable, which is why you can compare them directly.

Implementing the Spaceship Operator

To make your own objects comparable, include Comparable and define the <=> method. Everything else follows automatically.

Here is a Temperature class that compares by degrees:

class Temperature
  include Comparable

  attr_reader :celsius

  def initialize(celsius)
    @celsius = celsius
  end

  def <=>(other)
    return nil unless other.is_a?(Temperature)
    celsius <=> other.celsius
  end

  def to_s
    "#{celsius}°C"
  end
end

t1 = Temperature.new(20)
t2 = Temperature.new(30)
t1 < t2            # => true
t2 >= Temperature.new(25)  # => true
t1.between?(Temperature.new(10), Temperature.new(25))  # => true
t1.clamp(Temperature.new(0), Temperature.new(50))      # => 20°C

The <=> method must return exactly -1, 0, 1, or nil. Returning a boolean will break everything. Also, returning nil for incompatible types is the standard way to signal that comparison is not possible — Ruby handles it gracefully.

The Enumerable Module

Enumerable is where Ruby iteration gets its real power. It provides dozens of methods — map, select, find, reject, inject, sort, and many more — as long as your class defines an each method.

That is the only requirement. Define each, and you get everything else for free.

class SquareSequence
  include Enumerable

  def initialize(count)
    @count = count
  end

  def each
    @count.times { |n| yield (n + 1) ** 2 }
  end
end

squares = SquareSequence.new(5)
squares.each { |s| puts s }
# 1
# 4
# 9
# 16
# 25

squares.map { |s| s * 2 }      # => [2, 8, 18, 32, 50]
squares.select { |s| s > 10 } # => [16, 25]
squares.find { |s| s > 20 }    # => 25
squares.reject { |s| s.even? } # => [1, 9, 25]
squares.first(2)               # => [1, 4]

Because each yields values one at a time, every Enumerable method works. map builds a new array by transforming each element. select keeps only the elements that return true. find returns the first match. reject does the opposite of select. The list goes on.

Ruby core classes that include Enumerable are Array, Hash, Range, Enumerator, and Set. They all define each, which is why they all share this common interface.

Filtering and Searching

These are the methods you will reach for most often:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

numbers.select { |n| n.even? }    # => [2, 4, 6, 8, 10]
numbers.reject { |n| n.even? }    # => [1, 3, 5, 7, 9]
numbers.find { |n| n > 7 }       # => 8
numbers.any? { |n| n > 10 }      # => false
numbers.all? { |n| n > 0 }       # => true
numbers.none? { |n| n == 0 }     # => true
numbers.count { |n| n.even? }    # => 5

any?, all?, none?, and count are particularly useful for boolean checks. They short-circuit where possible — any? stops as soon as it finds a truthy result, so they are efficient even on large collections.

Transformation with map and flatten

map transforms each element and always returns an array. When you chain operations that also return arrays, you often end up with nested arrays. flatten solves that:

words = ["hello", "world"]
words.map { |w| w.chars }              # => [["h", "e", "l", "l", "o"], ["w", "o", "r", "l", "d"]]
words.map { |w| w.chars }.flatten      # => ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"]

Ruby also provides flat_map as a shortcut for this common pattern:

words.flat_map { |w| w.chars }         # => ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"]

Aggregation with inject

inject (also called reduce) accumulates a running value across all elements. It takes an initial value and a block, or just a symbol:

[1, 2, 3, 4, 5].inject(0) { |sum, n| sum + n }  # => 15
[1, 2, 3, 4, 5].inject(:*)                       # => 120

This is especially handy for building hashes or aggregating data.

Combining Comparable and Enumerable

The real pattern that shows up in real Ruby codebases is combining both modules in the same class. Define each for Enumerable and <=> for Comparable, and you get sorting, searching, and comparison in one package.

A PlayingCard class shows this well:

class PlayingCard
  RANKS = %w[2 3 4 5 6 7 8 9 10 J Q K A].freeze
  SUITS = %w[clubs diamonds hearts spades].freeze

  include Comparable

  attr_reader :rank, :suit

  def initialize(rank, suit)
    @rank = RANKS.index(rank)
    @suit = SUITS.index(suit)
  end

  def <=>(other)
    return nil unless other.is_a?(PlayingCard)
    result = rank <=> other.rank
    result.zero? ? suit <=> other.suit : result
  end

  def to_s
    "#{RANKS[@rank]} of #{SUITS[@suit]}"
  end
end

class CardDeck
  include Enumerable

  def initialize
    @cards = []
    RANKS.each do |rank|
      SUITS.each do |suit|
        @cards << PlayingCard.new(rank, suit)
      end
    end
  end

  def each(&block)
    @cards.each(&block)
  end

  def shuffle!
    @cards.shuffle!
  end
end

deck = CardDeck.new
deck.shuffle!

# Sort uses Comparable on PlayingCard
sorted = deck.sort
sorted.first   # => 2 of clubs
sorted.last    # => A of spades

# Enumerable methods work on the deck
deck.find { |c| c.to_s == "A of spades" }  # => A of spades
deck.select { |c| c.suit == 2 }.count     # => 13 (hearts)

Without <=>, sort would not know how to order cards. Without each, none of the Enumerable methods would work. Together they give the deck a rich interface with almost no boilerplate.

A Few Gotchas Worth Knowing

Floats and the Spaceship Operator

Floating-point arithmetic introduces precision errors that affect comparison:

(0.1 + 0.2) <=> 0.3  # => nil

This returns nil because 0.1 + 0.2 produces 0.30000000000000004, which Ruby correctly identifies as incomparable to 0.3. For float comparisons in production code, use a tolerance:

def roughly_equal?(a, b, epsilon: 1e-9)
  (a - b).abs < epsilon
end

sort vs sort_by

Both sort a collection, but sort calls <=> on every pair of elements, while sort_by calls the block once per element and then sorts on those computed values. For expensive comparisons, sort_by is significantly faster:

# Calling to_s on every comparison — wasteful
deck.sort { |a, b| a.to_s <=> b.to_s }

# Calls to_s once per card, then sorts those values
deck.sort_by { |c| c.to_s }

The between? Gotcha

between? is inclusive on both ends. There is no exclusive version built in. If you need to check exclusive bounds, use manual comparison:

5.between?(1, 10)  # => true
5.between?(5, 5)   # => true — endpoints included

clamp Is Not the Same as between?

clamp caps a value to a range, while between? checks whether a value falls within a range. This distinction matters:

age = 75
age.between?(0, 18)   # => false
age.clamp(0, 18)      # => 18 — clamps to the maximum

See Also