In Progress
Unit 1, Lesson 1
In Progress

# Comparable

Video transcript & code

Today we're going to continue our work on a class that represents measurements in feet. We spent the last few episodes giving this class the ability to compare itself for equality with other objects. We talked about identity, equivalence, case equality, and hash equality.

``````class Feet

def initialize(magnitude)
@magnitude = magnitude.to_f
freeze
end

def to_s
"#{@magnitude} feet"
end

def +(other)
Feet.new(@magnitude + other.magnitude)
end

def ==(other)
other.is_a?(Feet) && magnitude == other.magnitude
end

def hash
[magnitude, Feet].hash
end

alias_method :eql?, :==
end
``````

But there's more to comparison than just equality. One of the properties that we take for granted on core types like strings and numbers that we can compare two values to see which is greater.

``````3 > 4                           # => false
``````

So far, our `Feet` objects lack this fundamental ability. Because operators in Ruby are really methods, comparing two `Feet` objects results in a `NoMethodError`.

``````require "./feet"

Feet.new(3) > Feet.new(4)       # =>
# ~> -:3:in `<main>': undefined method `>' for #<Feet:0x0000000132ea78 @magnitude=3.0> (NoMethodError)
``````

We can begin to address this omission by defining the requisite operator methods, such as greater-than. We can crib the implementation from the equivalence operator; the only change we need to make is in which operator to apply to the magnitudes of the two objects.

``````class Feet

def initialize(magnitude)
@magnitude = magnitude.to_f
freeze
end

def to_s
"#{@magnitude} feet"
end

def +(other)
Feet.new(@magnitude + other.magnitude)
end

def ==(other)
other.is_a?(Feet) && magnitude == other.magnitude
end

def >(other)
other.is_a?(Feet) && magnitude > other.magnitude
end

def hash
[magnitude, Feet].hash
end

alias_method :eql?, :==
end
``````

That takes care of the greater-than case. But there are a lot of comparison operators. Besides greater-than, there's greater-than-or-equal, less-than, and less-than-or-equal. If we continue onwards like this, we're going to be writing a lot of tedious, repetitive code.

There's also one other comparison operator we need to implement: the spaceship operator. This operator isn't as well known as the others, but it's just as important, if not more so.

Before we go any further, let's quickly review how the spaceship operator works on core classes.

When the left-hand value is lesser than the right-hand value, it returns -1. When the left-hand is greater than the right-hand, it returns 1. And when the two values are equivalent, it returns 0. In effect, it wraps the functions of all the other comparison operators into one super-operator.

``````3 <=> 4                         # => -1
4 <=> 3                         # => 1
4 <=> 4                         # => 0
``````

Why is the spaceship so important? It's easiest to explain by demonstration. Let's throw some `Feet` objects into an array, and then try to sort them.

``````require "./feet2"

[Feet.new(8), Feet.new(6), Feet.new(7)].sort
# =>
# ~> -:3:in `sort': comparison of Feet with Feet failed (ArgumentError)
# ~>    from -:3:in `<main>'
``````

We get the argument error "comparison of Feet with Feet failed".

Now let's implement the spaceship operator. We'll do it the same way we implemented equivalence and greater-than, but this time using the spaceship operator on the `Feet` magnitudes.

``````class Feet

def initialize(magnitude)
@magnitude = magnitude.to_f
freeze
end

def to_s
"#{@magnitude} feet"
end

def +(other)
Feet.new(@magnitude + other.magnitude)
end

def ==(other)
other.is_a?(Feet) && magnitude == other.magnitude
end

def >(other)
other.is_a?(Feet) && magnitude > other.magnitude
end

def <=>(other)
other.is_a?(Feet) && magnitude <=> other.magnitude
end

def hash
[magnitude, Feet].hash
end

alias_method :eql?, :==
end
``````

With that done, we'll try again to sort the array of `Feet`.

``````require "./feet3"

[Feet.new(8), Feet.new(6), Feet.new(7)].sort
# => [#<Feet:0x00000000acb840 @magnitude=6.0>,
#     #<Feet:0x00000000acb818 @magnitude=7.0>,
#     #<Feet:0x00000000acb8b8 @magnitude=8.0>]
``````

This time, the array is successfully sorted. It should be clear now why the spaceship operator matters: it's the operator Ruby uses by default to compare two objects when sorting them.

But that's not the only reason the spaceship is important. As it turns out, now that we've implemented spaceship, we don't need to bother with all the other comparison operators. We can simply include the core Ruby module Comparable. So long as we implement the spaceship, `Comparable` gives us all the other comparisons for free.

So we can get rid of our handwritten greater-than operator now. Not only that, but since the spaceship can also be used to determine equivalence, we don't need our own equivalence operator anymore either. The `Comparable` module provides us with one.

``````class Feet
include Comparable

def initialize(magnitude)
@magnitude = magnitude.to_f
freeze
end

def to_s
"#{@magnitude} feet"
end

def +(other)
Feet.new(@magnitude + other.magnitude)
end

def <=>(other)
other.is_a?(Feet) && magnitude <=> other.magnitude
end

def hash
[magnitude, Feet].hash
end

alias_method :eql?, :==
end
``````

`Feet` objects now have access to lesser-than, lesser-than-or-equal, greater-than, and greater-than-or equal operators. As well as equivalence.

``````require "./feet4"
Feet.new(3) < Feet.new(4)       # => true
Feet.new(3) <= Feet.new(4)      # => true
Feet.new(3) > Feet.new(4)       # => false
Feet.new(3) >= Feet.new(4)      # => false
Feet.new(3) == Feet.new(4)      # => false
``````

In edition to these, `Comparable` provides one other handy method: `between?`. This method can tell us if an object falls between two other objects in value.

``````require "./feet4"
Feet.new(4).between?(Feet.new(3), Feet.new(5))
# => true
``````

Today we've added a total of one line to our `Feet` class, but we've gained five essential operators and a bonus method. That seems like a good deal to me. Happy hacking!