Merge same named arrays in ruby hashes

624 views Asked by At

I'd like to take two hashes of a form like these:

hash_1 = {:a=>{:b=>3, :c=>{:stuff1=>[{:d=>1, :e=>2}, {:d=>4, :e=>2}], :stuff2=>[{:f=>33, :g=>44}, {:f=>55, :g=>66}], :h=>4}}}

hash_2 = {:a=>{:b=>3, :c=>{:stuff1=>[{:d=>8, :e=>5}, {:d=>7, :e=>5}], :stuff2=>[{:f=>45, :g=>89}, {:f=>78, :g=>67}], :h=>4}}}

And get this back (note :stuff1 and :stuff2 are added together):

result = {:a=>{:b=>3, :c=>{:stuff1=>[{:d=>1, :e=>2}, {:d=>4, :e=>2}, {:d=>8, :e=>5}, {:d=>7, :e=>5}], :stuff2=>[{:f=>33, :g=>44}, {:f=>55, :g=>66}, {:f=>45, :g=>89}, {:f=>78, :g=>67}], :h=>4}}}

I've found this post, but my case is with nested hashes, so any help from some good ruby hands would be appreciated.

Basically, I want to "merge" the array values of same named keys when the values corresponding to those keys are arrays. Of course the following will replace hash_1's :stuff1 array with hash_2's :stuff1 array (and similarly for :stuff2), but I want an array '+' type of merge, not an update/replace, or merge! ...

hash_1.merge(hash_2)  # NOT what I want => {:a=>{:b=>3, :c=>{:stuff1=>[{:d=>8, :e=>5}, {:d=>7, :e=>5}], :stuff2=>[{:f=>45, :g=>89}, {:f=>78, :g=>67}], :h=>4}}}

I'm using ruby 1.9.2, btw. I know hashes have been updated a bit lately, though I don't think that'll affect the answer.

Thanks!

3

There are 3 answers

0
Marek Příhoda On BEST ANSWER
# adapted from http://snippets.dzone.com/posts/show/4706
class Hash
  def deep_merge_with_array_values_concatenated(hash)
    target = dup

    hash.keys.each do |key|
      if hash[key].is_a? Hash and self[key].is_a? Hash
        target[key] = target[key].deep_merge_with_array_values_concatenated(hash[key])
        next
      end

      if hash[key].is_a?(Array) && target[key].is_a?(Array)
        target[key] = target[key] + hash[key]
      else
        target[key] = hash[key]
      end
    end

    target
  end
end

p hash_1.deep_merge_with_array_values_concatenated(hash_2)
0
tokland On

I think the specifications are not complete. Anyway, a functional recursive approach (second hash only used to concat values on array values):

class Hash
  def concat_on_common_array_values(hash)
    Hash[map do |key, value|
      if value.is_a?(Hash) && hash[key].is_a?(Hash)
        [key, value.concat_on_common_array_values(hash[key])] 
      elsif value.is_a?(Array) && hash[key].is_a?(Array)
        [key, value + hash[key]]
      else
        [key, value]
      end      
    end]
  end
end

p hash_1.concat_on_common_array_values(hash_2)
# {:a=>{:b=>3, :c=>{:stuff1=>[{:d=>1, :e=>2}, {:d=>4, :e=>2}, {:d=>8, :e=>5}, {:d=>7, :e=>5}], :stuff2=>[{:f=>33, :g=>44}, {:f=>55, :g=>66}, {:f=>45, :g=>89}, {:f=>78, :g=>67}], :h=>4}}}
0
megas On

You can define block for merge method, this block will be called for each duplication key.

hash_1.merge(hash_2) do |key, old_value, new_value|
  old_value + new_value
end