DEV Community

Cover image for The spaceship operator <=> in Ruby
Josh Blengino
Josh Blengino

Posted on • Edited on

The spaceship operator <=> in Ruby

If you're a Ruby noob like me, you probably saw the "spaceship operator" and thought of Space Invader or Darth Vader's infamous TIE fighter. I am currently on day 2 of learning Ruby going into day 3 and I had trouble understanding what this interesting looking operator does and what you could use it for. Hopefully by the end of this post, you and I can both take away some practical use cases for this method.

Ruby Comparison Operators

Before we start experimenting with the <=> operator, let's take a look at more common comparison operators.

 1 > 0     #=> true     (Greater than)
 0 < 1     #=> true     (Less than)
 1 >= 0    #=> true     (Greater or equal than)
 0 <= 1    #=> true     (Less or equal than)
 1 == 0    #=> false    (Equals)
 1 != 0    #=> true     (Not equals)
Enter fullscreen mode Exit fullscreen mode

Pretty straightforward, they all return a boolean value. An important takeaway from one of my readings was to remember that all Ruby operators are actually methods. This is because the behavior (return value) will depend on the type of class (object) it is called on.

<=> Operator

It's technical name is the combined comparison operator. The major difference compared to the other comparison operators is that it returns 3 different values: (-1), (0), or (1).

1 <=> 1     #=> 0
0 <=> 1     #=> -1
1 <=> 0     #=> 1
Enter fullscreen mode Exit fullscreen mode

For Integers: (0) if right side = left, (-1) if less than, and (1) if greater than.

"string" <=> "string"     #=> 0     (contents are the same)
"string" <=> "stringg"    #=> -1    (length of string)
"string1" <=> "string2"   #=> -1    (string integer)
"STRING" <=> "string"     #=> -1    (capital)
Enter fullscreen mode Exit fullscreen mode

For Strings the behavior is not as straight forward. It looks like uppercase letters are given a lower value than lowercase letters. Why?



Another great post on this topic mentions how characters in strings are compared with binary values under the hood, which helps to better understand the above return values.

fighter1 = {type: "TIE Fighter", owner: "Darth Vader", affiliation: "Galactic Empire", id: 1}
fighter2 = {type: "X-wing", owner: "Luke Skywalker", affiliation: "Rebel Alliance", id: 2}

fighter1 <=> fighter1     #=> 0
fighter2 <=> fighter1     #=> nil
fighter1 <=> fighter2     #=> nil
Enter fullscreen mode Exit fullscreen mode

For Hashes it's even less clear...

fighter1 = {type: "TIE Fighter", owner: "Darth Vader", affiliation: "Galactic Empire", id: 1}
fighter2 = {type: "TIE Fighter", owner: "Darth Vader", affiliation: "Galactic Empire", id: 1}

fighter1 <=> fighter1     #=> 0
fighter2 <=> fighter1     #=> 0
fighter1 <=> fighter2     #=> 0
Enter fullscreen mode Exit fullscreen mode

Here the contents are exactly the same with expected results.

fighter1 = {type: "TIE Fighter", owner: "Darth Vader", affiliation: "Galactic Empire", id: 1}
fighter2 = {type: "X-wing", owner: "Luke Skywalker", affiliation: "Rebel Alliance", id: 2}

fighter1[:owner] <=> fighter1[:owner]     #=> 0
fighter2[:owner] <=> fighter1[:owner]     #=> 1
fighter1[:owner] <=> fighter2[:owner]     #=> -1
Enter fullscreen mode Exit fullscreen mode

Here it looks like the string length comes in to play after changing fighter2 back to normal.

fighter1 = {type: "TIE Fighter", owner: "Darth Vader", affiliation: "Galactic Empire", id: 1}
fighter2 = {type: "X-wing", owner: "Luke Skywalker", affiliation: "Rebel Alliance", id: 2}

fighter1[:id] <=> fighter1[:id]     #=> 0
fighter2[:id] <=> fighter1[:id]     #=> 1
fighter1[:id] <=> fighter2[:id]     #=> -1
Enter fullscreen mode Exit fullscreen mode

Even though these examples do not showcase every possible scenario for these data types, we now know that the behavior of the <=> operator for Arrays and Hashes is a little unpredictable depending on their respective contents. Even Ruby's documentation mentions how unpredictable the return value could be based on the objects being compared.
That's still not very helpful for how we could use this in a practical way.

Using <=> with Ruby's sort method

star_fighters = [
  {type: "TIE Fighter", owner: "Darth Vader", affiliation: "Galactic Empire", id: 1}, 
  {type: "X-wing", owner: "Luke Skywalker", affiliation: "Rebel Alliance", id: 2},
  {type: "Jedi Starfighter", owner: "Obi-Wan Kenobi", affiliation: "The Republic", id: 3}

]

sorted_star_fighters = star_fighters.sort do |fighter1, fighter2|
  fighter1[:id] <=> fighter2[:id]
end
puts sorted_star_fighters
#=> {:type=>"TIE Fighter", :owner=>"Darth Vader", :affiliation=>"Galactic Empire", :id=>1}, {:type=>"X-wing", :owner=>"Luke Skywalker", :affiliation=>"Rebel Alliance", :id=>2}, {:type=>"Jedi Starfighter", :owner=>"Obi-Wan Kenobi", :affiliation=>"The Republic", :id=>3}
Enter fullscreen mode Exit fullscreen mode

In this scenario, our hashes are being ordered from lowest to highest because based on how our id's are set up, fighter2's id will always be greater than fighter1's, which will return -1.

star_fighters = [
  {type: "TIE Fighter", owner: "Darth Vader", affiliation: "Galactic Empire", id: 1}, 
  {type: "X-wing", owner: "Luke Skywalker", affiliation: "Rebel Alliance", id: 2},
  {type: "Jedi Starfighter", owner: "Obi-Wan Kenobi", affiliation: "The Republic", id: 3}

]

sorted_star_fighters = star_fighters.sort do |fighter1, fighter2|
  fighter2[:id] <=> fighter1[:id]
end
puts sorted_star_fighters
#=> {:type=>"Jedi Starfighter", :owner=>"Obi-Wan Kenobi", :affiliation=>"The Republic", :id=>3}, {:type=>"X-wing", :owner=>"Luke Skywalker", :affiliation=>"Rebel Alliance", :id=>2}, {:type=>"TIE Fighter", :owner=>"Darth Vader", :affiliation=>"Galactic Empire", :id=>1}
Enter fullscreen mode Exit fullscreen mode

In this scenario, our hashes are being ordered from highest to lowest because in this case, fighter2's id will always be greater than fighter1's id, which will return 1.

Takeaways

  1. Return values are integers: (-1), (0), or (1)
  2. Return values can be unpredictable with Strings & Hashes depending on their contents which makes Integers a more attractive option to work with.
  3. The <=> operator can come in handy when using sort to order your data from ascending or descending order in a cleaner way instead of doing something like this:
star_fighters = [
  {type: "TIE Fighter", owner: "Darth Vader", affiliation: "Galactic Empire", id: 1}, 
  {type: "X-wing", owner: "Luke Skywalker", affiliation: "Rebel Alliance", id: 2},
  {type: "Jedi Starfighter", owner: "Obi-Wan Kenobi", affiliation: "The Republic", id: 3}

]

sorted_star_fighters = star_fighters.sort do |fighter1, fighter2|
  if fighter1[:id] == fighter2[:id]
    0
  elsif fighter1[:id] < fighter2[:id]
    -1
  elsif fighter1[:id] > fighter2[:id]
    1
  end
end
puts sorted_star_fighters
#=> {:type=>"TIE Fighter", :owner=>"Darth Vader", :affiliation=>"Galactic Empire", :id=>1}, {:type=>"X-wing", :owner=>"Luke Skywalker", :affiliation=>"Rebel Alliance", :id=>2}, {:type=>"Jedi Starfighter", :owner=>"Obi-Wan Kenobi", :affiliation=>"The Republic", :id=>3}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Based on my short experience with the <=> operator, it seems to be most useful in tandem with the sort enumerator. I would love to hear if anyone else knows any other use cases in the comments below. Hopefully this post gave you a slightly better understanding of the <=> operator. I am definitely going to continue to experiment with it. Other languages that use the <=> operator include: Perl, Apache, Groovy, PHP, Eclipse, Ceylon, and C++. Having a good grasp on how and when to use conditional operators to compare values will allow you to control the flow of your data like a jedi controlling the flow of The Force.

Resources

Ruby equality
Ruby operators
Ruby sort enumerator
HTML Encoding (Character Sets)
https://www.youtube.com/watch?v=tpsdxtf01po
https://www.youtube.com/watch?v=f4NItw7r33E

Top comments (0)