user9666612
user9666612

Reputation:

How to sort hash in Ruby

I have the following hash:

scores = {
  "Charlie" => 0
  "Delta" => 5 
  "Beta" => 2
  "Alpha" => 0
}

The numbers listed are integers, and the teams are represented as strings.

How can I sort the list by score, and if there is a tie, list it alphabetically, and then output it?

I imagine the intended output to look like:

1. Delta, 5 pts
2. Beta, 2 pts
3. Alpha, 0 pts
4. Charlie, 0 pts

I sorted the hash, but am not sure how to sort by alphabetical order if there is a tie. The code I used is below:

scores = Hash[ scores.sort_by { |team_name, scores_array| scores_array.sum.to_s } ]

Upvotes: 1

Views: 652

Answers (5)

Melechesh
Melechesh

Reputation: 1

You can try this

scores = {
  'Charlie' => 0,
  'Delta' => 5,
  'Beta' => 2,
  'Alpha' => 0
}

scores_sorted = scores.sort_by { |_key, value| -value }

scores_sorted.each.with_index(1) do |value, index|
 puts "#{index}. #{value[0]}, #{value[1]} pts"
end

Upvotes: 0

ku'
ku'

Reputation: 102

You can sort it like this

scores = {
  "Charlie" => 0,
  "Delta" => 5,
  "Beta" => 2,
  "Alpha" => 0
}
puts scores
  .map{|name, score| [-score, name]}
  .sort
  .zip((0...scores.size).to_a)
  .map{|(score, name), i| "#{i + 1}. #{name}, #{-score} pts"}

notice the minus score. It a trick to sort integer reversely.

Upvotes: 0

pamcevoy
pamcevoy

Reputation: 1246

You can use <=> to compare and do it in a block where you first compare by value (score) and if those match, then compare by key (name).

scores = {
  "Alpha" => 0,
  "Beta" => 2,
  "Charlie" => 0,
  "Delta" => 5,
}

# convert to an array of arrays
# => [[Alpha, 0], [Beta, 2], [Charlie, 0], [Delta, 5]]
# then sort by value and, if needed, by key
scores = scores.to_a.sort! { |a,b|
  cmp = a[1] <=> b[1]             # sort by value (index = 1)
  cmp = a[0] <=> b[0] if cmp == 0 # sort by key (index = 0) if values matched
  cmp
}.to_h # convert back to a hash

puts scores

Or if you want to extract the comparison code into a method for reuse/clarity, you can have it call the method.

# Compare entries from the scores hash.
# Each entry has a name (key) and score (value) (e.g. ["Alpha", 0].
# First compare by scores, then by names (if scores match).
def compare_score_entries(a, b)
  cmp = a[1] <=> b[1]
  cmp = a[0] <=> b[0] if cmp == 0
  return cmp
end

scores = scores.sort(&method(:compare_score_entries)).to_h

Upvotes: 1

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369458

In Ruby, Arrays are lexicographically ordered. You can make use of that fact for sorting by multiple keys: just create an array of the keys you want to sort by:

scores.sort_by {|team, score| [-score, team] }.to_h
#=> {'Delta' => 5, 'Beta' => 2, 'Alpha' => 0, 'Charlie' => 0}

Upvotes: 4

Schwern
Schwern

Reputation: 164809

The general pattern for sorting a Hash into a sorted Array of key/value pairs looks like this.

sorted_array_of_tuples = hash.sort { |a,b| ... }

A Hash has the Enumerable mixin which means it can use sort. Used on a Hash sort compares tuples (two element Arrays) of the key and value, so we can craft block that compares both the key and value easily.

Usually you then use the sorted Array.

sorted = hash.sort { ... }
sorted.each { |t|
    puts "#{t[0]}: #{t[1]}"
}

But you can turn it back into a Hash with to_h. Since Hashes in Ruby remember the order their keys were inserted, the newly minted Hash created from the sorted Array will retain its sorting.

sorted_hash = hash.sort { |a,b| ... }.to_h

...but if more keys are added to sorted_hash they will not be sorted, they'll just go at the end. So you'll probably want to freeze the sorted hash to prevent modifications ruining the sorting.

sorted_hash.freeze

As for the sort block, in other languages an idiom of "compare by this, and if they're equal, compare by that" would look like this:

sorted_scores = scores.sort { |a,b|
    # Compare values or compare keys
    a[1] <=> b[1]    || a[0] <=> b[0]
}

This takes advantage that <=> returns 0 when they're equal. In other languages 0 is false so you can use logical operators to create a whole chain of primary, secondary, and tertiary comparisons.

But in Ruby 0 isn't false. Only false is false. Usually this is a good thing, but here it means we need to be more specific.

sorted_scores = scores.sort { |a,b|
  # Compare values
  cmp = a[1] <=> b[1]
  # Compare keys if the values were equal
  cmp = a[0] <=> b[0] if cmp == 0
  # Return the comparison
  cmp
}

Upvotes: 2

Related Questions