How to sort hash in Ruby - 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 } ]

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}

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
}

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

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.

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

Related

Is there a one liner or more efficient way to get this done?

Given a hash, for example:
hash = { "fish" => 2, "cat" => 3, "dog" => 1 }
I need to get:
A comma separated string for all the values, E.g. "2,3,1"
An integer that hold the total sum of the values, E.g. 6
My current code:
value_string = hash.map { |k,v| "#{v}"}.join(',')
sum = 0
hash.map { |k,v| sum += v}
You can do it like this:
hash.values.join(",") # => "2,3,1"
hash.values.inject(:+) # => 6
Here is a solution to compute both the values in single iteration (some sort of one liner)
r1,r2 = hash.reduce([nil, 0]){|m,(_,v)| [[m[0], v.to_s].compact.join(","),m[-1]+v]}
#=> ["2,3,1", 6]

Finding `uniq` values in array with their indexes

Let say I have an array
[1, 2, 3, 4, 3, 2]
I wonder how I can get the hash { 1 => 0, 4 => 3 } (value => index). I need the index of the earlier occurrence.
Could also try this
unique = array.select { |a| array.count(a) === 1 }
indexes = unique.map { |u| array.index(u) }
Hash[unique.zip(indexes)]
My algorithm is to first generate a new array of the single elements. I do that by filtering the array for the one's where the count is equal to 1. We can use the array#count method.
I then create a second array, where I map the values of the first array, to their indexes in the original array.
I then combine the values using the array#zip function and convert to a hash with Hash.
Output
{ 1 => 0, 4 => 3 }
a.each_with_index.with_object({}) { |(e, i), h| h.key?(e) ? h[e] = nil : h[e] = i }
.reject! { |k, v| v.nil? }

Replace a single element in an array

I have an array with unique elements. Is there a way to replace a certain value in it with another value without using its index value?
Examples:
array = [1,2,3,4]
if array.include? 4
# "replace 4 with 'Z'"
end
array #=> [1,2,3,'Z']
hash = {"One" => [1,2,3,4]}
if hash["One"].include? 4
# "replace 4 with 'Z'"
end
hash #=> {"One" => [1,2,3,'Z']}
p array.map { |x| x == 4 ? 'Z' : x }
# => [1, 2, 3, 'Z']
You can do it as:
array[array.index(4)] = "Z"
If the element is not necessarily in the array, then
if i = array.index(4)
array[i] = "Z"
end
You can use Array#map
array = array.map do |e|
if e == 4
'Z'
else
e
end
end
to edit the array in place, rather than creating a new array, use Array#map!
If you have more than one thing you want to replace, you can use a hash to map old to new:
replacements = {
4 => 'Z',
5 => 'five',
}
array = array.map do |e|
replacements.fetch(e, e)
end
This make uses of a feature of Hash#fetch, where if the key is not found, the second argument is used as a default.
A very simple solution that assumes there will be no duplicates and that the order doesn't matter:
hash = { 'One' => [1, 2, 3, 4] }
hash['One'].instance_eval { push 'Z' if delete 4 }
instance_eval sets the value of self to the receiver (in this case, the array [1,2,3,4]) for the duration of the block passed to it.

Each with index with object in Ruby

I am trying to iterate over an array and conditionally increment a counter. I am using index to compare to other array's elements:
elements.each_with_index.with_object(0) do |(element, index), diff|
diff += 1 unless other[index] == element
end
I can't get diff to change value even when changing it unconditionally.
This can be solved with inject:
elements.each_with_index.inject(0) do |diff, (element, index)|
diff += 1 unless other[index] == element
diff
end
But I am wondering if .each_with_index.with_object(0) is a valid construction and how to use it?
From ruby docs for each_with_object
Note that you can’t use immutable objects like numbers, true or false
as the memo. You would think the following returns 120, but since the
memo is never changed, it does not.
(1..5).each_with_object(1) { |value, memo| memo *= value } # => 1
So each_with_object does not work on immutable objects like integer.
You want to count the number of element wise differences, right?
elements = [1, 2, 3, 4, 5]
other = [1, 2, 0, 4, 5]
# ^
I'd use Array#zip to combine both arrays element wise and Array#count to count the unequal pairs:
elements.zip(other).count { |a, b| a != b } #=> 1

Check to see if an array is already sorted?

I know how to put an array in order, but in this case I just want to see if it is in order. An array of strings would be the easiest, I imagine, and answers on that front are appreciated, but an answer that includes the ability to check for order based on some arbitrary parameter is optimal.
Here's an example dataset. The name of:
[["a", 3],["b",53],["c",2]]
Where the elements are themselves arrays containing several elements, the first of which is a string. I want to see if the elements are in alphabetical order based on this string.
It looks like a generic abstraction, let's open Enumerable:
module Enumerable
def sorted?
each_cons(2).all? { |a, b| (a <=> b) <= 0 }
end
end
[["a", 3], ["b", 53],["c", 2]].sorted? #=> true
Notice that we have to write (a <=> b) <= 0 instead of a <= b because there are classes that support <=> but not the comparator operators (i.e. Array), since they do not include the module Comparable.
You also said you'd like to have the ability "to check for order based on some arbitrary parameter":
module Enumerable
def sorted_by?
each_cons(2).all? { |a, b| ((yield a) <=> (yield b)) <= 0 }
end
end
[["a", 3], ["b", 1], ["c", 2]].sorted_by? { |k, v| v } #=> false
Using lazy enumerables (Ruby >= 2.1), we can reuse Enumerable#sorted?:
module Enumerable
def sorted_by?(&block)
lazy.map(&block).sorted?
end
end
You can compare them two by two:
[["a", 3],["b",53],["c",2]].each_cons(2).all?{|p, n| (p <=> n) != 1} # => true
reduce can compare each element to the one before, and stop when it finds one out of order:
array.reduce{|prev,l| break unless l[0] >= prev[0]; l}
If it turns out the array isn't sorted, will your next action always be to sort it? For that use case (though of course depending on the number of times the array will already be sorted), you may not want to check whether it is sorted, but instead simply choose to always sort the array. Sorting an already sorted array is pretty efficient with many algorithms and merely checking whether an array is already sorted is not much less work, making checking + sorting more work than simply always sorting.
def ascending? (array)
yes = true
array.reduce { |l, r| break unless yes &= (l[0] <= r[0]); l }
yes
end
def descending? (array)
yes = true
array.reduce { |l, r| break unless yes &= (l[0] >= r[0]); l }
yes
end
Iterate over the objects and make sure each following element is >= the current element (or previous is <=, obviously) the current element.
For this to work efficiently you will want to sort during insertion.
If you are dealing with unique items, a SortedSet is also an option.
For clarification, if we patch array to allow for a sorted insertion, then we can keep the array in a sorted state:
class Array
def add_sorted(o)
size = self.size
if size == 0
self << o
elsif self.last < o
self << o
elsif self.first > o
self.insert(0, o)
else
# This portion can be improved by using a binary search instead of linear
self.each_with_index {|n, i| if n > o; self.insert(i, o); break; end}
end
end
end
a = []
12.times{a.add_sorted(Random.rand(10))}
p a # => [1, 1, 2, 2, 3, 4, 5, 5, 5, 5, 7]
or to use the built in sort:
class Array
def add_sorted2(o)
self << o
self.sort
end
end
or, if you are dealing with unique items:
require "set"
b = SortedSet.new
12.times{b << Random.rand(10)}
p b # => #<SortedSet: {1, 3, 4, 5, 6, 7, 8, 9}>
These are all way too hard. You don't have to sort, but you can use sort to check. Scrambled array below for demonstration purposes.
arr = [["b",3],["a",53],["c",2]]
arr.sort == arr # => false
p arr.sort # => [["a",53],["b",3],["c",2]]

Resources