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.
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:
- pivot is chosen at element
9
at middle of array - 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
- now recursively repeat for left(this is always empty in this case) and right partitions
- 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
}
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.
How does Ruby's Combined Comparison Operator work?
First question: At various points in its operation the sort
method has to compare pairs of objects to see what their relative ordering should be. It does the comparison by applying the block you pass to sort
, i.e., {|first, second| second <=> first}
. Not sure what you mean by "how do the items within the array become objects when we don't declare them as such?". All data in ruby is an object, so there's no declaration or conversion needed given that all variables are object references.
Second question: Yes, you could do fruits.sort.reverse
, but that would require additional work after the sort to do the reverse operation. Also, reverse
can't handle more complex sorting tasks, such as sorting people by multiple criteria such as gender & last name, or hair color, height, and weight. Writing your own comparator can handle quite complex orderings.
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.
Passing a comparison operator as argument to a method in Ruby
The comparison operators are just methods in Ruby, so you could do:
1 <= 2 # is the same as
1.<=(2)
which means you can public_send
them just as any other public method:
1.public_send(:<=, 2)
How to create a custom sort method in Ruby
The best answer is provided by @AJcodez below:
points.sort_by{ |p| [p.x, p.y] }
The "correct" answer I originally provided, while it technically works, is not code I would recommend writing. I recall composing my response to fit the question's use of if
/else
rather than stopping to think or research whether Ruby had a more expressive, succinct way, which, of course, it does.
With a case
statement:
ar.sort do |a, b|
case
when a.x < b.x
-1
when a.x > b.x
1
else
a.y <=> b.y
end
end
With ternary:
ar.sort { |a,b| a.x < b.x ? -1 : (a.x > b.x ? 1 : (a.y <=> b.y)) }
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.
How does Array#sort work when a block is passed?
In your example
a.sort
is equivalent to
a.sort { |x, y| x <=> y }
As you know, to sort an array, you need to be able to compare its elements (if you doubt that, just try to implement any sort algorithm without using any comparison, no <
, >
, <=
or >=
).
The block you provide is really a function which will be called by the sort
algorithm to compare two items. That is x
and y
will always be some elements of the input array chosen by the sort
algorithm during its execution.
The sort
algorithm will assume that this comparison function/block will meet the requirements for method <=>
:
- return -1 if x < y
- return 0 if x = y
- return 1 if x > y
Failure to provide an adequate comparison function/block will result in array whose order is undefined.
You should now understand why
a.sort { |x, y| x <=> y }
and
a.sort { |x, y| y <=> x }
return the same array in opposite orders.
To elaborate on what Tate Johnson added, if you implement the comparison function <=>
on any of your classes, you gain the following
- You may include the module
Comparable
in your class which will automatically define for you the following methods:between?
,==
,>=
,<
,<=
and>
. - Instances of your class can now be sorted using the default (ie without argument) invocation to
sort
.
Note that the <=>
method is already provided wherever it makes sense in ruby's standard library (Bignum
, Array
, File::Stat
, Fixnum
, String
, Time
, etc...).
Related Topics
Why 6.84 - 3.6 == 3.2399999999999998
Difference Between an It Block and a Specify Block in Rspec
What Does ':Location => ...' and 'Head :Ok' Mean in the 'Respond_To' Format Statement
Ruby Loading Config (Yaml) File in Same Dir as Source
Adding Attributes to a Ruby Object Dynamically
Rails: Money Gem Converts All Amounts to Zero
Retrieving Array of Ids in Mongoid
Tzinfo-Data Present But Not Seen
Link_To Method and Click Event in Rails
How to Change Hash Keys from 'Symbol's to 'String'S
Ruby: Create a String from Bytes
Guidelines for Where to Put Classes in Rails Apps That Don't Fit Anywhere