Skip to Content
Course content

49: Comparable Module and Custom Sorting

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

I see this happen all the time when developers first encounter the Comparable module: they think that simply adding include Comparable to the top of their class is like flipping a magic switch that suddenly makes their objects "sortable." They expect Ruby to somehow intuit how two instances of a custom class should be compared.

Including Comparable Isn't a Magic Switch

Let's look at why that doesn't work. Imagine we're building a system to track software versions. You might try something like this:

class SoftwareVersion
  include Comparable
  attr_reader :major, :minor

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

v1 = SoftwareVersion.new(1, 2)
v2 = SoftwareVersion.new(2, 0)
v1 < v2 # This will raise a NoMethodError or return an error

If you run this, Ruby will complain. Even though we included Comparable, we haven't actually told Ruby how to compare two versions. Comparable is a "mixin" that provides the logic for <, >, <=, >=, and between?, but it does so by relying entirely on one single method: the spaceship operator <=>.

The Spaceship Operator is the Engine

To make Comparable work, you have to implement <=>. I call it the engine because it powers everything else. The spaceship operator is designed to return one of three things: -1 if the current object is smaller, 0 if they are equal, and 1 if the current object is larger.

Here is how we actually implement that for our software versions. I'll use a little Ruby shorthand here to keep it clean:

class SoftwareVersion
  include Comparable
  attr_reader :major, :minor

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

  def <=>(other)
    return nil unless other.is_a?(SoftwareVersion)

    if @major != other.major
      @major <=> other.major
    else
      @minor <=> other.minor
    end
  end
end

v1 = SoftwareVersion.new(1, 10)
v2 = SoftwareVersion.new(2, 0)
v1 < v2 # => true
v1 == v2 # => false

Notice that inside the <=> method, I'm actually calling <=> on the integers themselves. Since Integers already implement the spaceship operator, we can just delegate the hard work to them. It's a clean, recursive way of thinking about comparison.

Choosing Between sort and sort_by

Once your class is Comparable, calling Array#sort on a list of those objects just works. But as you get into larger datasets, you'll realize there's a performance difference between sort and sort_by.

sort uses the <=> method repeatedly. If your comparison logic is expensive (like hitting a database or doing complex regex), sort will be slow because it calls that logic every time it compares two elements.

sort_by is different. It uses something called a Schwartzian Transform. It calculates the "sort key" for every element exactly once, stores it, and then sorts those keys. I usually reach for sort_by whenever I'm sorting by a specific attribute rather than the object's inherent value.

# This calls <=> many times
versions.sort 

# This calls the block once per element, then sorts the results
versions.sort_by { |v| [v.major, v.minor] }

My rule of thumb: if you've implemented Comparable for a natural, inherent order, use sort. If you're doing a one-off sort based on a specific property, use sort_by.




📋 Practical Task

Implementing a Tournament Player Ranking System

You are building a tournament system. Each Player has a score and a wins count. You need to implement the Comparable module so that players can be ranked automatically.

  • The primary ranking factor is score (higher is better).
  • If two players have the same score, the one with more wins is ranked higher.
  • If both score and wins are identical, they are considered equal.

Requirements:

  1. Create a Player class that includes Comparable.
  2. Implement the <=> method to handle the logic described above.
  3. Create an array of at least four Player objects (including at least two with the same score but different win counts).
  4. Use the .sort method to display the players from lowest rank to highest rank.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.