merge some complex hashes in ruby - ruby

I'd like to merge the following hashes together.
h1 = {"201201" => {:received => 2}, "201202" => {:received => 4 }}
h2 = {"201201" => {:closed => 1}, "201202" => {:closed => 1 }}
particularly, my expected result is:
h1 = {"201201" => {:received => 2, :closed => 1}, "201202" => {:received => 4, :closed => 1 }}
I have tried every way as:
h = h1.merge(h2){|key, first, second| {first , second} }
unfortunately, neither seemed to work out fine for me.
any advice would be really appreciated.

This should work for you:
h = h1.merge(h2){|key, first, second| first.merge(second)}

Related

Most performant way to group/summarise two hashes?

I have two hashes with some data that I need to aggregate. The first one is a mapping of which ids (id_1, id_2, id_3, id_4) belong under what category (a, b, c):
hash_1 = {'a' => ['id_1','id_2'], 'b' => ['id_3'], 'c' => ['id_4']}
The second hash holds values of how many events happened per id for a given date (date_1, date_2, date_3):
hash_2 = {
'id_1' => {'date_1' => 5, 'date_2' => 6, 'date_3' => 8},
'id_2' => {'date_1' => 0, 'date_3' => 6},
'id_3' => {'date_1' => 0, 'date_2' => nil, 'date_3' => 1},
'id_4' => {'date_1' => 10, 'date_2' => 1}
}
What I want is to get the total event per category (a,b,c). For the above example, the result would look something like:
hash_3 = {'a' => (5+6+8+0+6), 'b' => (0+0+1), 'c' => (10+1)}
My problem is, that there are about 5000 categories, each pointing to typically 1 to 3 ids, and each ID having event counts for 30 dates or more. So this takes quite a bit of computation. What will be the most performant (time effective) way to do this grouping in Ruby?
update
This is what I tried so far (took like 6-8 seconds!, horribly slow):
def total_clicks_per_category
{}.tap do |res|
hash_1.each do |cat, ids|
res[cat] = total_event_per_ids(ids)
end
end
end
def total_event_per_ids(ids)
ids.reduce(0) do |memo, id|
events = hash_2.fetch(id, {})
memo + (events.values.reduce(:+) || 0)
end
end
P.S. I’m using Ruby 2.3.
I'm writing this on a phone so I cannot test right now, but it looks OK.
g = hash_2.each_with_object({}) { |(k,v),g| g[k] = v.values.compact.sum }
hash_3 = hash_1.each_with_object({}) { |(k,v),h| h[k] = g.values_at(*v).sum }
First, create an intermediate hash that holds the sum of hash_2:
hash_4 = hash_2.map{|k, v| [k, v.values.inject(:+)]}.to_h
# => {"id_1"=>19, "id_2"=>6, "id_3"=>1, "id_4"=>11}
Then do the final summation:
hash_3 = hash_1.map{|k, v| [k, v.map{|k| hash_4[k]}.inject(:+)]}.to_h
# => {"a"=>25, "b"=>1, "c"=>11}
Theory
5000*3*30 isn't that many. Ruby probably will need a second at most for this kind of job.
Hash lookup is fast by default, you won't be able to optimize much.
You could pre-calculate hash_2_sum, though :
hash_2_sum = {
'id_1' => 5+6+8,
'id_2' => 0+6,
'id_3' => 0+0+1,
'id_4' => 10+1
}
A loop on hash1 with hash_2_sum lookup, and you're done.
Code
Your example has been updated with some nil values. You need to remove them with compact, and make sure the sum is 0 when no element is found with inject(0, :+):
hash_1 = {'a' => ['id_1','id_2'], 'b' => ['id_3'], 'c' => ['id_4']}
hash_2 = {
'id_1' => { 'date_1' => 5, 'date_2' => 6, 'date_3' => 8 },
'id_2' => { 'date_1' => 0, 'date_3' => 6 },
'id_3' => { 'date_1' => 0, 'date_2' => nil, 'date_3' => 1 },
'id_4' => { 'date_1' => 10, 'date_2' => 1 }
}
hash_2_sum = hash_2.each_with_object({}) do |(key, dates), sum|
sum[key] = dates.values.compact.inject(0, :+)
end
hash_3 = hash_1.each_with_object({}) do |(key, ids), sum|
sum[key] = hash_2_sum.values_at(*ids).inject(0, :+)
end
# {"a"=>25, "b"=>1, "c"=>11}
Note
{}.tap do |res|
hash_1.each do |cat, ids|
res[cat] = total_event_per_ids(ids)
end
end
isn't very readable IMHO.
You can either use each_with_object or Array#to_h :
result = [1, 2, 3].each_with_object({}) do |i, hash|
hash[i] = i * i
end
#=> {1=>1, 2=>4, 3=>9}
result = [1, 2, 3].map { |i| [i, i * i] }.to_h
#=> {1=>1, 2=>4, 3=>9}

Ruby, turn array of hashes into single hash

I have the following Array of Hashes:
a = [{:a => 1, :b => "x"}, {:a => 2, :b => "y"}]
I need to turn it into:
z={"x" => 1, "y" => 2}
or:
z={1 => "x", 2 => "y"}
Can I do this in a clean and functional way?
Something like this:
Hash[a.map(&:values)] # => {1=>"x", 2=>"y"}
if you want the other way:
Hash[a.map(&:values).map(&:reverse)] # => {"x"=>1, "y"=>2}
incorporating the suggestion from #squiguy:
Hash[a.map(&:values)].invert

Ruby loops. Hash into string

I am working with Ruby. I need to grab each key/value and put it into a string.
So far I have:
values = ['first' => '1', 'second' => '2']
#thelink = values.collect do | key, value |
"#{key}=#{value}&"
end
When I print #thelink I see:
first1second2=&
But Really what I want is
first=1&second=2
Could anybody help/explain please?
There is something subtle you are missing here {} vs [].
See the below taken from IRB tests:
irb(main):002:0> {'first' => 1, 'second' => 2}
=> {"second"=>2, "first"=>1}
irb(main):003:0> ['first' => 1, 'second' => 2]
=> [{"second"=>2, "first"=>1}]
irb(main):004:0> {'first' => 1, 'second' => 2}.class
=> Hash
irb(main):005:0> ['first' => 1, 'second' => 2].class
=> Array
Similar to this:
irb(main):006:0> {'first' => 1, 'second' => 2}.collect { |key,value| puts "#{key}:#{value}" }
second:2
first:1
=> [nil, nil]
irb(main):007:0> ['first' => 1, 'second' => 2].collect { |key,value| puts "#{key}:#{value}" }
second2first1:
=> [nil]
The array has a single element (a hash) that, as a string, is everything concatenated. This is the important thing to note here.
On the other hand, the hash iterates by handing you the key/value pairs that you are expecting.
Hope that helps.
I think your code has a typo (a hash is delimited by {} not by []). Try this
values = {'first' => '1', 'second' => '2'}
r = values.map{|k,v| "#{k}=#{v}"}.join('&')
puts r
#shows: first=1&second=2

How to merge Ruby hashes

How can I merge these two hashes:
{:car => {:color => "red"}}
{:car => {:speed => "100mph"}}
To get:
{:car => {:color => "red", :speed => "100mph"}}
There is a Hash#merge method:
ruby-1.9.2 > a = {:car => {:color => "red"}}
=> {:car=>{:color=>"red"}}
ruby-1.9.2 > b = {:car => {:speed => "100mph"}}
=> {:car=>{:speed=>"100mph"}}
ruby-1.9.2 > a.merge(b) {|key, a_val, b_val| a_val.merge b_val }
=> {:car=>{:color=>"red", :speed=>"100mph"}}
You can create a recursive method if you need to merge nested hashes:
def merge_recursively(a, b)
a.merge(b) {|key, a_item, b_item| merge_recursively(a_item, b_item) }
end
ruby-1.9.2 > merge_recursively(a,b)
=> {:car=>{:color=>"red", :speed=>"100mph"}}
Hash#deep_merge
Rails 3.0+
a = {:car => {:color => "red"}}
b = {:car => {:speed => "100mph"}}
a.deep_merge(b)
=> {:car=>{:color=>"red", :speed=>"100mph"}}
Source: https://speakerdeck.com/u/jeg2/p/10-things-you-didnt-know-rails-could-do
Slide 24
Also,
http://apidock.com/rails/v3.2.13/Hash/deep_merge
You can use the merge method defined in the ruby library. https://ruby-doc.org/core-2.2.0/Hash.html#method-i-merge
Example
h1={"a"=>1,"b"=>2}
h2={"b"=>3,"c"=>3}
h1.merge!(h2)
It will give you output like this {"a"=>1,"b"=>3,"c"=>3}
Merge method does not allow duplicate key, so key b will be overwritten from 2 to 3.
To overcome the above problem, you can hack merge method like this.
h1.merge(h2){|k,v1,v2|[v1,v2]}
The above code snippet will be give you output
{"a"=>1,"b"=>[2,3],"c"=>3}
h1 = {:car => {:color => "red"}}
h2 = {:car => {:speed => "100mph"}}
h3 = h1[:car].merge(h2[:car])
h4 = {:car => h3}

Changing the value of one attribute in a Ruby collection

Say for example I've got a collection like this:
[{"name" => "Ganesh", "magic_number" => 7}, {"name" => "Comrade", "magic_number" => 2}...]
How can I change the value of ALL the magic_numbers in the collection to be the same value (e.g. 8)?
I'm sure it's using something like map or collect but I can't seem to do it at the moment and return me the whole collection with the changes, just one or the other...
Just use .each:
a = [{"name" => "Ganesh", "magic_number" => 7}, {"name" => "Comrade", "magic_number" => 2} ]
a.each { |x| x['magic_number'] = 8 }
# a is now [{"magic_number"=>8, "name"=>"Ganesh"}, {"magic_number"=>8, "name"=>"Comrade"}]
The argument to the block is a reference to the original elements so you can change them as desired. Note that this changes a in-place which I think is what you're after.
This works:
x = [{"name" => "Ganesh", "magic_number" => 7}, {"name" => "Comrade", "magic_number" => 2}]
x.map{|i| i["magic_number"] = 0; i }
=> [{"magic_number"=>0, "name"=>"Ganesh"}, {"magic_number"=>0, "name"=>"Comrade"}]

Resources