Discoveries in Ruby(and Rails): Give classes the ability to be compared using the Comparable module
Tags: ruby
Published: 2022-01-15 09:26:00

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 `Product`s. A product can have many different `Price` per customer/conuntry. The modeling might look like this

```ruby
class Price
  attr_accessor :amount_cents

  def initialize(amount_cents)
    self.amount_cents = amount_cents
  end
end
```

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

```ruby
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
```

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

```ruby
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
```

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)              
```

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.

```ruby
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
```

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

```ruby
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
```
and get the output

```
price_1 is less than price_2
=> nil
```

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

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

price_3.between? price_1, price_2
```
The output would be `true`

We can also sort them!

```ruby
[price_3, price_1, price_2].sort
[<Price:0x00007fa65e225d38 @amount_cents=100>,                          
 <Price:0x00007fa65e0be9b8 @amount_cents=150>,                          
 <Price:0x00007fa661b965e0 @amount_cents=200>]      
```

## 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 

```ruby
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
```


## 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 

- [Comparable docs](https://ruby-doc.org/core-3.1.0/Comparable.html)