Any instance of key anywhere in a nested hash - ruby

Given a hash like so:
h = {
"actual_amount" => 20,
"otherkey" => "value",
"otherkey2" => [{"actual_amount" => 30, "random_amount" => 45}]
}
where there are any number of layers of nesting, is there a simple way to pluck all the key-value pairs (or just the values) of the keys that are actual_amount?

I've assumed the values of keys are literals or arrays of hashes.
This question clearly calls for a recursive solution.
def amounts(h)
h.each_with_object([]) do |(k,v),a|
case v
when Array
v.each { |g| a.concat amounts(g) }
else
a << v if k == "actual_amount"
end
end
end
Suppose
h = {
"actual_amount"=>20,
1=>2,
2=>[
{ "actual_amount"=>30,
3=>[
{ "actual_amount" => 40 },
{ 4=>5 }
]
},
{ 5=>6 }
]
}
then
amounts(h)
#=> [20, 30, 40]

Using the hash, provided by Cary, as an input:
▶ flatten = ->(inp) do
▷ [*(inp.respond_to?(:map) ? inp.map(&:flatten) : inp)]
▷ end
▶ res = flatten(h).first
▶ res.select.with_index do |_, i|
▷ i > 0 && res[i - 1] == 'actual_amount'
▷ end
#⇒ [20, 30, 40]

Related

Ruby - sorting hashes into descending order by value

I'm trying to solve the following:
"You are given a dictionary/hash/object containing some languages and your test results in the given languages. Return the list of languages where your test score is at least 60, in descending order of the results.
Examples:
{"Java" => 10, "Ruby" => 80, "Python" => 65} --> ["Ruby", "Python"]
{"Hindi" => 60, "Dutch" => 93, "Greek" => 71} --> ["Dutch", "Greek", "Hindi"]
{"C++" => 50, "ASM" => 10, "Haskell" => 20} --> []
I am having trouble sorting into descending order by results. Here is what I have so far
def my_languages(results)
array = []
results.each { |a,b|
results.values.sort.reverse
if b.to_i >= 60
array << a
end
}
return array
end
Not the most elegant solution but I am a complete Ruby newbie (and Stack Overflow newbie too - sorry!) Any advice would be much appreciated!
You are kinda mixing the sort and filtering out phases. My solution
Filter results with value >= 60
Sort for values (descending, -v)
Extract the first element for every array (the language name)
def my_languages(results)
results.select { |k, v| v >= 60 }.sort_by { |(k,v)| -v }.map(&:first)
end
h = { "Java" => 10, "Ruby" => 80, "Python" => 65 }
h.select { |_,v| v >= 60 }.
keys.
sort_by { |k| -h[k] }
#=> ["Ruby", "Python"]
The steps are as follows.
g = h.select { |_,v| v >= 60 }
#=> {"Ruby"=>80, "Python"=>65}
a = g.keys
#=> ["Ruby", "Python"]
a.sort_by { |k| -h[k] }
#=> ["Ruby", "Python"]
If you don't care for -h[k] two alternative follow.
h.select { |_,v| v >= 60 }.
keys.
sort_by { |k| h[k] }.
reverse
and
a = h.select { |_,v| v >= 60 }.
keys
a.max_by(a.size) { |k| h[k] }
I doubt that one would notice any significant difference in performance among the three.
Enumerable#max_by, min_by, max and min have been permitted to have an optional argument since Ruby v2.2.
To make it faster I would check minimum value when mapping:
hash = {"Hindi" => 60, "Dutch" => 93, "Greek" => 71}
hash.sort.map { |arr| arr[0] if arr[1] >= 60 }.compact
# or imo cleaner
hash.sort.select { |a| a[1] >= 60 }.map(&:first)

Ruby range.reduce with hash accumulator

I have this method
def heights
(60..68).reduce({}) { |h, i| h.merge!( { %(#{i/12}'#{i%12}") => i } ) }
end
it returns a hash of heights
{
"5'0\"" => 60, "5'1\"" => 61, "5'2\"" => 62,
"5'3\"" => 63, "5'4\"" => 64, "5'5\"" => 65,
"5'6\"" => 66, "5'7\"" => 67, "5'8\"" => 68
}
That's what I want. However, I don't like using the merge! method. I'd much rather use the hash[key] = value syntax for assignment:
def heights
(60..68).reduce({}) { |h, i| h[%(#{i/12}'#{i%12}")] = i }
end
But this code throws errors. I know that with reduce, in your pipes you can name your accumulator and element.
I also understand that
sum = 0
(1..5).each { |i| sum += i }
is equivalent to
(1..5).reduce(0) { |sum, i| sum + i }
So why doesn't this
hash = {}
(1..5).each { |i| hash[i.to_s] = i }
work the same as
(1..5).reduce({}) { |hash, i| hash["#{i}"] = i }
You could use each_with_object instead of reduce:
(60..68).each_with_object({}) { |i, h| h[%(#{i/12}'#{i%12}")] = i }
enumerable.each_with_object(obj) { ... } returns obj so you don't need the artificial-feeling ; h in the block that you'd need with reduce.
Note that the order of the arguments to the block is different than with reduce.
Block in reduce should return new accumulator. In your case
(1..5).reduce({}) { |hash, i| hash["#{i}"] = i }
block returns i, which is an integer, so on the second iteration you will try to call [] on an integer. What you need it this:
(1..5).reduce({}) { |hash, i| hash["#{i}"] = i; hash }

Combine hash keys and values if keys are identical except for case

Let's say I have a Ruby Hash where at least two keys are identical, except for their case, for instance:
{ 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
Is there a way I can combine like keys (except for their case) such that the resulting Hash might look like this?
{ 'foo' => 3, 'bar' => 3 }
Thank you!
You can build a new hash:
new_hash = Hash.new(0)
old_hash.each_pair { |k, v| new_hash[k.downcase] += v }
You can use inject to loop all the hash items and build a new hash.
hash = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
hash.inject({}) do |result, (key, value)|
key = key.downcase
result[key] = result[key] ? result[key] + value : value
result
end
Here is one more way of doing this
h = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
p h.collect{|k, v| {k.downcase => v}}.reduce { |a, v| a.update(v) {|k, o, n| o + n } }
#=> {"foo"=>3, "bar"=>3}
h = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
h.each_with_object({}) { |(k,v),g| g[k.downcase] = g[k.downcase].to_i + v }
#=> {"foo"=>3, "bar"=>3}
This makes use of the fact that if g does not have a key e, g[e] on the right side will equal nil and nil.to_i #=> 0. On the other hand, if g has a key e, h[e].to_i will equal h[e].
Another way:
h.each_with_object({}) { |(k,v),g| g.update(k.downcase=>v) { |_,o,v| o+v } }
#=> {"foo"=>3, "bar"=>3}

How to merge array of hash based on the same keys in ruby?

How to merge array of hash based on the same keys in ruby?
example :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
How to get result like this?
a = [{:a=>[1, 10]},{:b=>8},{:c=>[7, 2]}]
Try
a.flat_map(&:entries)
.group_by(&:first)
.map{|k,v| Hash[k, v.map(&:last)]}
Another alternative:
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
p a.each_with_object({}) { |h, o| h.each { |k,v| (o[k] ||= []) << v } }
# => {:a=>[1, 10], :b=>[8], :c=>[7, 2]}
It also works when the Hashes have multiple key/value combinations, e.g:
b = [{:a=>1, :b=>5, :x=>10},{:a=>10, :y=>2},{:b=>8},{:c=>7},{:c=>2}]
p b.each_with_object({}) { |h, o| h.each { |k,v| (o[k] ||= []) << v } }
# => {:a=>[1, 10], :b=>[5, 8], :x=>[10], :y=>[2], :c=>[7, 2]}
Minor addition to answer by Arie Shaw to match required answer:
a.flat_map(&:entries)
.group_by(&:first)
.map{|k,v| Hash[k, v.size.eql?(1) ? v.last.last : v.map(&:last) ]}
#=> [{:a=>[1, 10]}, {:b=>8}, {:c=>[7, 2]}]
I'd do :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
merged_hash = a.each_with_object({}) do |item,hsh|
k,v = item.shift
hsh[k] = hsh.has_key?(k) ? [ *Array( v ), hsh[k] ] : v
end
merged_hash.map { |k,v| { k => v } }
# => [{:a=>[10, 1]}, {:b=>8}, {:c=>[2, 7]}]
update
A better taste :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
merged_hash = a.each_with_object({}) do |item,hsh|
k,v = item.shift
(hsh[k] ||= []) << v
end
merged_hash.map { |k,v| { k => v } }
# => [{:a=>[10, 1]}, {:b=>8}, {:c=>[2, 7]}]

How to replace all hash keys having a '.'?

I am using Ruby on Rails 4 and I would like to replace all hash keys so to change the hash from
h_before = {:"aaa.bbb" => 1, :c => 2, ...}
to
h_after = {:bbb => 1, :c => 2, ...}
That is, I would like to someway "demodulize" all hash keys having the .. How can I make that?
each_with_object is a cleaner and shorter approach than inject from the answer:
h_before.each_with_object({}){|(k, v),h| h[k.to_s.split(".").last.to_sym] = v}
=> {:bbb=>1, :c=>2}
h_before = {:"aaa.bbb" => 1, :c => 2}
h_after =
h_before.inject({}){|h, (k, v)| h[k.to_s.split(".").last.to_sym] = v; h}
# => {:bbb = > 1, :c => 2}
Since there are a bunch of answers claiming to do the same thing, I thought it was time to post some benchmarks:
require 'fruity'
h_before = {:"aaa.bbb" => 1, :c => 2}
def cdub_test(hash)
Hash[hash.map {|k, v| [k.to_s.gsub(/^.*\./,"").to_sym, v]}]
end
def matt_test(old_hash)
Hash[old_hash.map { |k,v| [ k.to_s.sub(/.*\./,'').to_sym, v ] }]
end
class Hash
require 'active_support/core_ext/hash/indifferent_access'
def grep_keys(pattern)
return inject(HashWithIndifferentAccess.new){|h, (k, v)|
h[$1 || k] = v if pattern =~ k.to_s ; h }
end
end
def phlip_test(hash)
hash.grep_keys(/\.(\w+)$/)
end
def bjhaid_test(hash)
hash.each_with_object({}){|(k, v),h| h[k.to_s.split(".").last.to_sym] = v}
end
def sawa_test(hash)
hash.inject({}){|h, (k, v)| h[k.to_s.split(".").last.to_sym] = v; h}
end
compare do
cdub { cdub_test(h_before) }
matt { matt_test(h_before) }
phlip { phlip_test(h_before) }
bjhaid { bjhaid_test(h_before) }
sawa { sawa_test(h_before) }
end
Which outputs:
Running each test 1024 times. Test will take about 1 second.
bjhaid is similar to sawa
sawa is faster than matt by 60.00000000000001% ± 10.0%
matt is faster than phlip by 30.000000000000004% ± 10.0% (results differ: {:bbb=>1, :c=>2} vs {"bbb"=>1})
phlip is similar to cdub (results differ: {"bbb"=>1} vs {:bbb=>1, :c=>2})
Notice that phlip's code doesn't return the desired results.
old_hash = {:"aaa.bbb" => 1, :c => 2 }
new_hash = Hash[old_hash.map { |k,v| [ k.to_s.sub(/.*\./,'').to_sym, v ] }]
1.9.3p448 :001 > hash = {:"aaa.bbb" => 1, :c => 2 }
=> {:"aaa.bbb"=>1, :c=>2}
1.9.3p448 :002 > Hash[hash.map {|k, v| [k.to_s.gsub(/^.*\./,"").to_sym, v]}]
=> {:bbb=>1, :c=>2}
My grep_keys has never failed me here:
class Hash
def grep_keys(pattern)
return inject(HashWithIndifferentAccess.new){|h, (k, v)|
h[$1 || k] = v if pattern =~ k.to_s ; h }
end
end
It returns a shallow-copy of the Hash, but only with the matched keys. If the input regular expression contains a () match, the method replaces the key with the matched value. (Note this might merge two or more keys, and discard all but a random value for them!) I use it all the time to cut up a Rails param into sub-params containing only the keys that some module needs.
{:"aaa.bbb" => 1, :c => 2 }.grep_keys(/\.(\w+)$/) returns {"bbb"=>1}.
This method upgrades your actual problem to "how to define a regexp that matches what you mean by 'having a ".".'"

Resources