rubyguides

The Comparable Module in Ruby: Sorting and Comparison Made Simple

The Comparable module is one of Ruby’s most useful built-in modules. It lets you compare objects using familiar operators like <, >, and ==, even when your class is not one of Ruby’s built-in types. In this guide, you’ll learn how to include Comparable in your classes, define the spaceship operator, and make your objects work naturally with sorting, minimum and maximum checks, and range-style comparisons.

Comparable is a good fit whenever one attribute can act as the primary ordering rule. A score, timestamp, name, or priority value can all work, as long as the comparison is stable and easy to explain. If the ordering rule is unclear, the rest of the code becomes harder to trust, so the first step is always deciding what “smaller” or “greater” should mean for your object.

intro context

Comparable is easiest to understand when you see it as a small contract between your class and Ruby itself. You tell Ruby how two objects should be ordered, then Ruby reuses that answer for the standard comparison operators. If you want a broader tour of comparison and iteration tools, Comparable and Enumerable in Ruby shows how this pattern fits into larger Ruby code. If you want to look at the method lookup side of the story, Ruby introspection explains how Ruby walks a class hierarchy and finds behavior.

The official Ruby Comparable module documentation lists every operator that the module provides after you define <=>.

Comparable module basics

When you include the Comparable module in a class and define the <=> (spaceship) operator, Ruby automatically gives you all these methods for free:

  • < (less than)
  • <= (less than or equal)
  • > (greater than)
  • >= (greater than or equal)
  • == (equal)
  • between? (range check)

This is useful for sorting, finding minimum and maximum values, and any scenario where you need to compare objects without writing a separate helper for each operator. Once the comparison rule exists, the rest of the class gets a consistent set of methods for free.

One important detail is that Comparable does not create an ordering out of thin air. It simply repeats the logic you define in <=>. That means your ordering logic should be straightforward, consistent, and grounded in data that every instance carries. If the rule is easy to describe in a sentence, it is usually a good candidate for Comparable.

For example, a score-based ranking is easy to compare because every player has a score and higher scores mean a clear result. A more ambiguous rule, like comparing by the length of a name and then by a hidden status flag, becomes harder to explain and harder to maintain. Keep the rule obvious, and the rest of Comparable stays predictable.

How to include comparable in your class

Here’s a simple example using a Player class for a game:

The first step is choosing the value that should drive the ordering. In the example below, score is the right choice because the class represents a game result and the score already has a natural direction. Once that choice is clear, the code for <=> stays very small.

class Player
  include Comparable

  attr_reader :name, :score

  def initialize(name, score)
    @name = name
    @score = score
  end

  def <=>(other)
    score <=> other.score
  end

  def to_s
    "#{name}: #{score}"
  end
end

The key is defining the <=> method, which returns:

  • -1 when self < other
  • 0 when self == other
  • 1 when self > other
  • nil when comparison isn’t possible

That return value is the contract Comparable relies on. Ruby does not guess how your class should behave, it just takes the result from <=> and builds the other comparison methods around it. That means the implementation should be predictable, symmetric, and based on data that can actually be ordered.

Before you write the class, it helps to decide what the “other” object should look like. In this example, both objects need a score, so comparing one player to another is safe. If your class can be compared to several different kinds of objects, be strict about what you accept and return nil when the input does not fit the ordering rule.

What the spaceship operator should compare

Most Comparable classes use one main attribute as the ordering key. In the Player example, score is the most natural choice because the class represents a score-driven object. If you compare by name instead, sorting would still work, but the result would reflect alphabetical order instead of game ranking.

When you choose the comparison field, keep these points in mind:

# Good: single clear attribute
def <=>(other); score <=> other.score; end

# Avoid: ambiguous comparison across unrelated fields
def <=>(other)
  [name.length, created_at] <=> [other.name.length, other.created_at]
end

When you choose the comparison field, keep these points in mind:

  • Use a value that is present on every instance
  • Prefer a value that already has a natural order
  • Keep the rule simple enough that another reader can predict the result
  • Return nil only when the objects cannot be compared safely

If a class can be compared in more than one meaningful way, pick the one that best matches the main job of the object. Secondary ordering rules can still be handled later with custom sort blocks when needed.

How the comparison methods work

Once Comparable is included, you get all the comparison operators:

player1 = Player.new("Alice", 1500)
player2 = Player.new("Bob", 2000)
player3 = Player.new("Charlie", 1500)

player1 < player2          # => true
player2 > player1          # => true
player1 <= player3         # => true (same score)
player1 == player3         # => true (same score)
player1 >= player2         # => false
player2.between?(player1, player3)  # => false

The benefit here is consistency. You define one comparison method, and Ruby gives you a whole family of operators that all follow the same rule. That makes the class easier to use in arrays, ranges, and conditional checks because the rest of Ruby can rely on the same ordering logic.

It also means you can keep your application code short. Instead of writing separate helper methods for less_than?, greater_than?, and within_range?, you can use the built-in operators directly. That saves repetition and keeps the ordering rule in one place.

Practical examples

Sorting players

players = [
  Player.new("Alice", 1500),
  Player.new("Bob", 2000),
  Player.new("Charlie", 1800)
]

players.sort.each { |p| puts p }
# Output:
# Alice: 1500
# Charlie: 1800
# Bob: 2000

Sorting is often the first place Comparable pays off, but it is not the only one. The same definition also helps with min, max, and sort_by-style workflows that need a stable ordering rule. That means one well-written <=> method can support several different parts of the codebase.

Finding min and max

players = [
  Player.new("Alice", 1500),
  Player.new("Bob", 2000),
  Player.new("Charlie", 1800)
]

players.min    # => #<Player:0x00007f8a2c3d4 @name="Alice", @score=1500>
players.max    # => #<Player:0x00007f8a2c3e5 @name="Bob", @score=2000>

Using comparable with core classes

Ruby’s core classes already include Comparable. That is why expressions like "apple" < "banana" and 5.between?(1, 10) work without any extra setup. Those classes already know how to compare themselves, so you can use the same operators on them that you would use on your own objects.

# Strings are compared lexicographically
"apple" < "banana"    # => true
"zebra" > "apple"     # => true

# Numbers work as expected
5.between?(1, 10)    # => true
3.5.between?(3, 4)   # => false

# Arrays are compared element by element
[1, 2, 3] < [1, 2, 4]    # => true
[1, 2, 3] == [1, 2, 3]   # => true

How to define multiple comparison criteria

Sometimes a single value is not enough. A team might need to sort by wins first and then by losses, or a task list might need priority first and due date second. In those cases, the <=> method can combine values step by step so the class still has one clear ordering rule.

class Team
  include Comparable

  attr_reader :name, :wins, :losses

  def initialize(name, wins, losses)
    @name = name
    @wins = wins
    @losses = losses
  end

  def <=>(other)
    # First compare by wins, then by losses (fewer is better)
    result = other.wins <=> wins
    result.zero? ? losses <=> other.losses : result
  end
end

teams = [
  Team.new("Lions", 10, 4),
  Team.new("Tigers", 10, 3),
  Team.new("Bears", 8, 6)
]

teams.sort.each { |t| puts "#{t.name}: #{t.wins}-#{t.losses}" }
# Output:
# Tigers: 10-3
# Lions: 10-4
# Bears: 8-6

The important part is that the comparison remains deterministic. If two teams tie on wins, the second field should always decide the result in the same way. That keeps sorting predictable and prevents surprises when the array order changes between runs.

Common mistakes

Forgetting to include comparable

# Without Comparable - won't work!
class BadExample
  def initialize(value)
    @value = value
  end

  def <=>(other)
    @value <=> other.value
  end
end

a = BadExample.new(5)
b = BadExample.new(10)
a < b  # => NoMethodError: undefined method `<'

Returning nil incorrectly

Always return -1, 0, or 1 for valid comparisons:

Use nil only when the two objects truly do not belong to the same ordering system. For ordinary values, a direct comparison keeps the code easier to read and easier to debug. That way callers can trust <=> to behave like a real ordering rule instead of a special-case branch that appears only for a few inputs.

def <=>(other)
  return nil if other.nil?  # Handle nil case explicitly
  @value <=> other.value
end

If you return nil, Ruby treats the comparison as unavailable. That can be useful when the objects are not really comparable, but it should not be the default for ordinary values. For ordinary ordering, make sure the method always reaches a real comparison result.

The two examples above show opposite ends of the spectrum. In the first case, a small number of objects share one obvious ordering rule. In the second, the class needs a fallback path when the input is not comparable at all. Both patterns are valid, but they solve different problems, so it helps to choose the one that matches the data you are actually modeling.

Summary

# Quick reference: Comparable in one snippet
class Item
  include Comparable
  def initialize(priority); @priority = priority; end
  attr_reader :priority
  def <=>(other); priority <=> other.priority; end
end

Item.new(1) < Item.new(2)  # => true

The Comparable module is a powerful way to add comparison functionality to your classes. It keeps the ordering logic in one place, then lets Ruby reuse that logic across the standard comparison operators.

  1. Include Comparable in your class
  2. Define the <=> method returning -1, 0, or 1
  3. Get free comparison methods: <, <=>, >, >=, between?

This pattern is used throughout Ruby’s standard library. The String, Numeric, Array, and Hash classes all include Comparable. Now you can make your own classes work just as well with sorting, min/max operations, comparison operators, and any code that expects the Comparable module to give objects a clear order.

Once your class has a clean ordering rule, the next step is usually to use it in the rest of your codebase. The companion guide on Comparable and Enumerable in Ruby shows how this module pairs with collection traversal. If you want a second example of Ruby introspection in practice, Ruby introspection shows how Ruby locates methods and mixins when it evaluates an object.