Ruby sort by multiple values?
a.sort { |a, b| [a['foo'], a['bar']] <=> [b['foo'], b['bar']] }
Sorting array of hashes based on multiple values
arr = [
{
:title=>"Working as a SSE",
:organisation=>{:id=>428, :name=>"google"},
:from=>"2018-6-1",
:to=>"2017-6-1"
}, {
:title=>"Concatenate two video files to single video in players",
:organisation=>{:id=>197, :name=>"UNFPA"},
:from=>"2014-1-1",
:to=>"2015-12-1"
}, {
:title=>"Highcharts Demo",
:organisation=>{:id=>6, :name=>"UNDFS"},
:from=>"2016-1-1",
:to=>"2017-6-1"
}, {
:title=>"Working as a Judicial Affairs",
:organisation=>{:id=>427, :name=>"swtp"},
:from=>"2017-1-1",
:to=>"2018-6-1"
}
]
arr.sort_by { |h| [h[:to], h[:from], h[:organisation][:name], h[:title]] }
#=> [{:title=>"Concatenate two video files to single video in players",
# :organisation=>{:id=>197, :name=>"UNFPA"},
# :from=>"2014-1-1",
# :to=>"2015-12-1"},
# {:title=>"Highcharts Demo",
# :organisation=>{:id=>6, :name=>"UNDFS"},
# :from=>"2016-1-1",
# :to=>"2017-6-1"},
# {:title=>"Working as a SSE",
# :organisation=>{:id=>428, :name=>"google"},
# :from=>"2018-6-1",
# :to=>"2017-6-1"},
# {:title=>"Working as a Judicial Affairs",
# :organisation=>{:id=>427, :name=>"swtp"},
# :from=>"2017-1-1",
# :to=>"2018-6-1"}]
See Array#<=>, particularly the third paragraph of the doc, and Enumerable#sort_by.
ruby sort_by multiple fields
Your sorting works correctly in MRI Ruby 1.8.7, 1.9.3, and 2.0.0:
Item = Struct.new(:name, :dir, :sort_dir)
entries = [Item.new('favicon.ico', false, 1), Item.new('bin', true, 0),
Item.new('web.config', false, 1), Item.new('images', true, 0),
Item.new('global.asax', false, 1), Item.new('content', true, 0)]
entries.sort_by{|e| [e.sort_dir, e.name]}
# => [#<struct Item name="bin", dir=true, sort_dir=0>,
# #<struct Item name="content", dir=true, sort_dir=0>,
# #<struct Item name="images", dir=true, sort_dir=0>,
# #<struct Item name="favicon.ico", dir=false, sort_dir=1>,
# #<struct Item name="global.asax", dir=false, sort_dir=1>,
# #<struct Item name="web.config", dir=false, sort_dir=1>]
Have you tried outputting the result of your sort_by
to a console? I'm not familiar with the render json:
portion of your code, but perhaps that's where things are going wrong. My best guess is that somehow in the conversion to JSON (if that's what it does) the sorting is getting messed up.
My other idea is that perhaps you expect sort_by
to modify entries
; it does not. If you want entries
itself to be sorted after the call, use sort_by!
(note the !
at the end of the method name).
Update: It looks like the issue is that you want a case-insensitive search. Simply adding upcase
should do the trick:
entries.sort_by{|e| [e.sort_dir, e.name.upcase]}
Ruby sort by multiple fields and multilple directions for different data types
Very interesting problem. I also think the sort_by
method would be most helpful.
My solution (for numerical values only) works like this:
DIRECTION_MULTIPLIER = { asc: 1, desc: -1 }
def multi_sort(items, order)
items.sort_by do |item|
order.collect do |key, direction|
item[key]*DIRECTION_MULTIPLIER[direction]
end
end
end
# ... items ...
multi_sort(items, a_sort: :asc, display_sort: :desc)
The idea is to construct a list for each item passed by sort_by
. This list consists out of all values for which a sort order was given. Hence, we use that Ruby knows that [1,2]
is smaller than [1,3]
but greater than [0,0]
.
An interesting part to note is that the last parameters for the function will be passed as one Hash and the order of these hash pairs will be maintained. This "ordered" behavior in Hashes is not necessarily true for all languages, but the Ruby documentation states: Hashes enumerate their values in the order that the corresponding keys were inserted
.
-- Edit for more generality --
Since, chamnap asked for a more general solution which works with arbitrary data types and nil
, here a more comprehensive solution which relies on the <=>
operator:
require 'date'
DIRECTION_MULTIPLIER = { asc: 1, desc: -1 }
# Note: nil will be sorted towards the bottom (regardless if :asc or :desc)
def multi_sort(items, order)
items.sort do |this, that|
order.reduce(0) do |diff, order|
next diff if diff != 0 # this and that have differed at an earlier order entry
key, direction = order
# deal with nil cases
next 0 if this[key].nil? && that[key].nil?
next 1 if this[key].nil?
next -1 if that[key].nil?
# do the actual comparison
comparison = this[key] <=> that[key]
next comparison * DIRECTION_MULTIPLIER[direction]
end
end
end
I am using the sort
method. The block gets called each time the sort function needs to compare to elements. The block shall return -1, 0 or 1 (smaller, equal or higher in the order) for the respective pair. Within this sort block I am going through the order
hash which contains the key and the direction for a hash value in items. If we have found an earlier difference in order (e.g. the first key was higher) we just return that value. If the past comparisons came up with equal order, we use the <=>
operator to compare the two elements passed to the sort block (and multiply the result it with -1 if we want descending order). The only annoying thing is to deal with nil
values, which adds the three lines above the actual comparison.
And here my test code:
items = [ {n: 'ABC ', a: 1, b: Date.today+2},
{n: 'Huhu ', a: nil, b: Date.today-1},
{n: 'Man ', a: nil, b: Date.today},
{n: 'Woman', a: nil, b: Date.today},
{n: 'DEF ', a: 7, b: Date.today-1}]
multi_sort(items, b: :asc, a: :desc, n: :asc)
On a more general note: Since the logic for sorting becomes a little more complicated, I would wrap the data in actual objects with attributes. Then you could overwrite the <=>
operator as seen here.
Sorting multiple values by ascending and descending
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.
Sorting: Sort array based on multiple conditions in Ruby
You should always use sort_by
for a keyed sort. Not only is it much more readable, it is also much more efficient. In addition, I would also prefer to use destructuring bind, again, for readability:
array.sort_by {|name, age| [age, name] }
How do I sort in ruby/rails on two fields?
The best way would be to have your database do it, but if you want to use Ruby:
@games = @data.sort_by {|x| [x.game_date, x.team] }
The sorting behaviour of Array
is to sort by the first member, then the second, then the third, and so on. Since you want the date as your primary key and the team as the second, an array of those two will sort the way you want.
Sorting nested hashes by multiple values in ruby
I would do this:
hash = {12=>{:points=>0, :diff=>0}, 1=>{:points=>18, :diff=>57}, 4=>{:points=>12, :diff=>67}, 5=>{:points=>9, :diff=>62}}
hash.sort_by { |_, v| [-v[:points], v[:diff]] }.to_h
#=> {1=>{:points=>18, :diff=>57}, 4=>{:points=>12, :diff=>67}, 5=>{:points=>9, :diff=>62}, 12=>{:points=>0, :diff=>0}}
Basically, it extracts the values to sort by into a structure like this: [[0,0], [-18,57], [-12,67], [-9,62]]
. Note the -
at -v[:points]
which leads to a descending ordering. The second number is only taken into account if the first match.
Related Topics
In Ruby on Rails, After Send_File Method Delete the File from Server
How to Reference a Function in Ruby
Couldn't Require Openssl in Ruby
How to Know When to "Refresh" My Model Object in Rails
How to Say Something Happened "X Minutes Ago" or "X Hours Ago" or "X Days Ago" in Ruby
Ruby 'Encode': "\Xc3" from Ascii-8Bit to Utf-8 (Encoding::Undefinedconversionerror)
Creating a Model That Has a Tree Structure
Prevent Rails Test from Deleting Seed Data
Installing Jekyll on Ubuntu 14.04
Is It Possible for Rspec to Expect Change in Two Tables
Ruby CSV - Get Current Line/Row Number
Setting Up Private Github Access with Aws Elastic Beanstalk and Ruby Container
What's the Difference Between Gets.Chomp() VS. Stdin.Gets.Chomp()