开发者

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)
      开发者_Python百科  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?


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"}}}


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)


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.


Try this monkey-patching solution:

class Hash
  def recursive_merge(hash = nil)
    return self unless hash.is_a?(Hash)
    base = self
    hash.each do |key, v|
      if base[key].is_a?(Hash) && hash[key].is_a?(Hash)
        base[key].recursive_merge(hash[key])
      else
        base[key]= hash[key]
      end
    end
    base
  end
end


In order to merge one into the other as the ticket suggested, you could modify @Phrogz function

def recurse_merge( merge_from, merge_to )
  merge_from.merge(merge_to) do |_,x,y|
    (x.is_a?(Hash) && y.is_a?(Hash)) ? recurse_merge(x,y) : x
  end
end

In case there is duplicate key, it will only use the content of merge_from hash


Here is even better solution for recursive merging that uses refinements and has bang method alongside with block support. This code does work on pure Ruby.

module HashRecursive
    refine Hash do
        def merge(other_hash, recursive=false, &block)
            if recursive
                block_actual = Proc.new {|key, oldval, newval|
                    newval = block.call(key, oldval, newval) if block_given?
                    [oldval, newval].all? {|v| v.is_a?(Hash)} ? oldval.merge(newval, &block_actual) : newval
                }   
                self.merge(other_hash, &block_actual)
            else
                super(other_hash, &block)
            end
        end
        def merge!(other_hash, recursive=false, &block)
            if recursive
                self.replace(self.merge(other_hash, recursive, &block))
            else
                super(other_hash, &block)
            end
        end
    end
end

using HashRecursive

After using HashRecursive was executed you can use default Hash::merge and Hash::merge! as if they haven't been modified. You can use blocks with these methods as before.

The new thing is that you can pass boolean recursive (second argument) to these modified methods and they will merge hashes recursively.


Example usage for answering the question. It's extremely easy:

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

puts hash_a.merge(hash_b)                                   # Won't override hash_a
# output:   { :a => { :b => { :x => "y" } } }

puts hash_a                                                 # hash_a is unchanged
# output:   { :a => { :b => { :c => "d" } } }

hash_a.merge!(hash_b, recursive=true)                       # Will override hash_a

puts hash_a                                                 # hash_a was changed
# output:   { :a => { :b => { :c => "d", :x => "y" } } }

For advanced example take a look at this answer.

Also take a look at my recursive version of Hash::each(Hash::each_pair) here.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜