67

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?

user1223862
  • 1,193
  • 2
  • 9
  • 10
  • 5
    I suggest marking [Jon M's answer](http://stackoverflow.com/questions/9381553/ruby-merge-nested-hash#9381776) or [Dan's answer](http://stackoverflow.com/questions/9381553/ruby-merge-nested-hash#30225093) as accepted. – Neonit Apr 11 '17 at 08:45

9 Answers9

63

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

Carl Witthoft
  • 20,573
  • 9
  • 43
  • 73
xlembouras
  • 8,215
  • 4
  • 33
  • 42
  • 11
    this does not work. it will replace the existing array with the new array. http://apidock.com/rails/v3.2.13/Hash/deep_merge It seems to work for Rails 4: http://apidock.com/rails/v4.0.2/Hash/deep_merge – Sascha Kaestle Jan 22 '14 at 10:32
  • No. Although Rails 3.2.18 does have `deep_merge` method, but it accept a block only from version 4.0.2 – mirelon Aug 20 '14 at 13:18
  • @mirelon The OP didn't ask for the block, so the answer is correct. – Janko Sep 26 '14 at 19:30
  • 13
    However, the question isn't about Rails, is it? So it's a bit irritating that this answer is the one with the most upvotes. – Neonit Apr 11 '17 at 08:39
58

I found a more generic deep-merge algorithm here, and used it like so:

class ::Hash
    def deep_merge(second)
        merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
        self.merge(second, &merger)
    end
end

a.deep_merge(b)
Jon M
  • 11,669
  • 3
  • 41
  • 47
53

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 { |_, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
    merge(second.to_h, &merger)
  end
end


a.deep_merge(b)
Varun Garg
  • 2,464
  • 23
  • 37
Dan
  • 531
  • 4
  • 3
11

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.

Russell
  • 12,261
  • 4
  • 52
  • 75
  • 1
    NoMethodError: undefined method `+' for {:color=>"red"}:Hash – user1223862 Feb 22 '12 at 14:20
  • It looks like you are trying to use this answer with a hash containing other keys - `{:color=>"red"}` is not in your example. As I said in my answer, this will only work if you want to merge all the keys in your hash in the same way. – Russell Feb 22 '12 at 14:34
  • Perhaps you could add the hash you're working with in full to the question? – Russell Feb 22 '12 at 14:36
  • 1
    This is actually a pretty handy trick! Had no idea `Hash#merge` took an optional block. – Damien Wilson Jun 01 '12 at 23:14
  • If merging a hash of keys to arrays, you could do something like this to also merge empty lists: `a.merge(b) { |k, x, y| x + (y ? y : []) }` – Dave Sep 30 '15 at 11:44
8

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..

Steve Midgley
  • 2,226
  • 2
  • 18
  • 20
4

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

akostadinov
  • 17,364
  • 6
  • 77
  • 85
2

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 for simple usage is written at this answer. Here is an advanced example.

The example in this question is bad because it got nothing to do with recursive merging. Following line would meet question's example:

a.merge!(b) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}

Let me give you a better example to show the power of the code above. Imagine two rooms, each have one bookshelf in it. There are 3 rows on each bookshelf and each bookshelf currently have 2 books. Code:

room1   =   {
    :shelf  =>  {
        :row1   =>  [
            {
                :title  =>  "Hamlet",
                :author =>  "William Shakespeare"
            }
        ],
        :row2   =>  [
            {
                :title  =>  "Pride and Prejudice",
                :author =>  "Jane Austen"
            }
        ]
    }
}

room2   =   {
    :shelf  =>  {
        :row2   =>  [
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}

We are going to move books from the shelf in the second room to the same rows on the shelf in the first room. First we will do this without setting recursive flag, i.e. same as using unmodified Hash::merge!:

room1.merge!(room2) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
puts room1

The output will tell us that the shelf in the first room would look like this:

room1   =   {
    :shelf  =>  {
        :row2   =>  [
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}

As you can see, not having recursive forced us to throw out our precious books.

Now we will do the same thing but with setting recursive flag to true. You can pass as second argument either recursive=true or just true:

room1.merge!(room2, true) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
puts room1

Now the output will tell us that we actually moved our books:

room1   =   {
    :shelf  =>  {
        :row1   =>  [
            {
                :title  =>  "Hamlet",
                :author =>  "William Shakespeare"
            }
        ],
        :row2   =>  [
            {
                :title  =>  "Pride and Prejudice",
                :author =>  "Jane Austen"
            },
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}

That last execution could be rewritten as following:

room1 = room1.merge(room2, recursive=true) do |k, v1, v2|
    if v1.is_a?(Array) && v2.is_a?(Array)
        v1+v2
    else
        v2
    end
end
puts room1

or

block = Proc.new {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
room1.merge!(room2, recursive=true, &block)
puts room1

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

MOPO3OB
  • 383
  • 3
  • 16
1

I think Jon M's answer is the best, but it fails when you merge in a hash with a nil/undefined value. This update solves the issue:

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

a.deep_merge(b)
koendc
  • 173
  • 1
  • 6
-1
a[:book] = a[:book] + b[:book]

Or

a[:book] <<  b[:book].first
nkm
  • 5,844
  • 2
  • 24
  • 38
  • 1
    This will work in this particular case, but considering this question's title and its placement in search results, I think we want a generic recursive merge solution here. – Matt Zukowski Aug 25 '14 at 19:35