Rubyでは Hash#merge
を使うことで、2つのハッシュをマージできます。
Hash#merge (Ruby 3.2 リファレンスマニュアル)
2つのハッシュでキーが異なる場合は、それぞれのキーを持つハッシュへとマージされます。
h1 = { name: 'foo' } h2 = { color: 'red' } h1.merge(h2) #=> { name: 'foo', color: 'red' }
一方、2つのハッシュでキーが重複する場合は、 merge
メソッドに渡した引数のハッシュの値が使われます。
h1 = { name: 'foo' } h2 = { name: 'bar' } h1.merge(h2) #=> { name: 'bar' }
そのため、ネストしたハッシュをマージしようとしても、ネストしたハッシュが統合されず、 merge
メソッドに渡した引数のハッシュが使われます。
h1 = { name: { apple: 'シナノゴールド', } } h2 = { name: { sweet_potato: 'シルクスイート' } } h1.merge(h2) #=> { name: { sweet_potato: 'シルクスイート' } }
ネストしたハッシュもうまいことマージする方法がないかを調べたところ、 ActiveSupport#deep_merge
がありました。
- 11.2.3 deep_mergeとdeep_merge! | Active Support コア拡張機能 - Railsガイド
- https://api.rubyonrails.org/v7.0.4/classes/Hash.html#method-i-deep_merge
そこで、使ってみたときのことをメモとして残します。
目次
環境
- Rails 7.0.4.2
deep_mergeによる、ネストしたハッシュのマージ
同じキーがない場合、そのままマージ
Hash#merge
と同じ挙動が、ネストしたハッシュにも適用されています。
h1 = { name: { apple: 'シナノゴールド', } } h2 = { name: { sweet_potato: 'シルクスイート' } } h1.deep_merge(h2) #=> { name: { apple: 'シナノゴールド', sweet_potato: 'シルクスイート' } }
同じキーがある場合、deep_mergeの引数を採用する
こちらも、Hash#merge
と同じ挙動が、ネストしたハッシュにも適用されています。
h1 = { name: { apple: 'シナノゴールド', } } h2 = { name: { apple: '秋映', } } h1.deep_merge(h2) #=> { name: { apple: '秋映' } }
シンボルと文字列で同じ名前をもつハッシュのマージ
deep_mergeだけだと、別のキー扱い
Hash#merge
ではシンボルと文字列は別のキーという扱いでマージされます。
h1 = { name: 'foo' } h2 = { 'name' => 'bar' } h1.merge(h2) #=> { name: 'foo', 'name' => 'bar' }
ActiveSupport#deep_merge
も同様な挙動です。
h1 = { name: 'foo' } h2 = { 'name' => 'bar' } h1.deep_merge(h2) #=> { name: 'foo', 'name' => 'bar' }
deep_merge + with_indifferent_access を組み合わせてマージ
ActiveSupportには、ハッシュのシンボルと文字列を同様に扱う with_indifferent_access
メソッドがあります。
11.8 ハッシュキーのシンボルと文字列を同様に扱う(indifferent access) | Active Support コア拡張機能 - Railsガイド
そこで、 deep_merge
と with_indifferent_access
を組み合わせて使うと、同じキーとしてマージされました。
h1 = { name: 'foo' } h2 = { 'name' => 'bar' } h1.with_indifferent_access.deep_merge(h2) #=> #<HashWithIndifferentAccess { 'name' => 'bar' }>
ネストしているハッシュでも、同じキーという扱いでマージされました。
h1 = { name: { apple: 'シナノゴールド', } } h2 = { name: { 'apple' => '秋映', } } h1.with_indifferent_access.deep_merge(h2) #=> #<HashWithIndifferentAccess { "name" => #<HashWithIndifferentAccess { "apple" => "秋映" }> }>
deep_mergeにblockを渡す
重複キーに対する値を決められる
ActiveSupport#deep_merge
の説明を見ると、
Like with Hash#merge in the standard library, a block can be provided to merge values
https://api.rubyonrails.org/v7.0.4/classes/Hash.html#method-i-deep_merge
とのことでした。
次に Hash#merge
の説明を見ると
ブロック付きのときはブロックを呼び出してその返す値を重複キーに対応する値にします。
https://docs.ruby-lang.org/ja/latest/method/Hash/i/merge.html
とのことです。
試してみると、たしかに Hash#merge
と同じ挙動でした。
# フラットなハッシュ h1 = { name: 'foo' } h2 = { name: 'bar' } h1.deep_merge(h2) do |_key, this_val, other_val| this_val + other_val end #=> { name: 'foobar' } # ネストしたハッシュ h1 = { name: { apple: 'シナノゴールド', } } h2 = { name: { apple: '秋映', } } h1.deep_merge(h2) do |key, this_val, other_val| "key: #{key} > #{this_val} and #{other_val}" end #=> { name: { apple: 'key: apple > シナノゴールド and 秋映' }}
以上より、 merge
同様、 deep_merge
もblockにてマージ結果を指定できることから、
- 結果のハッシュにキーがなければ、その値を入れる
- 結果のハッシュにキーがあれば、加算する
ような場合でも、 if
ではなく deep_merge
を使って表現できそうです。
# if - else を使う result = {} [{ apple: 100 }, { sweet_potato: 50}, { apple: 200 }].each do |item| item.each do |k, v| if result.has_key?(k) result[k] += v else result[k] = v end end end # deep_mergeを使う result = {} [{ apple: 100 }, { sweet_potato: 50}, { apple: 200 }].each do |item| result.deep_merge(item) { |_key, this_val, other_val| this_val + other_val } end
ちなみに、もし、シンボルと文字列で同じ名前なキーを持つ場合でも加算したい場合は、 with_indifferent_access
を合わせて使います。
h1 = { amount: { apple: 100, sweet_potato: 50 } } h2 = { amount: { 'apple' => 200, } } h1.with_indifferent_access.deep_merge(h2) do |_key, this_val, other_val| this_val + other_val end #=> #<HashWithIndifferentAccess { "amount" => #<HashWithIndifferentAccess { "apple" => 300, "sweet_potato" => 50 }> }>
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app
プルリクはこちらです。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app/pull/11
なお、記事中のソースコードはhelperとhelper_specに記載してあります。