How can I sort by multiple conditions with different orders?
Question
I'd really like to handle this without monkey-patching but I haven't been able to find another option yet.
I have an array (in Ruby) that I need to sort by multiple conditions. I know how to use the sort method and I've used the trick on sorting using an array of options to sort by multiple conditions. However, in this case I need the first condition to sort ascending and the second to sort descending. For example:
ordered_list = [[1, 2], [1, 1], [2, 1]]
Any suggestions?
Edit: Just realized I should mention that I can't easily compare the first and second values (I'm actually working with object attributes here). So for a simple example it's more like:
ordered_list = [[1, "b"], [1, "a"], [2, "a"]]
Solution
How about:
ordered_list = [[1, "b"], [1, "a"], [2, "a"]]
ordered_list.sort! do |a,b|
[a[0],b[1]] <=> [b[0], a[1]]
end
OTHER TIPS
I was having a nightmare of a time trying to figure out how to reverse sort a specific attribute but normally sort the other two. Just a note about the sorting for those that come along after this and are confused by the |a,b| block syntax. You cannot use the {|a,b| a.blah <=> b.blah}
block style with sort_by!
or sort_by
. It must be used with sort!
or sort
. Also, as indicated previously by the other posters swap a
and b
across the comparison operator <=>
to reverse the sort order. Like this:
To sort by blah and craw normally, but sort by bleu in reverse order do this:
something.sort!{|a,b| [a.blah, b.bleu, a.craw] <=> [b.blah, a.bleu, b.craw]}
It is also possible to use the -
sign with sort_by
or sort_by!
to do a reverse sort on numerals (as far as I am aware it only works on numbers so don't try it with strings as it just errors and kills the page).
Assume a.craw
is an integer. For example:
something.sort_by!{|a| [a.blah, -a.craw, a.bleu]}
I had this same basic problem, and solved it by adding this:
class Inverter
attr_reader :o
def initialize(o)
@o = o
end
def <=>(other)
if @o.is && other.o.is
-(@o <=> other.o)
else
@o <=> other.o
end
end
end
This is a wrapper that simply inverts the <=> function, which then allows you to do things like this:
your_objects.sort_by {|y| [y.prop1,Inverter.new(y.prop2)]}
Enumerable#multisort
is a generic solution that can be applied to arrays of any size, not just those with 2 items. Arguments are booleans that indicate whether a specific field should be sorted ascending or descending (usage below):
items = [
[3, "Britney"],
[1, "Corin"],
[2, "Cody"],
[5, "Adam"],
[1, "Sally"],
[2, "Zack"],
[5, "Betty"]
]
module Enumerable
def multisort(*args)
sort do |a, b|
i, res = -1, 0
res = a[i] <=> b[i] until !res.zero? or (i+=1) == a.size
args[i] == false ? -res : res
end
end
end
items.multisort(true, false)
# => [[1, "Sally"], [1, "Corin"], [2, "Zack"], [2, "Cody"], [3, "Britney"], [5, "Betty"], [5, "Adam"]]
items.multisort(false, true)
# => [[5, "Adam"], [5, "Betty"], [3, "Britney"], [2, "Cody"], [2, "Zack"], [1, "Corin"], [1, "Sally"]]
I've been using Glenn's recipe for quite a while now. Tired of copying code from project to project over and over again, I've decided to make it a gem: