rubyguides

Comparable and Enumerable in Ruby

Ruby’s Comparable and Enumerable modules are two of the most useful mixins in the language. With Comparable, your objects gain sorting and range checks from a single <=> method. With Enumerable, your collections get dozens of iteration methods from a single each definition. Understanding how these two modules work will make you a better Ruby developer, whether you’re building custom objects or working with the built-in collections.

TL;DR

Comparable gives your objects a single comparison rule through <=>, and Enumerable gives your collections a rich set of iteration methods once you define each. Use them together when a class should be both sortable and iterable. The examples below start with the modules separately, then show how they combine in a real class.

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.

The important thing to notice is that Comparable does not invent an ordering for you. It asks you to define the ordering once, then lets Ruby use that rule everywhere else. That keeps your class easy to reason about and avoids scattering comparison logic across the codebase.

Implementing the spaceship operator

To make your own objects comparable, include Comparable and define the <=> method. Everything else follows automatically. The spaceship operator must return exactly -1, 0, 1, or nil. Returning a boolean or any other value will cause the derived comparison methods to behave unpredictably. Returning nil for incompatible types is the standard Ruby convention for signaling that two objects cannot be compared, and the framework handles that gracefully without raising errors.

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.

That contract is what makes the rest of Comparable useful. Once the spaceship operator is in place, Ruby can derive the rest of the comparison methods from it. If the ordering changes later, there is one place to update and one place to review.

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.

That shared interface is one of the cleanest parts of Ruby’s design. A class only needs to produce items one at a time, and the standard library can do the rest. That keeps collection code small while still giving you plenty of expressive methods.

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.

These methods are usually the clearest way to ask a collection a yes-or-no question. They read like intent, not like plumbing, and they help the code stay small even when the collection itself is large.

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"]

flat_map is a good shorthand when every item expands into a list and you want to merge those lists right away. It keeps the intent in one expression and avoids a separate flatten call later.

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.

inject shines when the final result is not itself a collection. You can use it to add up values, build a string, or accumulate state in a single pass. Once that pattern is familiar, many small loops become easier to replace with one expressive line.

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. The card stores its rank and suit as numeric indices so they sort naturally. The spaceship operator compares rank first, then falls back to suit for ties:

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

The CardDeck class wraps an array of cards and includes Enumerable by delegating each to the internal array. That single delegation gives the deck access to sort, find, select, and every other method Enumerable provides. The deck does not need any comparison logic of its own because sort calls <=> on each PlayingCard object, which already knows how to order itself.

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. The combination is the real win: one small comparison rule and one iterator open up a lot of behavior. A class like PlayingCard becomes easy to sort, search, and inspect without needing a lot of extra helper methods.

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

The point here is not that Comparable is unreliable. It is that your comparison rule needs to match the data. Floats are often a bad fit for exact ordering when the real question is “close enough”. A tolerance keeps the code honest about that difference.

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 }

sort is fine when the comparison itself is cheap and already defined on the object. sort_by is usually better when you need to derive a key first. The distinction matters because it keeps the code faster and makes the intent clearer to the next reader.

When the sorting key is derived from a string, a timestamp, or a nested field, sort_by also keeps the code easier to scan. You can see the key extraction in one place instead of repeating comparison logic in several branches.

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

That inclusion is useful when you want a clean range check, but it can surprise you if you are thinking in half-open intervals. Ruby keeps the method simple, so the caller has to decide whether the endpoints should count.

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

between? asks a question, while clamp changes the value to fit the range. That is the practical difference to keep in mind. If you only need to test the value, use between?. If you want to limit the value, use clamp.

The two methods often show up together in validation code. A form field might check between? to decide whether to show an error, then use clamp to keep an internal score or index within safe bounds.

Closing note

The real value of these two modules is that they make custom classes behave like native Ruby objects. Comparable gives you sorting and range checks. Enumerable gives you a whole toolbox for iterating. When you include both in a class that represents a collection or a sortable value, the rest of Ruby starts working with that class naturally.

See Also