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
- The spaceship operator <=> and what it returns
- Including Comparable to derive all operators
- Getting between? and clamp for free
- Sorting custom objects with sort and sort_by
- Using min and max on your own classes
- How Comparable differs from Enumerable
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 >= a2️⃣ 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_s3️⃣ 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)
# trueCommon Errors (and the fix)
- "comparison of X with Y failed" — ❌ Your <=> returned nil. ✅ Make sure it returns -1, 0, or 1 for the types you compare.
- "undefined method '<' " — ❌ You forgot include Comparable. ✅ Add it; <=> alone doesn't create <.
- Strings sort numbers wrongly — ❌ Comparing "2.10" vs "2.1" as text. ✅ Compare an Array of integers instead.
- max returns the smallest — ❌ You reversed the logic inside <=>. ✅ Keep <=> ascending; reverse at the call site.
- == unexpectedly false — ❌ You overrode eql? but not <=>. ✅ Comparable's == follows <=> == 0.
Pro Tips
- 💡 Delegate to an Array: [a, b] <=> [other.a, other.b] compares multiple fields in priority order.
- 💡 Store comparable units: money in cents, durations in seconds — integers compare cleanly without float drift.
- 💡 Pair with Enumerable: a collection class with Enumerable elements that include Comparable gives you sortable, searchable data for free.
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 write | You get free |
|---|---|
| def <=>(other) | < <= == > >= |
| include Comparable | between?(lo, hi) |
| (returns -1/0/1) | clamp(lo, hi) |
| elements comparable | array.sort |
| elements comparable | array.min / array.max |
| sort on any key | array.sort_by(&:key) |
🎉 Lesson Complete!
- ✅ <=> returns -1, 0, or 1 (or nil when uncomparable)
- ✅ include Comparable derives all operators from it
- ✅ between? and clamp arrive for free
- ✅ sort, min, and max all use <=>
- ✅ Comparable orders one object; Enumerable traverses a collection
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
- Previous: Method Arguments (Keyword, Splat, Block)
- Next: Refinements — Safely scope monkey-patches with refine and using
- Quick reference: Ruby cheat sheet
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.