I'm trying to sort an array of objects based upon different attributes. Some of those attributes I would like to sort in ascending order and some in descending order. I have been able to sort by ascending or descending but have been unable to combine the two.
Here is the simple class I am working with:
class Dog
attr_reader :name, :gender
DOGS = []
def initialize(name, gender)
@name = name
@gender = gender
DOGS << self
end
def self.all
DOGS
end
def self.sort_all_by_gender_then_name
self.all.sort_by { |d| [d.gender, d.name] }
end
end
I can then instantiate some dogs to be sorted later.
@rover = Dog.new("Rover", "Male")
@max = Dog.new("Max", "Male")
@fluffy = Dog.new("Fluffy", "Female")
@cocoa = Dog.new("Cocoa", "Female")
I can then use the sort_all_by_gender_then_name method.
Dog.sort_all_by_gender_then_name
=> [@cocoa, @fluffy, @max, @rover]
The array it returns includes females first, then males, all sorted by name in ascending order.
But what if I want to have gender be descending, and then name ascending, so that it would be males first and then sorted by name ascending. In this case:
=> [@max, @rover, @cocoa, @fluffy]
Or, if I wanted it by gender ascending, but name descending:
=> [@fluffy, @cocoa, @rover, @max]
When sorting numerical values, you can prepend a - to make it sort in reverse. However, I have been unable to find a way to do this with strings. Any help or ideas would be appreciated. Thanks.
sort() method sorts the elements of a list in ascending or descending order using the default < comparisons operator between items. Use the key parameter to pass the function name to be used for comparison instead of the default < operator. Set the reverse parameter to True, to get the list in descending order.
Here's one way to do it using .sort
instead of .sort_by
:
dogs = [
{ name: "Rover", gender: "Male" },
{ name: "Max", gender: "Male" },
{ name: "Fluffy", gender: "Female" },
{ name: "Cocoa", gender: "Female" }
]
# gender asc, name asc
p(dogs.sort do |a, b|
[a[:gender], a[:name]] <=> [b[:gender], b[:name]]
end)
# gender desc, name asc
p(dogs.sort do |a, b|
[b[:gender], a[:name]] <=> [a[:gender], b[:name]]
end)
# gender asc, name desc
p(dogs.sort do |a, b|
[a[:gender], b[:name]] <=> [b[:gender], a[:name]]
end)
Output:
[{:name=>"Cocoa", :gender=>"Female"}, {:name=>"Fluffy", :gender=>"Female"}, {:name=>"Max", :gender=>"Male"}, {:name=>"Rover", :gender=>"Male"}]
[{:name=>"Max", :gender=>"Male"}, {:name=>"Rover", :gender=>"Male"}, {:name=>"Cocoa", :gender=>"Female"}, {:name=>"Fluffy", :gender=>"Female"}]
[{:name=>"Fluffy", :gender=>"Female"}, {:name=>"Cocoa", :gender=>"Female"}, {:name=>"Rover", :gender=>"Male"}, {:name=>"Max", :gender=>"Male"}]
Basically, this is doing something similar to negating numbers (as you mentioned in the question), by swapping the property to the other element if it needs to be sorted in descending order.
This ReversedOrder
mixin can help you accomplish the mixed direction sorts on seperate attributes, using sort_by
:
module ReversedOrder
def <=>(other)
- super
end
end
Use example:
dogs = [
{ name: "Rover", gender: "Male" },
{ name: "Max", gender: "Male" },
{ name: "Fluffy", gender: "Female" },
{ name: "Cocoa", gender: "Female" }
]
dogs.sort_by {|e| [e[:gender], e[:name]] }
=> [{:name=>"Cocoa", :gender=>"Female"},
{:name=>"Fluffy", :gender=>"Female"},
{:name=>"Max", :gender=>"Male"},
{:name=>"Rover", :gender=>"Male"}]
dogs.sort_by {|e| [e[:gender].dup.extend(ReversedOrder), e[:name]] }
=> [{:name=>"Max", :gender=>"Male"},
{:name=>"Rover", :gender=>"Male"},
{:name=>"Cocoa", :gender=>"Female"},
{:name=>"Fluffy", :gender=>"Female"}]
Note: Be careful to dup
the reversed element. Without that, you will mixin the comparison inverter to the actual object instead of just the key being made for sort_by
and it will forever produce reversed comparisons.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With