327

I have an array of hashes:

[
  { :foo => 'foo', :bar => 2 },
  { :foo => 'foo', :bar => 3 },
  { :foo => 'foo', :bar => 5 },
]

I am trying to sort this array in descending order according to the value of :bar in each hash.

I am using sort_by to sort above array:

a.sort_by { |h| h[:bar] }

However, this sorts the array in ascending order. How do I make it sort in descending order?

One solution was to do following:

a.sort_by { |h| -h[:bar] }

But that negative sign does not seem appropriate.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Waseem
  • 8,232
  • 9
  • 43
  • 54
  • 4
    Given the other options I still think -h[:bar] is the most elegant. What do you not like about it? – Michael Kohl Apr 15 '10 at 10:34
  • 3
    I was much more interested on conveying the intent of code. – Waseem Apr 17 '10 at 09:58
  • 1
    @Waseem could I trouble you to update the accepted answer? – colllin Feb 22 '13 at 03:05
  • @collindo What's wrong with the currently accepted answer? – Waseem Feb 25 '13 at 06:49
  • 7
    @Waseem There's nothing wrong with the current answer. There's just happens to be a far better answer. the Tin Man's answer is much more thorough and shows that `sort_by.reverse` is dramatically more efficient than the currently accepted answer. I believe it also better addresses the concern you mentioned above for "conveying the intent of code". On top of that, the Tin Man has updated his answer for the current version of ruby. This question has been viewed over 15k times. If you can save even 1 second of each viewer's time, I think it's worth it. – colllin Feb 26 '13 at 05:36
  • 2
    Here it is 2016 and we have another way (since v2.2): `arr.max_by(arr.size) { |h| h[:bar] } #=> [{:foo=>"foo", :bar=>5}, {:foo=>"foo", :bar=>3}, {:foo=>"foo", :bar=>2}]`. – Cary Swoveland May 24 '16 at 06:57

8 Answers8

657

It's always enlightening to do a benchmark on the various suggested answers. Here's what I found out:

#!/usr/bin/ruby

require 'benchmark'

ary = []
1000.times { 
  ary << {:bar => rand(1000)} 
}

n = 500
Benchmark.bm(20) do |x|
  x.report("sort")               { n.times { ary.sort{ |a,b| b[:bar] <=> a[:bar] } } }
  x.report("sort reverse")       { n.times { ary.sort{ |a,b| a[:bar] <=> b[:bar] }.reverse } }
  x.report("sort_by -a[:bar]")   { n.times { ary.sort_by{ |a| -a[:bar] } } }
  x.report("sort_by a[:bar]*-1") { n.times { ary.sort_by{ |a| a[:bar]*-1 } } }
  x.report("sort_by.reverse!")   { n.times { ary.sort_by{ |a| a[:bar] }.reverse } }
end

                          user     system      total        real
sort                  3.960000   0.010000   3.970000 (  3.990886)
sort reverse          4.040000   0.000000   4.040000 (  4.038849)
sort_by -a[:bar]      0.690000   0.000000   0.690000 (  0.692080)
sort_by a[:bar]*-1    0.700000   0.000000   0.700000 (  0.699735)
sort_by.reverse!      0.650000   0.000000   0.650000 (  0.654447)

I think it's interesting that @Pablo's sort_by{...}.reverse! is fastest. Before running the test I thought it would be slower than "-a[:bar]" but negating the value turns out to take longer than it does to reverse the entire array in one pass. It's not much of a difference, but every little speed-up helps.


Please note that these results are different in Ruby 1.9

Here are results for Ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-darwin10.8.0]:

                           user     system      total        real
sort                   1.340000   0.010000   1.350000 (  1.346331)
sort reverse           1.300000   0.000000   1.300000 (  1.310446)
sort_by -a[:bar]       0.430000   0.000000   0.430000 (  0.429606)
sort_by a[:bar]*-1     0.420000   0.000000   0.420000 (  0.414383)
sort_by.reverse!       0.400000   0.000000   0.400000 (  0.401275)

These are on an old MacBook Pro. Newer, or faster machines, will have lower values, but the relative differences will remain.


Here's a bit updated version on newer hardware and the 2.1.1 version of Ruby:

#!/usr/bin/ruby

require 'benchmark'

puts "Running Ruby #{RUBY_VERSION}"

ary = []
1000.times {
  ary << {:bar => rand(1000)}
}

n = 500

puts "n=#{n}"
Benchmark.bm(20) do |x|
  x.report("sort")               { n.times { ary.dup.sort{ |a,b| b[:bar] <=> a[:bar] } } }
  x.report("sort reverse")       { n.times { ary.dup.sort{ |a,b| a[:bar] <=> b[:bar] }.reverse } }
  x.report("sort_by -a[:bar]")   { n.times { ary.dup.sort_by{ |a| -a[:bar] } } }
  x.report("sort_by a[:bar]*-1") { n.times { ary.dup.sort_by{ |a| a[:bar]*-1 } } }
  x.report("sort_by.reverse")    { n.times { ary.dup.sort_by{ |a| a[:bar] }.reverse } }
  x.report("sort_by.reverse!")   { n.times { ary.dup.sort_by{ |a| a[:bar] }.reverse! } }
end

# >> Running Ruby 2.1.1
# >> n=500
# >>                            user     system      total        real
# >> sort                   0.670000   0.000000   0.670000 (  0.667754)
# >> sort reverse           0.650000   0.000000   0.650000 (  0.655582)
# >> sort_by -a[:bar]       0.260000   0.010000   0.270000 (  0.255919)
# >> sort_by a[:bar]*-1     0.250000   0.000000   0.250000 (  0.258924)
# >> sort_by.reverse        0.250000   0.000000   0.250000 (  0.245179)
# >> sort_by.reverse!       0.240000   0.000000   0.240000 (  0.242340)

New results running the above code using Ruby 2.2.1 on a more recent Macbook Pro. Again, the exact numbers aren't important, it's their relationships:

Running Ruby 2.2.1
n=500
                           user     system      total        real
sort                   0.650000   0.000000   0.650000 (  0.653191)
sort reverse           0.650000   0.000000   0.650000 (  0.648761)
sort_by -a[:bar]       0.240000   0.010000   0.250000 (  0.245193)
sort_by a[:bar]*-1     0.240000   0.000000   0.240000 (  0.240541)
sort_by.reverse        0.230000   0.000000   0.230000 (  0.228571)
sort_by.reverse!       0.230000   0.000000   0.230000 (  0.230040)

Updated for Ruby 2.7.1 on a Mid-2015 MacBook Pro:

Running Ruby 2.7.1
n=500     
                           user     system      total        real
sort                   0.494707   0.003662   0.498369 (  0.501064)
sort reverse           0.480181   0.005186   0.485367 (  0.487972)
sort_by -a[:bar]       0.121521   0.003781   0.125302 (  0.126557)
sort_by a[:bar]*-1     0.115097   0.003931   0.119028 (  0.122991)
sort_by.reverse        0.110459   0.003414   0.113873 (  0.114443)
sort_by.reverse!       0.108997   0.001631   0.110628 (  0.111532)

...the reverse method doesn't actually return a reversed array - it returns an enumerator that just starts at the end and works backwards.

The source for Array#reverse is:

               static VALUE
rb_ary_reverse_m(VALUE ary)
{
    long len = RARRAY_LEN(ary);
    VALUE dup = rb_ary_new2(len);

    if (len > 0) {
        const VALUE *p1 = RARRAY_CONST_PTR_TRANSIENT(ary);
        VALUE *p2 = (VALUE *)RARRAY_CONST_PTR_TRANSIENT(dup) + len - 1;
        do *p2-- = *p1++; while (--len > 0);
    }
    ARY_SET_LEN(dup, RARRAY_LEN(ary));
    return dup;
}

do *p2-- = *p1++; while (--len > 0); is copying the pointers to the elements in reverse order if I remember my C correctly, so the array is reversed.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • Please note that these results are different in Ruby 1.9 – NARKOZ Sep 15 '12 at 17:48
  • 1
    The times got better with 1.9.3, but `sort_by` is still faster, as is `sort_by.reverse!`. – the Tin Man Sep 17 '12 at 05:56
  • 10
    @theTinMan Can you please provide a TL;DR for your answer. All this benchmark information is quite useful. But a TL;DR on top of the answer will be useful for people who just want the answer. I know they should read the whole explanation and I think they will. Still a TL;DR will be quite useful IMHO. Thanks for your effort. – Waseem Feb 26 '13 at 18:45
  • 4
    The TL;DR answer is to read the results. This is important information to know, so either skim it, or study it, either way take what you get from it. FYI, if you were on my team I'm give you a test on it if I thought you skimmed it. – the Tin Man Jun 25 '13 at 04:26
  • 9
    I agree with @Waseem. As well-researched as this answer is, the OP did not ask "what's the fastest way to do a descending sort in Ruby". A TL;DR at the top showing simple uses, followed by the benchmarks, would improve this answer IMO. –  Jan 09 '15 at 00:57
  • 1
    @NathanH, Stack Overflow isn't a discussion board. You can't jump into a Q&A, add a comment, and expect to get help. Instead, read "[ask]" and create a new question that explains what you're doing, provide the minimal data, expected output, and your minimal code that explains what you want showing what you've written to solve the problem. – the Tin Man Dec 08 '15 at 20:42
  • @Waseem and user2189331 yall should look into "cargo cult programming" sometime :-) – Todd Jun 01 '16 at 16:31
  • 1
    @theTinMan the `reverse` method doesn't actually return a reversed array - it returns an enumerator that just starts at the end and works backwards. That's why it's faster than negation. It's doing the same thing `each` would do but in reverse. – jakeonrails Feb 01 '17 at 23:51
  • `sort_by` has a tradeoff though, which is in memory. this benchmark misses that. but check out the details on the Why `sort_by` is faster here: http://agelber.com/blog/ruby-sort-vs-sort-by – Alex Moore-Niemi Feb 14 '17 at 16:22
  • Yes, the trade-off is increased memory usage, but that's a common problem. We can do it fast and consume memory or we can conserve memory and do it more slowly. Knowing which will occur when is why we do benchmarks. – the Tin Man Feb 14 '17 at 19:23
  • 2
    TL;DR use `sort_by().reverse` or `.reverse!` unless you care about memory. – Cyril Duchon-Doris Feb 28 '17 at 11:30
  • 1
    @jakeonrails I looked at the source and the code actually reverses the array. Returning a `reverse_each` enumerator seems like it'd be better, but that's not what is in the code for https://ruby-doc.org/core-2.7.1/Array.html#method-i-reverse – the Tin Man Apr 24 '20 at 22:05
  • "This is important information to know" -- have to disagree. optimizing without profiling first is a waste of dev time, in all cases. good luck with those tests. – johncip Mar 01 '23 at 02:42
103

Just a quick thing, that denotes the intent of descending order.

descending = -1
a.sort_by { |h| h[:bar] * descending }

(Will think of a better way in the mean time) ;)


a.sort_by { |h| h[:bar] }.reverse!
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Pablo Fernandez
  • 103,170
  • 56
  • 192
  • 232
  • Pablo, nice job on finding a better way! See the benchmark I did. – the Tin Man Apr 16 '10 at 06:25
  • the first method is faster (though maybe uglier) because it only loops once. As for the second, you don't need the !, that's for inplace operations. – tokland Nov 19 '10 at 12:13
  • 5
    If you don't use the bang after reverse you won't be reversing the array but creating another one that's reversed. – Pablo Fernandez Nov 19 '10 at 15:17
  • Does anyone know if the reverse is an O(N) operation or if it does something clever internally with the pointers? I guess it creates a new one it has to be O(N), but is the reverse! also? – user137717 May 02 '22 at 20:43
60

You could do:

a.sort{|a,b| b[:bar] <=> a[:bar]}
JRL
  • 76,767
  • 18
  • 98
  • 146
  • 6
    but the whole point of using `sort_by` was that it avoided running the comparison function so many times – user102008 Sep 28 '11 at 19:33
  • 3
    -1. `sort_by` is so much more efficient, and more readable. Negating the value or doing a reverse at the end will be faster and more readable. – Marc-André Lafortune Nov 05 '12 at 01:20
  • 1
    I like this answer because `* -1` doesn't work with all values (e.g. Time), and `reverse` will re-order values that sorted as equal – Abe Voelker Feb 07 '20 at 16:21
11

I see that we have (beside others) basically two options:

a.sort_by { |h| -h[:bar] }

and

a.sort_by { |h| h[:bar] }.reverse

While both ways give you the same result when your sorting key is unique, keep in mind that the reverse way will reverse the order of keys that are equal.

Example:

a = [{foo: 1, bar: 1},{foo: 2,bar: 1}]
a.sort_by {|h| -h[:bar]}
 => [{:foo=>1, :bar=>1}, {:foo=>2, :bar=>1}]
a.sort_by {|h| h[:bar]}.reverse
 => [{:foo=>2, :bar=>1}, {:foo=>1, :bar=>1}]

While you often don't need to care about this, sometimes you do. To avoid such behavior you could introduce a second sorting key (that for sure needs to be unique at least for all items that have the same sorting key):

a.sort_by {|h| [-h[:bar],-h[:foo]]}
 => [{:foo=>2, :bar=>1}, {:foo=>1, :bar=>1}]
a.sort_by {|h| [h[:bar],h[:foo]]}.reverse
 => [{:foo=>2, :bar=>1}, {:foo=>1, :bar=>1}]
Niklas Hoesl
  • 320
  • 3
  • 8
  • +1 for pointing out that the semantics of `reverse` are different. I believe it will also mess up a previous sort, in the case where one is trying to apply multiple sorts in some order. – johncip Jun 06 '18 at 07:01
7

What about:

 a.sort {|x,y| y[:bar]<=>x[:bar]}

It works!!

irb
>> a = [
?>   { :foo => 'foo', :bar => 2 },
?>   { :foo => 'foo', :bar => 3 },
?>   { :foo => 'foo', :bar => 5 },
?> ]
=> [{:bar=>2, :foo=>"foo"}, {:bar=>3, :foo=>"foo"}, {:bar=>5, :foo=>"foo"}]

>>  a.sort {|x,y| y[:bar]<=>x[:bar]}
=> [{:bar=>5, :foo=>"foo"}, {:bar=>3, :foo=>"foo"}, {:bar=>2, :foo=>"foo"}]
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
OscarRyz
  • 196,001
  • 113
  • 385
  • 569
  • Yeah it actually works, but I think the PO wants to show intent with the code (he has a working solution already). – Pablo Fernandez Apr 15 '10 at 01:42
  • While `sort` will work, it's only faster when sorting immediate values. If you have to dig for them `sort_by` is faster. See the benchmark. – the Tin Man Nov 10 '19 at 05:15
4

Regarding the benchmark suite mentioned, these results also hold for sorted arrays.

sort_by/reverse it is:

# foo.rb
require 'benchmark'

NUM_RUNS = 1000

# arr = []
arr1 = 3000.times.map { { num: rand(1000) } }
arr2 = 3000.times.map { |n| { num: n } }.reverse

Benchmark.bm(20) do |x|
  { 'randomized'     => arr1,
    'sorted'         => arr2 }.each do |label, arr|
    puts '---------------------------------------------------'
    puts label

    x.report('sort_by / reverse') {
      NUM_RUNS.times { arr.sort_by { |h| h[:num] }.reverse }
    }
    x.report('sort_by -') {
      NUM_RUNS.times { arr.sort_by { |h| -h[:num] } }
    }
  end
end

And the results:

$: ruby foo.rb
                           user     system      total        real
---------------------------------------------------
randomized
sort_by / reverse      1.680000   0.010000   1.690000 (  1.682051)
sort_by -              1.830000   0.000000   1.830000 (  1.830359)
---------------------------------------------------
sorted
sort_by / reverse      0.400000   0.000000   0.400000 (  0.402990)
sort_by -              0.500000   0.000000   0.500000 (  0.499350)
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • You should be able to do sort_by{}.reverse! (reverse without the bang creates an new array and I'd expect that to be slower of course) – Bibek Shrestha Sep 15 '17 at 18:34
3

For those folks who like to measure speed in IPS ;)

require 'benchmark/ips'

ary = []
1000.times { 
  ary << {:bar => rand(1000)} 
}

Benchmark.ips do |x|
  x.report("sort")               { ary.sort{ |a,b| b[:bar] <=> a[:bar] } }
  x.report("sort reverse")       { ary.sort{ |a,b| a[:bar] <=> b[:bar] }.reverse }
  x.report("sort_by -a[:bar]")   { ary.sort_by{ |a| -a[:bar] } }
  x.report("sort_by a[:bar]*-1") { ary.sort_by{ |a| a[:bar]*-1 } }
  x.report("sort_by.reverse!")   { ary.sort_by{ |a| a[:bar] }.reverse }
  x.compare!
end

And results:

Warming up --------------------------------------
                sort    93.000  i/100ms
        sort reverse    91.000  i/100ms
    sort_by -a[:bar]   382.000  i/100ms
  sort_by a[:bar]*-1   398.000  i/100ms
    sort_by.reverse!   397.000  i/100ms
Calculating -------------------------------------
                sort    938.530  (± 1.8%) i/s -      4.743k in   5.055290s
        sort reverse    901.157  (± 6.1%) i/s -      4.550k in   5.075351s
    sort_by -a[:bar]      3.814k (± 4.4%) i/s -     19.100k in   5.019260s
  sort_by a[:bar]*-1      3.732k (± 4.3%) i/s -     18.706k in   5.021720s
    sort_by.reverse!      3.928k (± 3.6%) i/s -     19.850k in   5.060202s

Comparison:
    sort_by.reverse!:     3927.8 i/s
    sort_by -a[:bar]:     3813.9 i/s - same-ish: difference falls within error
  sort_by a[:bar]*-1:     3732.3 i/s - same-ish: difference falls within error
                sort:      938.5 i/s - 4.19x  slower
        sort reverse:      901.2 i/s - 4.36x  slower
mpospelov
  • 1,510
  • 1
  • 15
  • 24
2

Simple Solution from ascending to descending and vice versa is:

STRINGS

str = ['ravi', 'aravind', 'joker', 'poker']
asc_string = str.sort # => ["aravind", "joker", "poker", "ravi"]
asc_string.reverse # => ["ravi", "poker", "joker", "aravind"]

DIGITS

digit = [234,45,1,5,78,45,34,9]
asc_digit = digit.sort # => [1, 5, 9, 34, 45, 45, 78, 234]
asc_digit.reverse # => [234, 78, 45, 45, 34, 9, 5, 1]
Ravistm
  • 2,163
  • 25
  • 25