The spaceship operator <=> in Ruby
Josh Blengino
Posted on August 11, 2021
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)
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
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)
For Strings
the behavior is not as straight forward. It looks like uppercase letters are given a lower value than lowercase letters. Why?
The Ruby Enumerable <=> operator and #max vs. #max_by
Sylvia Pap ・ Feb 12 '20
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
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
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
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
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}
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}
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
- Return values are integers:
(-1), (0), or (1)
- Return values can be unpredictable with
Strings
&Hashes
depending on their contents which makesIntegers
a more attractive option to work with. - 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}
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
Posted on August 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.