Edited at

Hashのdeep_mergeでHash内部にあるArrayも連結する

More than 1 year has passed since last update.

Rubyで深いHash同士をマージさせたいことがあります。

例えば、

a = { x: { y: { z1: 'foo' } } }

b = { x: { y: { z2: 'yoo' } } }

という a, b から

c = { x: { y: { z1: 'foo', z2: 'yoo' } } }

という c を得たいとします。


Hash#deep_merge

こういうときに使えるのが Hash#deep_merge です。

これは標準のインスタンスメソッドではなく、ActiveSupportに入っています。

deep_mergeを使えば、以下のように書けます。

c == a.deep_merge(b)

#=> true


Arrayの時は連結したい

さらに以下の要件を追加したいと思います。

d = { x: { y: { z: [1, 2, 3] } } }

e = { x: { y: { z: [4, 5] } } }

f = { x: { y: { z: [1, 2, 3, 4, 5] } } }

de を組み合わせで f を得たいとします。

つまり、deep_mergeしたい2つのHashの中の値として、Arrayが存在する場合、Array同士を連結させたい、ということです。

試しに deep_merge してみます。

d.deep_merge(e)

#=> {:x=>{:y=>{:z=>[4, 5]}}}

deの値で上書きされてしまいました。


Hash#deep_mergeはblockを渡すことができる

ここで、 deep_merge の実装を見てみます。(参考)

def deep_merge(other_hash, &block)

dup.deep_merge!(other_hash, &block)
end

def deep_merge!(other_hash, &block)
merge!(other_hash) do |key, this_val, other_val|
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
this_val.deep_merge(other_val, &block)
elsif block_given?
block.call(key, this_val, other_val)
else
other_val
end
end
end

deep_mergeが内部的に呼んでいる deep_merge! を見ると、blockを渡すことで、deep_mergeしたい2つのオブジェクトが共にHashでない場合の処理を書けるようです。


deep_mergeでArrayの場合は連結するようにする

以上より「Arrayであれば連結する」という処理をdeep_mergeにblockで渡せばよさそうなことがわかりましたので、そのように実装してみます。

d.deep_merge(e) do |key, this_val, other_val|

if this_val.is_a?(Array) && other_val.is_a?(Array)
this_val + other_val
else
other_val
end
end

#=> {:x=>{:y=>{:z=>[1, 2, 3, 4, 5]}}}

ほしい値が得られました。

おわり