How to find the largest value of a hash in an array of hashes - ruby

In my array, I'm trying to retrieve the key with the largest value of "value_2", so in this case, "B":
myArray = [
"A" => {
"value_1" => 30,
"value_2" => 240
},
"B" => {
"value_1" => 40,
"value_2" => 250
},
"C" => {
"value_1" => 18,
"value_2" => 60
}
]
myArray.each do |array_hash|
array_hash.each do |key, value|
if value["value_2"] == array_hash.values.max
puts key
end
end
end
I get the error:
"comparison of Hash with Hash failed (ArgumentError)".
What am I missing?

Though equivalent, the array given in the question is generally written:
arr = [{ "A" => { "value_1" => 30, "value_2" => 240 } },
{ "B" => { "value_1" => 40, "value_2" => 250 } },
{ "C" => { "value_1" => 18, "value_2" => 60 } }]
We can find the desired key as follows:
arr.max_by { |h| h.values.first["value_2"] }.keys.first
#=> "B"
See Enumerable#max_by. The steps are:
g = arr.max_by { |h| h.values.first["value_2"] }
#=> {"B"=>{"value_1"=>40, "value_2"=>250}}
a = g.keys
#=> ["B"]
a.first
#=> "B"
In calculating g, for
h = arr[0]
#=> {"A"=>{"value_1"=>30, "value_2"=>240}}
the block calculation is
a = h.values
#=> [{"value_1"=>30, "value_2"=>240}]
b = a.first
#=> {"value_1"=>30, "value_2"=>240}
b["value_2"]
#=> 240
Suppose now arr is as follows:
arr << { "D" => { "value_1" => 23, "value_2" => 250 } }
#=> [{"A"=>{"value_1"=>30, "value_2"=>240}},
# {"B"=>{"value_1"=>40, "value_2"=>250}},
# {"C"=>{"value_1"=>18, "value_2"=>60}},
# {"D"=>{"value_1"=>23, "value_2"=>250}}]
and we wish to return an array of all keys for which the value of "value_2" is maximum (["B", "D"]). We can obtain that as follows.
max_val = arr.map { |h| h.values.first["value_2"] }.max
#=> 250
arr.select { |h| h.values.first["value_2"] == max_val }.flat_map(&:keys)
#=> ["B", "D"]
flat_map(&:keys) is shorthand for:
flat_map { |h| h.keys }
which returns the same array as:
map { |h| h.keys.first }
See Enumerable#flat_map.

Code
p myArray.pop.max_by{|k,v|v["value_2"]}.first
Output
"B"

I'd use:
my_array = [
"A" => {
"value_1" => 30,
"value_2" => 240
},
"B" => {
"value_1" => 40,
"value_2" => 250
},
"C" => {
"value_1" => 18,
"value_2" => 60
}
]
h = Hash[*my_array]
# => {"A"=>{"value_1"=>30, "value_2"=>240},
# "B"=>{"value_1"=>40, "value_2"=>250},
# "C"=>{"value_1"=>18, "value_2"=>60}}
k = h.max_by { |k, v| v['value_2'] }.first # => "B"
Hash[*my_array] takes the array of hashes and turns it into a single hash. Then max_by will iterate each key/value pair, returning an array containing the key value "B" and the sub-hash, making it easy to grab the key using first:
k = h.max_by { |k, v| v['value_2'] } # => ["B", {"value_1"=>40, "value_2"=>250}]

I guess the idea of your solution is looping through each hash element and compare the found minimum value with hash["value_2"].
But you are getting an error at
if value["value_2"] == array_hash.values.max
Because the array_hash.values is still a hash
{"A"=>{"value_1"=>30, "value_2"=>240}}.values.max
#=> {"value_1"=>30, "value_2"=>240}
It should be like this:
max = nil
max_key = ""
myArray.each do |array_hash|
array_hash.each do |key, value|
if max.nil? || value.values.max > max
max = value.values.max
max_key = key
end
end
end
# max_key #=> "B"
Another solution:
myArray.map{ |h| h.transform_values{ |v| v["value_2"] } }.max_by{ |k| k.values }.keys.first

You asked "What am I missing?".
I think you are missing a proper understanding of the data structures that you are using. I suggest that you try printing the data structures and take a careful look at the results.
The simplest way is p myArray which gives:
[{"A"=>{"value_1"=>30, "value_2"=>240}, "B"=>{"value_1"=>40, "value_2"=>250}, "C"=>{"value_1"=>18, "value_2"=>60}}]
You can get prettier results using pp:
require 'pp'
pp myArray
yields:
[{"A"=>{"value_1"=>30, "value_2"=>240},
"B"=>{"value_1"=>40, "value_2"=>250},
"C"=>{"value_1"=>18, "value_2"=>60}}]
This helps you to see that myArray has only one element, a Hash.
You could also look at the expression array_hash.values.max inside the loop:
myArray.each do |array_hash|
p array_hash.values
end
gives:
[{"value_1"=>30, "value_2"=>240}, {"value_1"=>40, "value_2"=>250}, {"value_1"=>18, "value_2"=>60}]
Not what you expected? :-)
Given this, what would you expect to be returned by array_hash.values.max in the above loop?
Use p and/or pp liberally in your ruby code to help understand what's going on.

Related

Implement merge method in ruby

I'm trying to implement merge method in my code.
A = { "a" => 200, "b" => 100 }
B = { "b" => 100, "c" => 300 }
So when I call A.merge_method(B) in my main function, it should return
A.merge_method(B) #=> {"a"=>200, "b"=>200, "c"=>300}
How can I implement without using merge method?
Here is the general idea: collect all the keys of any hashes to be merged, then for each of the keys, collect the values in all the hashes that have that key, and sum them.
module HashWithMergeReduce
refine Hash do
def merge_reduce(*others, &op)
hashes = [self, *others]
hash_keys = hashes.map(&:keys).inject(Set.new, &:+)
hash_keys.each do |key|
hashes_with_key = hashes.select { |hash| hash.has_key?(key) }
self[key] = hashes_with_key.map { |hash| hash[key] }.reduce(&op)
end
self
end
end
end
module TestHashWithMergeReduce
using HashWithMergeReduce
a = { "a" => 200, "b" => 100 }
b = { "b" => 100, "c" => 300 }
puts a.merge_reduce(b, &:+)
# => {"a"=>200, "b"=>200, "c"=>300}
end
I can't bring myself to add the method merge_method to the class Hash, not only because it contaminates a core class, but also because it's only applicable to a small subset of hashes, ones for which all values are numeric.
One could refine Hash, as #Amadan has done, but I think it makes more sense to simply create a method similar to module methods that behave as functions (e.g, Math::sqrt), that takes all hashes as arguments.
def sum_values_by_key(*hashes)
hashes.each_with_object(Hash.new(0)) { |g,h| g.each { |k,v| h[k] += v } }
end
sum_values_by_key({ "a" => 200, "b" => 100 }, { "b" => 100, "c" => 300 })
#=> {"a"=>200, "b"=>200, "c"=>300}
sum_values_by_key({ "a" => 200, "b" => 100 }, { "b" => 100, "c" => 300 },
{ "a" => 150, "c" => 250 })
#=> {"a"=>350, "b"=>200, "c"=>550}
This uses the form of Hash::new that defines a default value, which here is zero. The expression:
h[k] += v
expands to:
h[k] = h[k] + v
If this hash has been defined h = Hash.new(0) and h has a key k, h[k] on the right of the equality is evaluated to return the value of k. If, however, h does not have a key k, h[k] on the right returns the default value, zero, to become:
h[k] = 0 + v
I've also changed the method name to make it more meaningful.

Convert array into hash and add a counter value to the new hash

I have the following array of hashes:
[
{"BREAD" => {:price => 1.50, :discount => true }},
{"BREAD" => {:price => 1.50, :discount => true }},
{"MARMITE" => {:price => 1.60, :discount => false}}
]
And I would like to translate this array into a hash that includes the counts for each item:
Output:
{
"BREAD" => {:price => 1.50, :discount => true, :count => 2},
"MARMITE" => {:price => 1.60, :discount => false, :count => 1}
}
I have tried two approaches to translate the array into a hash.
new_cart = cart.inject(:merge)
hash = Hash[cart.collect { |item| [item, ""] } ]
Both work but then I am stumped at how to capture and pass the count value.
Expected output
{
"BREAD" => {:price => 1.50, :discount => true, :count => 2},
"MARMITE" => {:price => 1.60, :discount => false, :count => 1}
}
We are given the array:
arr = [
{"BREAD" => {:price => 1.50, :discount => true }},
{"BREAD" => {:price => 1.50, :discount => true }},
{"MARMITE" => {:price => 1.60, :discount => false}}
]
and make the assumption that each hash has a single key and if two hashes have the same (single) key, the value of that key is the same in both hashes.
The first step is create an empty hash to which will add key-value pairs:
h = {}
Now we loop through arr to build the hash h. I've added a puts statement to display intermediate values in the calculation.
arr.each do |g|
k, v = g.first
puts "k=#{k}, v=#{v}"
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count => 1 })
end
end
displays:
k=BREAD, v={:price=>1.5, :discount=>true}
k=BREAD, v={:price=>1.5, :discount=>true}
k=MARMITE, v={:price=>1.6, :discount=>false}
and returns:
#=> [{"BREAD" =>{:price=>1.5, :discount=>true}},
# {"BREAD" =>{:price=>1.5, :discount=>true}},
# {"MARMITE"=>{:price=>1.6, :discount=>false}}]
each always returns its receiver (here arr), which is not what we want.
h #=> {"BREAD"=>{:price=>1.5, :discount=>true, :count=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :count=>1}}
is the result we need. See Hash#key? (aka, has_key?), Hash#[], Hash#[]= and Hash#merge.
Now let's wrap this in a method.
def hashify(arr)
h = {}
arr.each do |g|
k, v = g.first
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count=>1 })
end
end
h
end
hashify(arr)
#=> {"BREAD"=>{:price=>1.5, :discount=>true, :count=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :count=>1}}
Rubyists would often use the method Enumerable#each_with_object to simplify.
def hashify(arr)
arr.each_with_object({}) do |g,h|
k, v = g.first
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count => 1 })
end
end
end
Compare the two methods to identify their differences. See Enumerable#each_with_object.
When, as here, the keys are symbols, Ruby allows you to use the shorthand { count: 1 } for { :count=>1 }. Moreover, she permits you to write :count = 1 or count: 1 without the braces when the hash is an argument. For example,
{}.merge('cat'=>'meow', dog:'woof', :pig=>'oink')
#=> {"cat"=>"meow", :dog=>"woof", :pig=>"oink"}
It's probably more common to see the form count: 1 when keys are symbols and for the braces to be omitted when a hash is an argument.
Here's a further refinement you might see. First create
h = arr.group_by { |h| h.keys.first }
#=> {"BREAD" =>[{"BREAD"=>{:price=>1.5, :discount=>true}},
# {"BREAD"=>{:price=>1.5, :discount=>true}}],
# "MARMITE"=>[{"MARMITE"=>{:price=>1.6, :discount=>false}}]}
See Enumerable#group_by. Now convert the values (arrays) to their sizes:
counts = h.transform_values { |arr| arr.size }
#=> {"BREAD"=>2, "MARMITE"=>1}
which can be written in abbreviated form:
counts = h.transform_values(&:size)
#=> {"BREAD"=>2, "MARMITE"=>1}
See Hash#transform_values. We can now write:
uniq_arr = arr.uniq
#=> [{"BREAD"=>{:price=>1.5, :discount=>true}},
#= {"MARMITE"=>{:price=>1.6, :discount=>false}}]
uniq_arr.each_with_object({}) do |g,h|
puts "g=#{g}"
k,v = g.first
puts " k=#{k}, v=#{v}"
h[k] = v.merge(counts: counts[k])
puts " h=#{h}"
end
which displays:
g={"BREAD"=>{:price=>1.5, :discount=>true}}
k=BREAD, v={:price=>1.5, :discount=>true}
h={"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2}}
g={"MARMITE"=>{:price=>1.6, :discount=>false}}
k=MARMITE, v={:price=>1.6, :discount=>false}
h={"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2},
"MARMITE"=>{:price=>1.6, :discount=>false, :counts=>1}}
and returns:
#=> {"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :counts=>1}}
See Array#uniq.
This did the trick:
arr = [
{ bread: { price: 1.50, discount: true } },
{ bread: { price: 1.50, discount: true } },
{ marmite: { price: 1.60, discount: false } }
]
Get the count for each occurrence of hash, add as key value pair and store:
h = arr.uniq.each { |x| x[x.first.first][:count] = arr.count(x) }
Then convert hashes into arrays, flatten to a single array then construct a hash:
Hash[*h.collect(&:to_a).flatten]
#=> {:bread=>{:price=>1.50, :discount=>true, :count=>2}, :marmite=>{:price=>1.60, :discount=>false, :count=>1}}
Combined a couple of nice ideas from here:
https://raycodingdotnet.wordpress.com/2013/08/05/array-of-hashes-into-single-hash-in-ruby/
and here:
http://carol-nichols.com/2015/08/07/ruby-occurrence-couting/

Group by hash in Ruby

My goal is to take a hash of names and numbers, for example:
hash = {
"Matt" => 30,
"Dave" => 50,
"Alex" => 60
}
and to group them by whether they achieved a "passing" score. I'd like the results to be passed as an array into two separate keys, say :pass and :fail like this:
hash = { "pass" => ["Alex", 60], "fail" => [["Matt", 30]["Dave",60]]}
I know the group_by method is what I need, but am not sure as to how I would pass the values into the new keys.
The passing grade should be decided by the user. For this example, you could use 45.
You can do somthing this in such way:
PASSING_GRADE = 45
hash.group_by {|_, v| v >= PASSING_GRADE ? 'pass' : 'fail'}
Here is result:
{"fail"=>[["Matt", 30], "pass"=>[["Alex", 60], ["Dave", 50]]]}
You could simply do something like this:
sorted = {"pass" => [], "fail" => []}
hash.each do |name, grade|
if grade >= PASSING_GRADE
sorted["pass"] << [name, grade]
else
sorted["fail"] << [name, grade]
end
end
Here are two ways:
#1
a = hash.to_a
{ "pass" => a.select { |_,v| v > 50 }, "fail" => a.reject { |_,v| v > 50 } }
#=> {"pass"=>[["Alex", 60]], "fail"=>[["Matt", 30], ["Dave", 50]]}
#2
[:pass, :fail].zip(hash.to_a.partition { |_,v| v > 50 }).to_h
#=> {:pass=>[["Alex", 60]], :fail=>[["Matt", 30], ["Dave", 50]]}
These both give the return values as arrays of tuples, which you wanted for "fail" but not for "pass". Wouldn't that make it a pain to work with? Is it not better for both to be arrays of tuples?
Consider returning hashes for values:
{"pass" => hash.select { |_,v| v > 50 }, "fail" => hash.reject {|_,v| v > 50 }}
#=> {"pass"=>{"Alex"=>60}, "fail"=>{"Matt"=>30, "Dave"=>50}}
Would that not be more convenient?

How to get the next hash element from hash?

I have this hash:
HASH = {
'x' => { :amount => 0 },
'c' => { :amount => 5 },
'q' => { :amount => 10 },
'y' => { :amount => 20 },
'n' => { :amount => 50 }
}
How can I get the key with the next highest amount from the hash?
For example, if I supply x, it should return c. If there is no higher amount, then the key with the lowest amount should be returned. That means when I supply n, then x would be returned.
Can anybody help?
I'd use something like this:
def next_higher(key)
amount = HASH[key][:amount]
sorted = HASH.sort_by { |_, v| v[:amount] }
sorted.find(sorted.method(:first)) { |_, v| v[:amount] > amount }.first
end
next_higher "x" #=> "c"
next_higher "n" #=> "x"
I'd do something like this:
def find_next_by_amount(hash, key)
sorted = hash.sort_by { |_, v| v[:amount] }
index_of_next = sorted.index { |k, _| k == key }.next
sorted.fetch(index_of_next, sorted.first).first
end
find_next_by_amount(HASH, 'x')
# => "c"
find_next_by_amount(HASH, 'n')
# => "x"
Something like that:
def next(key)
amount = HASH[key][:amount]
kv_pairs = HASH.select{ |k, v| v[:amount] > amount }
result = kv_pairs.empty? ? HASH.first.first : kv_pairs.min_by{ |k, v| v}.first
end
I'm curious, why would you want something like that? Maybe there is better solution to underlying task.
EDIT: Realized that hash isn't necessary sorted by amount, adapted code for unsorted hashes.
One way:
A = HASH.sort_by { |_,h| h[:amount] }.map(&:first)
#=> ['x', 'c', 'q', 'y', 'n']
(If HASH's keys are already in the correct order, this is is just A = HASH.keys.)
def next_one(x)
A[(A.index(x)+1)%A.size]
end
next_one 'x' #=> 'c'
next_one 'q' #=> 'y'
next_one 'n' #=> 'x'
Alternatively, you could create a hash instead of a method:
e = A.cycle
#=> #<Enumerator: ["x", "c", "q", "y", "n"]:cycle>
g = A.size.times.with_object({}) { |_,g| g.update(e.next=>e.peek) }
#=> {"x"=>"c", "c"=>"q", "q"=>"y", "y"=>"n", "n"=>"x"}

Merge two hashes and return common data

I have two hashes:
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
I need to return hash such as:
h3 = {
"a" => { "h1" => 100, "h2" => nil},
"b" => { "h1" => 200, "h2" => 254},
"c" => { "h1" => nil, "h2" => 300}
}
I am trying to use:
h1.merge(h2) { |k, val, old| { 'h1' => val, 'h2' => old } }
but it return only:
{"a"=>100, "b"=>{"h1"=>200, "h2"=>254}, "c"=>300}
without nil.
Any ideas?
The block is only invoked for duplicate keys. You can either ensure that both hashes contain all keys:
h1 = { "a" => 100, "b" => 200, "c" => nil }
h2 = { "a" => nil, "b" => 254, "c" => 300 }
h1.merge(h2) { |k, val, old| { 'h1' => val, 'h2' => old } }
#=> {"a"=>{"h1"=>100, "h2"=>nil},
# "b"=>{"h1"=>200, "h2"=>254},
# "c"=>{"h1"=>nil, "h2"=>300}}
Or build a new hash yourself, e.g. using the keys from both hashes:
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
(h1.keys | h2.keys).map { |k| [k, { 'h1' => h1[k], 'h2' => h2[k] }] }.to_h
#=> {"a"=>{"h1"=>100, "h2"=>nil},
# "b"=>{"h1"=>200, "h2"=>254},
# "c"=>{"h1"=>nil, "h2"=>300}}
If you read the official document for merge (here) it say:
the value for each duplicate key is determined by calling the block
with the key, its value in hsh and its value in other_hash.
This means for keys "a" and "c" , the block is never called (as they are not duplicate keys), as a result you are missing nil in your result hash.
You can try this instead:
h3 = {}
(h1.keys + h2.keys).uniq.each{|a| h3[a] = {"h1" => h1[a], "h2" => h2[a]}}
h3
# => {"a"=>{"h1"=>100, "h2"=>nil}, "b"=>{"h1"=>200, "h2"=>254}, "c"=>{"h1"=>nil, "h2"=>300}}
Here is another way that is somewhat general in terms of the numbers of hashes and hash labels:
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
h3 = { "c" => 111, "b" => 222 }
a = [h1,h2,h3]
labels = {h1=>"h1", h2=>"h2", h3=>"h3"}
h.reduce([]) { |keys,h| keys | h.keys }
.each_with_object({}) { |k,h|
h[k] = a.each_with_object({}) { |f,g| g[labels[f]] = f[k] } }
#=> {"a"=>{"h1"=>100, "h2"=>nil, "h3"=>nil},
# "b"=>{"h1"=>200, "h2"=>254, "h3"=>222},
# "c"=>{"h1"=>nil, "h2"=>300, "h3"=>111}}
The steps:
keys = h.reduce([]) { |keys,h| keys | h.keys }
#=> ["a", "b", "c"]
enum = keys.each_with_object({})
#=> #<Enumerator: ["a", "b", "c"]:each_with_object({})>
k,h = enum.next
#=> ["a", {}]
h[k] = a.each_with_object({}) { |f,g| g[labels[f]] = f[k] }
#=> {"h1"=>100, "h2"=>nil, "h3"=>nil}
k,h = enum.next
#=> ["b", {"a"=>{"h1"=>100, "h2"=>nil, "h3"=>nil}}]
h[k] = a.each_with_object({}) { |f,g| g[labels[f]] = f[k] }
#=> {"h1"=>200, "h2"=>254, "h3"=>222}
k,h = enum.next
#=> ["c", {"a"=>{"h1"=>100, "h2"=>nil, "h3"=>nil},
# "b"=>{"h1"=>200, "h2"=>254, "h3"=>222}}]
h[k] = a.each_with_object({}) { |f,g| g[labels[f]] = f[k] }
#=> {"h1"=>nil, "h2"=>300, "h3"=>111}
h
#=> {"a"=>{"h1"=>100, "h2"=>nil, "h3"=>nil},
# "b"=>{"h1"=>200, "h2"=>254, "h3"=>222},
# "c"=>{"h1"=>nil, "h2"=>300, "h3"=>111}}

Resources