Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merging multi-dimensional hashes in Ruby

I have two hashes which have a structure something similar to this:

hash_a = { :a => { :b => { :c => "d" } } }
hash_b = { :a => { :b => { :x => "y" } } }

I want to merge these together to produce the following hash:

{ :a => { :b => { :c => "d", :x => "y" } } }

The merge function will replace the value of :a in the first hash with the value of :a in the second hash. So, I wrote my own recursive merge function, which looks like this:

def recursive_merge( merge_from, merge_to )
    merged_hash = merge_to
    first_key = merge_from.keys[0]
    if merge_to.has_key?(first_key)
        merged_hash[first_key] = recursive_merge( merge_from[first_key], merge_to[first_key] )
    else
        merged_hash[first_key] = merge_from[first_key]
    end
    merged_hash
end

But I get a runtime error: can't add a new key into hash during iteration. What's the best way of going about merging these hashes in Ruby?

like image 246
White Elephant Avatar asked Apr 07 '11 12:04

White Elephant


3 Answers

Ruby's existing Hash#merge allows a block form for resolving duplicates, making this rather simple. I've added functionality for merging multiple conflicting values at the 'leaves' of your tree into an array; you could choose to pick one or the other instead.

hash_a = { :a => { :b => { :c => "d", :z => 'foo' } } }
hash_b = { :a => { :b => { :x => "y", :z => 'bar' } } }

def recurse_merge(a,b)
  a.merge(b) do |_,x,y|
    (x.is_a?(Hash) && y.is_a?(Hash)) ? recurse_merge(x,y) : [*x,*y]
  end
end

p recurse_merge( hash_a, hash_b )
#=> {:a=>{:b=>{:c=>"d", :z=>["foo", "bar"], :x=>"y"}}}

Or, as a clean monkey-patch:

class Hash
  def merge_recursive(o)
    merge(o) do |_,x,y|
      if x.respond_to?(:merge_recursive) && y.is_a?(Hash)
        x.merge_recursive(y)
      else
        [*x,*y]
      end
    end
  end
end

p hash_a.merge_recursive hash_b
#=> {:a=>{:b=>{:c=>"d", :z=>["foo", "bar"], :x=>"y"}}}
like image 189
Phrogz Avatar answered Oct 26 '22 17:10

Phrogz


You can do it in one line :

merged_hash = hash_a.merge(hash_b){|k,hha,hhb| hha.merge(hhb){|l,hhha,hhhb| hhha.merge(hhhb)}}

If you want to imediatly merge the result into hash_a, just replace the method merge by the method merge!

If you are using rails 3 or rails 4 framework, it is even easier :

merged_hash = hash_a.deep_merge(hash_b)

or

hash_a.deep_merge!(hash_b)
like image 40
Douglas Avatar answered Oct 26 '22 19:10

Douglas


If you change the first line of recursive_merge to

merged_hash = merge_to.clone

it works as expected:

recursive_merge(hash_a, hash_b)    
->    {:a=>{:b=>{:c=>"d", :x=>"y"}}}

Changing the hash as you move through it is troublesome, you need a "work area" to accumulate your results.

like image 39
Paul Rubel Avatar answered Oct 26 '22 19:10

Paul Rubel