The Comparable Module

Reviewed & published by Brayan K

Comparable is a Ruby mixin that turns a single <=> method you write into the full set of comparison operators — <, <=, ==, >, >=, plus between? and clamp.

Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll make your own classes orderable and sortable by defining just one method.

What You'll Learn in This Lesson

1️⃣ Define <=> and include Comparable

The spaceship operator <=> answers one question: is the left object less than (-1), equal to (0), or greater than (1) the right? Define it, include Comparable, and every comparison operator appears for free. A neat trick is to delegate to an Array of fields, whose own <=> compares element by element.

# Define <=> (the "spaceship operator") and include Comparable.
# <=> must return -1 (less), 0 (equal), or 1 (greater).
class Version
  include Comparable
  attr_reader :major, :minor

  def initialize(str)
    @major, @minor = str.split(".").map(&:to_i)
  end

  def <=>(other)
    [major, minor] <=> [other.major, other.minor]   # Array does the work
  end

  def to_s = "#{major}.#{minor}"
end

a = Version.new("2.1")
b = Version.new("2.10")

# All of these come FREE from Comparable:
puts a < b
puts a > b
puts a == Version.new("2.1")
puts a <= b
puts b >= a

2️⃣ between? and clamp Come Free

Beyond the operators, Comparable hands you two practical helpers. between?(low, high) checks whether an object falls inside a range, and clamp(low, high) pulls an out-of-range value back to the nearest boundary — perfect for limiting volume, brightness, or a temperature reading.

# Comparable also gives you between? and clamp for free.
class Temp
  include Comparable
  attr_reader :degrees
  def initialize(degrees) = @degrees = degrees
  def <=>(other) = degrees <=> other.degrees
  def to_s = "#{degrees}°"
end

reading = Temp.new(25)
low  = Temp.new(18)
high = Temp.new(30)

puts reading.between?(low, high)          # is 25 within 18..30?
puts Temp.new(40).clamp(low, high).to_s   # pull 40 back into range -> 30
puts Temp.new(5).clamp(low, high).to_s    # pull 5 up into range  -> 18
puts [reading, low, high].min.to_s        # min uses <=> too
puts [reading, low, high].max.to_s

3️⃣ Sorting Custom Objects

Because Array#sort, min, and max all call <=>, defining it once makes your objects sortable everywhere. Keep <=> in natural ascending order for clarity, then reverse at the call site if you want descending. sort_by is often clearer still when you just want to sort on one attribute.

# Sorting custom objects: sort and sort_by both rely on <=>.
class Player
  include Comparable
  attr_reader :name, :score
  def initialize(name, score)
    @name = name
    @score = score
  end
  # Natural order: compare by score (ascending).
  def <=>(other) = score <=> other.score
  def to_s = "#{name}(#{score})"
end

players = [
  Player.new("Ada", 80),
  Player.new("Lee", 95),
  Player.new("Sam", 60),
]

# sort uses <=> directly: lowest score first.
puts players.sort.map(&:to_s).join(", ")

# Want highest first? sort, then reverse — or sort_by(&:score).reverse.
puts players.sort.reverse.map(&:to_s).join(", ")

# min/max come free from Comparable:
puts "Top: #{players.max}"     # highest score
puts "Bottom: #{players.min}"  # lowest score

🎯 Your Turn

The Book class is almost there — it just needs the comparison operator. Replace the ___ with the operator that returns -1, 0, or 1.

# 🎯 YOUR TURN — fill in the ___ so Books compare by page count.
class Book
  include Comparable
  attr_reader :title, :pages
  def initialize(title, pages)
    @title = title
    @pages = pages
  end
  def <=>(other) = pages ___ other.pages   # 👉 which operator returns -1/0/1?
  def to_s = "#{title} (#{pages}p)"
end

short = Book.new("Haiku", 40)
long  = Book.new("Saga", 900)

puts short < long
puts [long, short].min.to_s
puts long.between?(short, Book.new("Epic", 2000))

# ✅ Expected output:
#    true
#    Haiku (40p)
#    true

Common Errors (and the fix)

Pro Tips

Mini-Challenge: A Sortable Money Class

Build a Money class that stores cents, includes Comparable, and demonstrates sort, min, max, between?, and clamp. Run with ruby money.rb.

# 🎯 MINI-CHALLENGE: a Money class sortable by amount.
# Save as money.rb and run with: ruby money.rb
#
# 1. include Comparable and store the value in cents (avoid float drift).
# 2. Define <=> comparing cents.
# 3. Show sort, min, max, between?, and clamp all working.
#
# ✅ Expected output:
#    $0.99, $4.50, $4.50, $12.00
#    Cheapest: $0.99
#    Priciest: $12.00
#    In range? true
#    Clamped:  $10.00

# your code here

📋 Quick Reference — Comparable

You writeYou get free
def <=>(other)< <= == > >=
include Comparablebetween?(lo, hi)
(returns -1/0/1)clamp(lo, hi)
elements comparablearray.sort
elements comparablearray.min / array.max
sort on any keyarray.sort_by(&:key)

🎉 Lesson Complete!

Practice quiz

What must the spaceship operator <=> return?

  • true or false
  • Always an Integer 0..100
  • -1, 0, or 1 (or nil if uncomparable)
  • The two objects in an array

Answer: -1, 0, or 1 (or nil if uncomparable). <=> returns -1 (less), 0 (equal), 1 (greater), or nil when the objects can't be compared.

What single method must you define to get all comparison operators from Comparable?

  • <=>
  • ==
  • compare
  • <

Answer: <=>. Define <=> and include Comparable; the module derives <, <=, ==, >, >=, between?, and clamp.

If you define <=> but forget to include Comparable, calling obj1 < obj2 will:

  • Work fine
  • Return nil
  • Compare by object id
  • Raise NoMethodError because < isn't defined

Answer: Raise NoMethodError because < isn't defined. <=> alone doesn't create <; you must include Comparable to derive the operators.

What does [2, 1] <=> [2, 10] return?

  • 1
  • -1
  • 0
  • nil

Answer: -1. Arrays compare element by element: 2 == 2, then 1 < 10, so the result is -1.

Which TWO helper methods does Comparable provide beyond the operators?

  • between? and clamp
  • map and select
  • sort and reverse
  • min and max

Answer: between? and clamp. Comparable gives you between?(low, high) and clamp(low, high) for free.

Temp.new(40).clamp(Temp.new(18), Temp.new(30)) returns the temperature:

  • 40
  • 18
  • 30
  • nil

Answer: 30. clamp pulls an out-of-range value back to the nearest boundary; 40 clamps down to the high, 30.

Why does Array#sort work on your custom objects once they're Comparable?

  • sort reads instance variables directly
  • sort calls <=> under the hood to order elements
  • sort uses == only
  • It doesn't — you must write your own sort

Answer: sort calls <=> under the hood to order elements. sort, min, and max all rely on <=>, so defining it once makes objects sortable everywhere.

The recommended way to sort custom objects in DESCENDING order is to:

  • Reverse the logic inside <=>
  • Define a separate sort_desc method
  • Use min instead of max
  • Keep <=> ascending and reverse at the call site (e.g. array.sort.reverse)

Answer: Keep <=> ascending and reverse at the call site (e.g. array.sort.reverse). Keep <=> in natural ascending order and reverse at the call site, so max stays intuitive.

How do Comparable and Enumerable differ?

  • They are the same module under two names
  • Comparable orders one object vs another (<=>); Enumerable traverses a collection (each)
  • Comparable is for arrays only
  • Enumerable needs <=>, Comparable needs each

Answer: Comparable orders one object vs another (<=>); Enumerable traverses a collection (each). Comparable orders objects via <=>; Enumerable traverses collections via each.

Why store money as cents (integers) when making it Comparable?

  • Cents sort alphabetically
  • Comparable rejects floats
  • Integers compare cleanly without floating-point drift
  • It uses less memory than any other approach

Answer: Integers compare cleanly without floating-point drift. Comparable units like cents compare exactly, avoiding float rounding surprises.

Continue this course

Frequently asked questions

What does the spaceship operator <=> actually return?

The <=> operator returns -1 when the left object is less than the right, 0 when they're equal, and 1 when it's greater. It returns nil if the two objects can't be compared at all. Comparable is built entirely on this one method: once you define <=>, the module derives <, <=, ==, >, >=, between?, and clamp from it. Most of the time you delegate the actual comparison to an Array or a built-in type whose <=> already does the right thing.

What is the difference between Comparable and Enumerable?

They solve different problems. Comparable is about ordering a single object against another — it needs one method, <=>, and gives you the comparison operators and clamp. Enumerable is about traversing a collection of objects — it needs an each method and gives you map, select, reduce, and friends. They often team up: include Enumerable to make a class iterable, and give its elements Comparable so methods like sort and min_by know how to order them.

Why include Comparable instead of writing < and > myself?

Writing each operator by hand is repetitive and error-prone — it's easy to make < and > disagree about edge cases, or forget between?. By defining a single, authoritative <=> and including Comparable, every comparison is derived consistently from one source of truth. You also get clamp and between? for free, plus your objects slot straight into Array#sort, Array#min, and Array#max, which all call <=> under the hood.

How do I sort custom objects in descending order?

Keep <=> in its natural ascending order, then reverse the result with array.sort.reverse, or use array.sort_by(&:key).reverse. Reversing the comparison inside <=> itself works too but is surprising to read, because it makes max return what looks like the minimum. The clearest approach is to define <=> in the most intuitive ascending direction and let the call site decide the display order, so your class stays predictable everywhere it's used.