DEV Community

Ahmad khattab
Ahmad khattab

Posted on • Updated on

Discoveries in Ruby(and Rails): Give classes the ability to be compared using the Comparable module

The equality-test operators is a set of members that consist of ==(equals), !=(not equal), > (greater than), <(less than), >=(greater or equal), and <= (less or equal), with the most common being the == operator.

We can see that with both Integer and Float which are subclasses of the Numeric class. Objects of both classes can be compared using the equality-test operators because they include the Comparable module, and override the comparison method <=>, also called the spaceship operator or spaceship method.

Let's imagine in an e-commerce sight we have Products. A product can have many different Price per customer/conuntry. The modeling might look like this

class Price
  attr_accessor :amount_cents

  def initialize(amount_cents)
    self.amount_cents = amount_cents
  end
end
Enter fullscreen mode Exit fullscreen mode

Let's imagine that we need to compare the price of two products(for whatever reason we need to). To compare the amount_cents we might do something like

if price_1.amount_cents < price_2.amount_cents
  puts "price_1 is less than price_2"
elsif price_1.amount_cents > price_2.amount_cents
  puts "price_1 is greater than price_2"
else
  puts "both prices are equal"
end
Enter fullscreen mode Exit fullscreen mode

To compare the price objects we simply need to do so on the amount_cents attribute, since it's an instance of Numeric(either float or integer) it can be compared using the equality-test operators mentioned above.

But, we cannot do something like this

if price_1 < price_2
  puts "price_1 is less than price_2"
elsif price_1 > price_2
  puts "price_1 is greater than price_2"
else
  puts "both prices are equal"
end
Enter fullscreen mode Exit fullscreen mode

Because our class Price does not include the Comparable module, thus it does not support this. If we try to we get

 undefined method `<' for #<Price:0x00007fbd98992de0 @amount_cents=10> (NoMethodError)              
Enter fullscreen mode Exit fullscreen mode

The ruby interpreter tells us that the class does not have support for the < method. Because we did not define this.

Lo! and behold the Comparable module in action.

Let's allow our Price class to work nice with the equality-test operators. To do that we need to do

  • include the Comparable module
  • override the <=> method

Let's rewrite the Product class to support that.

class Price
  include Comparable
  attr_accessor :amount_cents

  def initialize(amount_cents)
    self.amount_cents = amount_cents
  end

  def <=>(other_price)
    if self.amount_cents < other_price.amount_cents
      -1
    elsif self.amount_cents > other_price.amount_cents
      1
    else
      0
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Notice the return value for the <=> method.

  • -1 means that it's less than the passed argument.
  • 1 means that it's greater than the passed argument.
  • 0 means both prices are equal.

Now, we can run this code

if price_1 < price_2
  puts "price_1 is less than price_2"
elsif price_1 > price_2
  puts "price_1 is greater than price_2"
else
  puts "both prices are equal"
end
Enter fullscreen mode Exit fullscreen mode

and get the output

price_1 is less than price_2
=> nil
Enter fullscreen mode Exit fullscreen mode

Including the Comparable module provides us with more methods more than just the equality-test operators. For example, using the Price class determine if a price is between two other price

price_1 = Price.new 100
price_2 = Price.new 200
price_3 = Price.new 150

price_3.between? price_1, price_2
Enter fullscreen mode Exit fullscreen mode

The output would be true

We can also sort them!

[price_3, price_1, price_2].sort
[<Price:0x00007fa65e225d38 @amount_cents=100>,                          
 <Price:0x00007fa65e0be9b8 @amount_cents=150>,                          
 <Price:0x00007fa661b965e0 @amount_cents=200>]      
Enter fullscreen mode Exit fullscreen mode

Note

We can piggy-back the method call to the attribute itself. Since it's an instance of Numeric. We can shorten the method to

class Price
  include Comparable
  attr_accessor :amount_cents

  def initialize(amount_cents)
    self.amount_cents = amount_cents
  end

  def <=>(other_price)
    self.amount_cents <=> other_price.amount_cents
  end
end
Enter fullscreen mode Exit fullscreen mode

Conclusion

We can allow our classes to work well with the equality-test family members and save ourselves much time trying to access attributes for comparison. And as a side-effect our Ruby code becomes more natural and ruby-ish, and we type less :).

Thanks for reading, and Happy Coding!.

Resources

Top comments (5)

Collapse
 
dorianmariefr_22 profile image
Dorian Marié

I would do self.amount_cents <=> other.amount_cents

Collapse
 
rockwell profile image
Ahmad khattab

Damn. Yea you're right. We can piggy-back function to Ruby. Since it's a Numeric.

Collapse
 
rockwell profile image
Ahmad khattab

Just added it to the post!!. Thanks for pointing that out!

Collapse
 
dorianmariefr_22 profile image
Dorian Marié

And yeah you should probably be using the money gem :)

Collapse
 
rockwell profile image
Ahmad khattab

Funny thing is, I am. Just couldn't think of a better example to illustrate the thingi!.