Ruby: merge nested hash

2019-01-17 14:07发布

I would like to merge a nested hash.

a = {:book=>
    [{:title=>"Hamlet",
      :author=>"William Shakespeare"
      }]}

b = {:book=>
    [{:title=>"Pride and Prejudice",
      :author=>"Jane Austen"
      }]}

I would like the merge to be:

{:book=>
   [{:title=>"Hamlet",
      :author=>"William Shakespeare"},
    {:title=>"Pride and Prejudice",
      :author=>"Jane Austen"}]}

What is the nest way to accomplish this?

9条回答
我命由我不由天
2楼-- · 2019-01-17 14:25

For rails 3.0.0+ or higher version there is the deep_merge function for ActiveSupport that does exactly what you ask for.

查看更多
贼婆χ
3楼-- · 2019-01-17 14:28

To add on to Jon M and koendc's answers, the below code will handle merges of hashes, and :nil as above, but it will also union any arrays that are present in both hashes (with the same key):

class ::Hash
    def deep_merge(second)
        merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
        self.merge(second.to_h, &merger)
    end
end

a.deep_merge(b)
查看更多
淡お忘
4楼-- · 2019-01-17 14:32

For variety's sake - and this will only work if you want to merge all the keys in your hash in the same way - you could do this:

a.merge(b) { |k, x, y| x + y }

When you pass a block to Hash#merge, k is the key being merged, where the key exists in both a and b, x is the value of a[k] and y is the value of b[k]. The result of the block becomes the value in the merged hash for key k.

I think in your specific case though, nkm's answer is better.

查看更多
走好不送
5楼-- · 2019-01-17 14:35

A little late to answer your question, but I wrote a fairly rich deep merge utility awhile back that is now maintained by Daniel Deleo on Github: https://github.com/danielsdeleo/deep_merge

It will merge your arrays exactly as you want. From the first example in the docs:

So if you have two hashes like this:

   source = {:x => [1,2,3], :y => 2}
   dest =   {:x => [4,5,'6'], :y => [7,8,9]}
   dest.deep_merge!(source)
   Results: {:x => [1,2,3,4,5,'6'], :y => 2}

It won't merge :y (because int and array aren't considered mergeable) - using the bang (!) syntax causes the source to overwrite.. Using the non-bang method will leave dest's internal values alone when an unmergeable entity is found. It will add the arrays contained in :x together because it knows how to merge arrays. It handles arbitrarily deep merging of hashes containing whatever data structures.

Lots more docs on Daniel's github repo now..

查看更多
6楼-- · 2019-01-17 14:35
a[:book] = a[:book] + b[:book]

Or

a[:book] <<  b[:book].first
查看更多
戒情不戒烟
7楼-- · 2019-01-17 14:37

All answers look to me overcomplicated. Here's what I came up with eventually:

# @param tgt [Hash] target hash that we will be **altering**
# @param src [Hash] read from this source hash
# @return the modified target hash
# @note this one does not merge Arrays
def self.deep_merge!(tgt_hash, src_hash)
  tgt_hash.merge!(src_hash) { |key, oldval, newval|
    if oldval.kind_of?(Hash) && newval.kind_of?(Hash)
      deep_merge!(oldval, newval)
    else
      newval
    end
  }
end

P.S. use as public, WTFPL or whatever license

查看更多
登录 后发表回答