Ruby's Powerful Comparable Module

Tim's written a short guide to the Comparable module found in Ruby.

Tim's written a short guide to the Comparable module found in Ruby.

One of my favourite Ruby features is the Comparable module. By mixing this module into your classes, you can compare your objects using the ==, <, >, <=, >= operators, as well as the between? and clamp methods. This allows you to quickly add some useful functionality with the minimum of coding effort. Hopefully, the following simple example will demonstrate some of the power of the Comparable module.

Let’s assume we are writing an application which deals with a football league consisting of a number of football teams. Each team’s placing in the league is determined by the number of points they have been awarded, say three points for a win and a single point for a draw. In this situation, it would be very easy to compare two team’s relative placings in the league. For example, your code might look like the following:

def FootballTeam
  attr_accessor :points
  def initialize points
    @points = points
  end
end

The relative placings of two teams can be determined using their points values. Let’s create a couple of teams and assign them some points:

chelsea = FootballTeam.new(10)
arsenal = FootballTeam.new(12)

In this case, chelsea.points > arsenal.points would evaluate to false, while arsenal.points <= chelsea.points would evaluate to true.

This is all very well, but let’s assume that if two teams have the same number of points, their relative placings in the league are determined by their goal difference - the number of goals the team has scored minus the number of goals the team has conceded. Our code might change to the following:

def FootballTeam
  attr_accessor :points, :goals_scored, :goals_conceded

  def initialize points, goals_scored, goals_conceded
    @points         = points
    @goals_scored   = goals_scored
    @goals_conceded = goals_conceded
  end
  def goal_difference
    @goals_scored - @goals_conceded
  end
end

We’ve added code to allow the number of goals scored and the number of goals conceded to be set when the team object is created, and a method to allow the goal difference to be calculated. However, we can no longer determine two team’s relative placings simply by comparing their points - we need to take goal difference into account as well. One approach would be to override each of the comparison operators, so that for example we would add:

def > other_team
  if self.points == other_team.points
    self.goal_difference > other_team.goal_difference
  else
    self.points > other_team.points
  end
end

Similar methods would be required for <, ==, <=, and >=

However, this is where the Comparable module can help us. To use the Comparable module, we simply need to add the line include Comparable in our class definition, and add a method called <=> (sometimes known as the spaceship operator). The module will handle everything else, and will provide the other comparison methods. This sounds a little too good to be true, so let’s update the code, and see the Comparison module in action:

def FootballTeam
  include Comparable

  attr_accessor :points, :goals_scored, :goals_conceded

  def initialize points, goals_scored, goals_conceded
    @points         = points
    @goals_scored   = goals_scored
    @goals_conceded = goals_conceded
  end

  def goal_difference
    @goals_scored - @goals_conceded
  end

  def <=> other_team
    if self.points < other_team.points
      -1
    elsif self.points > other_team.points
      1
    else
      if self.goal_difference < other_team.goal_difference
        -1
      elsif self.goal_difference > other_team.goal_difference
        1
      else
        0
      end
    end
  end
end

(We’ll refactor this in a moment.)

Now we can set up a few teams and compare them:

chelsea = FootballTeam.new(10, 11, 13)
arsenal = FootballTeam.new(12, 12,  9)
liverpool = FootballTeam.new(12, 12,  10)

chelsea > arsenal will evaluate to false, while arsenal >= chelsea will evaluate to true. Additionally, liverpool < arsenal will evaluate to true, and liverpool.between?(chelsea, arsenal) will also evaluate to true.

We’ve gained quite a lot of useful functionality by defining a single function.

We can now refine our code using the spaceship operator inside our <=> method:

def <=> other_team
  result = self.points <=> other_team.points

  if result == 0
    result = self.goal_difference <=> other_team.goal_difference
  end

  result
end

Hopefully, this post has given you an insight into why I’m a big fan of the Comparable module, and given you some ideas of how you can make use of it in your own code. I’d love to hear about the uses other coders have put it to - why not leave a comment?


Related Posts

Let's Work Together

We would love to hear from you so let's get in touch!

CONTACT US TODAY!