How Does Ruby's Sort Method Work with The Combined Comparison (Spaceship) Operator

How does Ruby's sort method work with the combined comparison (spaceship) operator?

a <=> b will return -1 if a belongs before b, 0 if they're equal, or 1 if a should follow b.

b <=> a will return -1 if b belongs before a, 0 if they're equal, or 1 if b should follow a.

Since you are reversing the order, the output should be reversed, just like the - operator, for example. 3-5 is -2, and 5-3 is 2.

array.sort { |b, a| b <=> a } is equal to array.sort { |a, b| a <=> b } because the first argument is before the spaceship, and the second is after. Ruby doesn't care what the name of the variable is.

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!

alternative to combined comparison operator (=) in .sort

return returns from a method. It can only be used inside a method. There is no method in your code, therefore you get an error … and even if there were a method, the code wouldn't do what you want, because return returns from the method, not the block.

To return a value from a block, use next:

array.sort! {|a, b| 
if b < a
next -1
elsif b > a
next 1
else
next 0
end
}

However, unlike in C, if/then/else is an expression in Ruby, not a statement. (Actually, everything is an expression in Ruby, there are no statements.) This means that everything, including a conditional expression, returns a value. For if/then/else the value being returned is the one from the branch that was taken.

So, instead of returning from each of the branches separately, we can just return the vlue of the whole if expression:

array.sort! {|a, b| 
next if b < a
-1
elsif b > a
1
else
0
end
}

And since the return value of a block (just like the return value of a method) is implicitly the last value evaluated inside the block, we can just say:

array.sort! {|a, b| 
if b < a
-1
elsif b > a
1
else
0
end
}

next is mostly useful to break out early of a block in a guard-clause style or to flatten a deeply nested conditional:

array.sort! {|a, b| 
next -1 if b < a
next 1 if b > a
0
}

Note that case would probably more appropriate than if here:

array.sort! {|a, b| 
case
when b < a
-1
when b > a
1
else
0
end
}

What is going on in ruby's sort method?

The a and b represent a pair of items. It could be any two taken out of your original list. The <=> is usually called the spaceship operator. It returns 0 if the two items are equal, -1 if the one on the left is smaller, and 1 if the one on the right is smaller.

There's more info on the spaceship operator in the Ruby API docs. That's the doc for the one on Fixnum since that's what was in your example, but you can check out the definition for Float, String, etc. there as well.

Updated: The sort function expects the block it's given to follow the same behavior as the spaceship operator. If the first argument, a should be sorted first, -1 should be returned; if the second argument, b should be sorted first, 1 should be returned; and so on. So in the example of list.sort { |a,b| a + b } you're telling sort that the second argument is bigger every time, since a + b is greater than 1 for every possible combination in that list. So what you're seeing when you get [5,3,1,4,2] is basically an artifact of the order that elements are passed to the block and would likely not be stable across Ruby implementations.

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.

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

Sort objects in array by 2 attributes in Ruby with the spaceship operator

Define <=> like this:

   def <=>(other)
[str.size, str] <=> [other.str.size, other.str]
end


Related Topics



Leave a reply



Submit