joshweir
joshweir

Reputation: 5627

Sort a hash of arrays by one of the arrays in ruby

In Ruby, is there a short and sweet way to sort this hash of arrays by score descending:

scored = {:id=>[1, 2, 3], :score=>[8.3, 5, 10]}

so it looks like this?:

scored = {:id=>[3, 1, 2], :score=>[10, 8.3, 5]}

I couldnt find an example where I can sort arrays within a hash like this? I could do this with some nasty code but I feel like there should be a 1 or 2 liner that does it?

Upvotes: 0

Views: 100

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110755

order = scored[:score].each_with_index.sort_by(&:first).map(&:last).reverse
  #=> [2,0,1]
scored.update(scored) { |_,a| a.values_at *order }
  #=> {:id=>[3, 1, 2], :score=>[10, 8.3, 5]}

If scored is to not to be mutated, replace update with merge.

Some points:

  • Computing order makes it easy for the reader to understand what's going on.
  • The second line uses the form of Hash#merge that employs a block to determine the values of keys that are present in both hashes being merged (which here is all keys). This is a convenient way to modify hash values (generally), in part because the new hash is returned.
  • I sorted then reversed, rather than sorted by negated values, to make the method more rubust. (That is, the elements of the arrays that are the values can be from any class that implements <=>).
  • With Ruby 2.2+, another way to sort an array arr in descending order is to use Enumerable#max_by: arr.max_by(arr.size).to_a.

The first line could be replaced with:

arr = scored[:score]
order = arr.each_index.sort_by { |i| arr[i] }.reverse
  #=> [2,0,1]

Upvotes: 2

fl00r
fl00r

Reputation: 83680

You could use sort_by

scored = {:id=>[1, 2, 3], :score=>[8.3, 5, 10]}

scored.tap do |s| 
  s[:id] = s[:id].sort_by.with_index{ |a, i| -s[:score][i] }
  s[:score] = s[:score].sort_by{ |a| -a }
end
#=> {:id=>[3, 1, 2], :score=>[10, 8.3, 5]}

Upvotes: 3

Drenmi
Drenmi

Reputation: 8787

Here is one possible solution. It has an intermediate step, where it utilizes a zipped version of the scores object, but produces the correct output:

s = scored.values.inject(&:zip).sort_by(&:last).reverse
#=> [[3, 10], [1, 8.3], [2, 5]]

result = { id: s.map(&:first), score: s.map(&:last) }
#=> { :id => [3, 1, 2], :score => [10, 8.3, 5] }

Upvotes: 1

Related Questions