What Is the Ruby ≪=≫ (Spaceship) Operator

What is the Ruby = (spaceship) operator?

The spaceship operator will return 1, 0, or −1 depending on the value of the left argument relative to the right argument.

a <=> b :=
if a < b then return -1
if a = b then return 0
if a > b then return 1
if a and b are not comparable then return nil

It's commonly used for sorting data.

It's also known as the Three-Way Comparison Operator. Perl was likely the first language to use it. Some other languages that support it are Apache Groovy, PHP 7+, and C++20.

ruby's = operator and sort method

Before you can understand sorting objects. You need to understand the .sort method in Ruby. If you were to sort 5 cards with numbers on them, you could take a look at all of them, find the lowest one easily, and just pick that one as your first card (presuming you're sorting from lowest to highest, which Ruby does). When your brain sorts, it can look at everything and sort from there.

There are 2 main elements of confusion here that are rarely addressed:

1) Ruby can't sort in the way you think of the word "sort". Ruby can only 'swap' array elements, and it can 'compare' array elements.

2) Ruby uses a comparison operator, called the spaceship, to attribute numbers to help it 'sort'. Those numbers are -1,0,1. People erroneously think those 3 numbers are helping it 'sort' (eg. if there was an array with 3 numbers such a 10,20,30, then the 10 would be a -1, the 20 a 0, and the 30 a 1, and Ruby is just simplifying the sorting by reducing it to -1,0,1. This is wrong. Ruby can't "sort". It can't only compare).

Look at the spaceship operator. It's 3 individual operators lumped into one, the <, the =, and the >. When Ruby compares two variables, it results in one of these numbers.

Spaceship Operator

That said, what does "results" mean? It DOESN'T mean that one of the variables is assigned a 0,1,-1. It simply is a way what Ruby can take two variables and do something with them. Now, if you just run:

puts 4 <=> 5

You'll get the result of -1, since whatever 'part' (eg. <, =, or >) of the comparison operator (spaceship) is true, gets the number that's assigned to it (as seen in the above picture). When Ruby sees this <=> with an array though, it has 2 things it will do to the array only: Leave the array alone OR swap the elements of the array.

If Ruby uses the <=> and gets a 1, it will swap the 2 elements of the array. If Ruby gets a result of -1 or 0, it will leave the array alone.

An example is if Ruby sees the array [2,1]. The sort method would make it pull in these figures like 2<=>1. Since the part of the spaceship (if you want to think of it like that) that's true is the > (ie. 2>1 is true), the result is '1' from Ruby. When Ruby sees a 1 result from the spaceship, it swaps the 2 elements of the array. Now the array is [1,2].

Hopefully at this point, you see that Ruby only compares with the <=> operator, and then swaps (or leaves alone) the 2 elements in the array it compares.

Understand the .sort method is an iterative method, meaning that it's a method that runs a block of code many times. Most people are introduced to the .sort method only after they've seen a methods such as .each or .upto (you don't need to know what those do if you haven't heard of them), but those methods run through the array 1 time ONLY. The .sort method is different in that it will run through your array as many times as it needs to so that it's sorted (by sorted, we mean compared and swapped).

To make sure you understand the Ruby syntax:

foo = [4, 5, 6]
puts foo.sort {|a,b| a <=> b}

The block of code (surrounded by {}'s) is what Ruby would do any way when it sorts from lowest to highest. But suffice it to say that the first iteration of the .sort method will assign the variables between the pipes (a, b) the first two elements of the array. So for the first iteration a=4 and b=5, and since 4<5, that results in a -1, which Ruby takes it to mean to NOT swap the array. It does this for a second iteration, meaning a=5 and b=6, sees that 5<6, results in -1 and leaves the array alone. Since all the <=> results were -1, Ruby stops looping through and feels the array is sorted at [4,5,6].

We can sort from high to low by simply swapping the order of the variables.

bar = [5, 1, 9]
puts bar.sort {|a,b| b <=> a}

Here's what Ruby is doing:

Iteration 1: Array [5,1,9]. a=5, b=1. Ruby sees the b<=>a, and says is 1 < 5? Yes. That results in -1. Stay the same.

Iteration 2: Array [5,1,9]. a=1, b=9. Ruby sees the b<=>a, and says is 9 < 1? No. That results in 1. Swap the 2 array elements. The array is now [5,9,1]

Iteration 3: Array [5,9,1]. Starting over b/c there was a +1 result in the array before going through it all. a=5, b=9. Ruby sees the b<=>a, says is 9<5? No. That results in 1. Swap. [9, 5, 1]

Iteration 4: Array [9,5,1]. a=5, b=1. Ruby sees the b<=>a, says is 1<5? Yes. That results in -1. Therefore, no swapping is performed. Done. [9,5,1].

Imagine an array with the number 50 for the first 999 elements, and a 1 for element 1000. You fully understand the sort method if you realize Ruby has got to go through this array thousands of times doing the same simple compare and swap routine to shift that 1 all the way to the beginning of the array.

Now, we can finally look at .sort when comes to an object.

def <=>(other)
other.score <=> score
end

This should now make a little more sense. When the .sort method is called on an object, like when you ran the:

@players.sort

it pulls up the "def <=>" method with the parameter (eg. 'other') which has the current object from @players (eg. 'whatever the current instance object is of '@players', since it's the sort method, it's eventually going to go through all of the elements of the '@players' array). It's just like when you try to run the puts method on a class, it automatically calls the to_s method inside that class. Same thing for the .sort method automatically looking for the <=> method.

Looking at the code inside of the <=> method, there must be a .score instance variable (with an accessor method) or simply a .score method in that class. And the result of that .score method should (hopefully) be a String or number - the 2 things ruby can 'sort'. If it's a number, then Ruby uses it's <=> 'sort' operation to rearrange all of those objects, now that it knows what part of those objects to sort (in this case, it's the result of the .score method or instance variable).

As a final tidbit, Ruby sorts alphabetically by converting it to numerical values as well. It just considers any letter to be assigned the code from ASCII (meaning since upper case letters have lower numerical values on the ASCII code chart, upper case will be sorted by default to be first).

Hope this helps!

Custom sort using spaceship operator in ruby

Array#sort uses Quicksort algorithm, which is not stable and can produce this behaviour when elements are "equal".

Reason is in choosing and moving pivot element at every step, ruby implementation seems to choose pivot at middle for this case (but it can be chosen differently).

This is what happens in your example:

  1. pivot is chosen at element 9 at middle of array
  2. now algorithm ensures that items on left of pivot are less than it and items on the right are greater or equal, because all are "equal" - this makes everything to be in right part
  3. now recursively repeat for left(this is always empty in this case) and right partitions
  4. result is sorted_left + [pivot] + sorted_right, left is empty thus pivot is moved

Ruby core documentation mentions this:

When the comparison of two elements returns 0, the order of the elements is unpredictable.

Also spaceship operator <=> does not play any role here, you could just call myArray.sort{0} for the same effect.

Update:

From updated question it's clear that you want to sort by two attributes, this can be done several ways:

Method1: you can invent a metric/key that takes both values into account and sort by it:

 status_order = { 'success' => 1, 'failed' => 2, 'pending' => 3 }
myObjects.sort_by{|o| "#{status_order[o.status]}_#{o.created_time}" }

This is not very optimal in terms of extreme performance, but is shorter.

Method2: implicit composite key by writing comparison rule like this:

status_order = { 'success' => 1, 'failed' => 2, 'pending' => 3 }
status_order.default = 0

myObjects.sort{|a,b|
if a.status == b.status
a.created_time <=> b.created_time
else
status_order[a.status] <=> status_order[b.status]
end
}

What actually happens when Spaceship operator is overwritten in Ruby?

Let's start with d.sort. d is an array, so you should read about Array#sort. The documentation says that sorting "will be done using the <=> operator". That means when sorting the array it will repeatedly evaluate x <=> y where x and y are different elements of d, in order to determine which elements are bigger/smaller.

Operators in Ruby are a little tricky, because they're actually method calls in disguise. x <=> y is just a different way of writing x.<=>(y). That is, x has a method named <=> which is being passed y as an argument. Since the elements of d are instances of the Age class, Age needs to define the instance method <=>. So when you see def <=> (other), this is really no different from a normal method definition (def foo(other)) but with a different method name.

Now that we're defining <=> for Age instances, what should the method actually do? Well each Age object stores a @value, so intuitively Age#<=> should compare the @values. That's what self.value <=> other.value does. This works because value returns a Fixnum, and luckily Fixnum#<=> is built into Ruby to do the proper comparison.

For an example of what's going on behind the scenes, we can try sorting arrays containing values that we don't know how to compare:

[Age.new(3), 4].sort
# NoMethodError: undefined method `value' for 4:Fixnum
[4, Age.new(3)].sort
# ArgumentError: comparison of Fixnum with Age failed

This shows how sorting different types leads to different <=> methods being called.

Confused with Ruby's = operator

It's called the 'spaceship' operator. More info: What is the Ruby <=> (spaceship) operator? and http://en.wikipedia.org/wiki/Spaceship_operator

Overriding Ruby's spaceship operator =

Your problem is you are only initializing one of the properties on either side, the other one will still be nil. nil isn't handled in the Array#<=> method, which ends up killing the sort.

There are a few ways to handle the problem first would be something like this

[self.weight.to_i, self.sweetness.to_i] <=> [other.weight.to_i, other.sweetness.to_i]

nil.to_i gives you 0, which will let this work.

What is = (the 'Spaceship' Operator) in PHP 7?

The <=> ("Spaceship") operator will offer combined comparison in that it will :

Return 0 if values on either side are equal
Return 1 if the value on the left is greater
Return -1 if the value on the right is greater

The rules used by the combined comparison operator are the same as the currently used comparison operators by PHP viz. <, <=, ==, >= and >. Those who are from Perl or Ruby programming background may already be familiar with this new operator proposed for PHP7.

   //Comparing Integers

echo 1 <=> 1; //output 0
echo 3 <=> 4; //output -1
echo 4 <=> 3; //output 1

//String Comparison

echo "x" <=> "x"; //output 0
echo "x" <=> "y"; //output -1
echo "y" <=> "x"; //output 1


Related Topics



Leave a reply



Submit