web-dev-qa-db-ja.com

Ruby:ネストされたハッシュのマージ

ネストされたハッシュをマージしたいと思います。

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

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

マージを次のようにします。

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

これを達成するためのネスト方法は何ですか?

46
user1223862

Rails 3.0.0+以降のバージョンでは、 deep_merge 関数が ActiveSupport まさにあなたが求めていることをします。

51
xlembouras

より一般的なディープマージアルゴリズム here を見つけて、次のように使用しました。

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)
46
Jon M

Jon Mとkoendcの回答に追加するために、以下のコードはハッシュのマージを処理し、上記のように:nilを処理しますが、両方のハッシュに存在する配列を結合します(同じキーを使用):

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)
33
Dan

さまざまな理由で-これは、ハッシュ内のすべてのキーを同じ方法でマージする場合にのみ機能します-これを行うことができます:

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

ブロックをHash#mergeに渡すと、kはマージされるキーであり、キーはabxの両方に存在しますa[k]の値であり、yb[k]の値です。ブロックの結果は、キーkのマージされたハッシュの値になります。

あなたの特定のケースでは、nkmの答えの方が良いと思います。

10
Russell

あなたの質問に答えるのに少し遅れましたが、GithubでDaniel Deleoによって現在保守されているかなり豊富なディープマージユーティリティを書きました: https://github.com/danielsdeleo/deep_merge

必要に応じて正確に配列をマージします。ドキュメントの最初の例から:

したがって、次のような2つのハッシュがある場合:

   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}

マージされません:y(intと配列はマージ可能と見なされないため)-bang(!)構文を使用すると、ソースが上書きされます。非bangメソッドを使用すると、マージ不可能なエンティティが見つかりました。配列をマージする方法を知っているため、:xに含まれる配列を一緒に追加します。任意のデータ構造を含むハッシュの任意の深さのマージを処理します。

ダニエルのGitHubレポジトリに関するドキュメントが増えました。

6
Steve Midgley

すべての答えは複雑すぎるように見えます。最終的に思いついたのは次のとおりです。

# @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

追伸パブリック、WTFPL、または任意のライセンスとして使用

4
akostadinov

再帰的マージのさらに良い解決策は、refinmentsを使用し、bang methodblockサポートこのコードはpureRubyで動作します。

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

using HashRecursiveが実行された後、デフォルトのHash::mergeおよびHash::merge!を変更していないかのように使用できます。以前と同様に、これらのメソッドでblocksを使用できます。

新しいことは、これらの変更されたメソッドにブールrecursive(2番目の引数)を渡すことができ、ハッシュを再帰的にマージすることです。


簡単な使用例this answer に書かれています。高度な例を次に示します。

この質問の例は、再帰的なマージとは何の関係もないので悪いです。次の行は質問の例を満たします。

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

上記のコードの威力を示すために、より良い例を挙げましょう。 2つの部屋を想像してください。各部屋には本棚が1つあります。各本棚には3行があり、各本棚には現在2冊の本があります。コード:

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

本を2番目の部屋の棚から最初の部屋の棚の同じ行に移動します。最初にrecursiveフラグを設定せずにこれを行います。つまり、変更されていないHash::merge!を使用するのと同じです。

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

出力から、最初の部屋の棚は次のようになることがわかります。

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

ご覧のとおり、recursiveがなかったため、私たちは貴重な本を捨てざるを得ませんでした。

次に、同じことを行いますが、recursiveフラグをtrueに設定します。 2番目の引数としてrecursive=trueまたは単にtrueのいずれかを渡すことができます。

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

これで、出力から、実際に本を移動したことがわかります。

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

その最後の実行は、次のように書き換えられます。

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

または

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

それでおしまい。また、再帰バージョンのHash::eachHash::each_pairhere もご覧ください。

1
MOPO3OB

Jon Mの答えは最高だと思いますが、ハッシュをnil/undefined値でマージすると失敗します。このアップデートは問題を解決します:

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)
0
koendc