Skip to Content
Course content

62: Custom Sortable Classes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've ever tried to sort a list of custom objects using .sort, you probably ran into a ArgumentError (comparison of X with Y failed). That happens because Ruby doesn't know if your object is "bigger" or "smaller" than another one. It's not magic; we have to tell it exactly how to make that decision.

How do I actually tell Ruby how to compare two objects of my own class?

The secret is the "spaceship operator": <=>. I call it that because it looks like a UFO. This operator is the foundation of all sorting in Ruby. It expects one of three specific return values: -1 if the left side is smaller, 0 if they're equal, and 1 if the left side is larger.

Let's say we're building a system to manage software versions. If we just use strings, "1.10" would be sorted as smaller than "1.2" because strings are compared alphabetically. That's a disaster for versioning. We need a Version class that understands numbers.

class Version
  attr_reader :major, :minor

  def initialize(major, minor)
    @major = major
    @minor = minor
  end

  def <=>(other)
    # First, compare the major versions
    comparison = @major <=> other.major
    
    # If major versions are different, we have our answer.
    # If they are the same (0), we fall back to comparing the minor versions.
    return comparison == 0 ? @minor <=> other.minor : comparison
  end
end

Now, if you have two Version objects, you can call v1 <=> v2 and get a meaningful result. But you'll notice something annoying: you still can't use v1 < v2 or v1 >= v2. You'd have to write all those methods yourself... unless you use a shortcut.

Wait, do I really have to write < and > and == for every single class?

Nope. This is where the Comparable module comes in. It's one of my favorite parts of the Ruby standard library. When you include Comparable in your class and define the <=> method, Ruby automatically gives you <, >, <=, >=, and between? for free.

class Version
  include Comparable # This is the magic line
  attr_reader :major, :minor

  def initialize(major, minor)
    @major = major
    @minor = minor
  end

  def <=>(other)
    comparison = @major <=> other.major
    comparison == 0 ? @minor <=> other.minor : comparison
  end
end

v1 = Version.new(1, 2)
v2 = Version.new(1, 10)

puts v1 < v2 # true
puts v1 > v2 # false

By including Comparable, your class now "behaves" like a number or a string. Even better, you can now throw a bunch of these objects into an array and call .sort on that array, and it will just work.

What should I do if the other object isn't the same type?

In a real app, you might accidentally try to compare a Version object to a string or nil. If you try to call other.major on a string, your program will crash with a NoMethodError. I've seen this kill production apps more times than I'd like to admit.

The convention in Ruby is that if <=> cannot compare two objects, it should return nil. This tells the calling method (like .sort) that the objects are not comparable, which allows Ruby to throw a descriptive ArgumentError instead of a confusing NoMethodError.

def <=>(other)
  return nil unless other.is_a?(Version)
  
  comparison = @major <=> other.major
  comparison == 0 ? @minor <=> other.minor : comparison
end

I always recommend adding this type check. It makes your code robust and prevents your app from blowing up just because some weird data crept into your collection.




📋 Practical Task

Build a Sortable Book Class

You are building a library catalog. Create a Book class that allows books to be sorted primarily by their publication_year (older books first) and secondarily by their title (alphabetically) if they were published in the same year.

  • The class should include the Comparable module.
  • Initialize the class with title and publication_year.
  • Implement the <=> operator to handle the two-step sorting logic.
  • Ensure that comparing a Book to a non-Book object returns nil.

Test your implementation by creating an array of three books (at least two from the same year) and calling .sort on the array to verify the order.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.