Ruby:合并嵌套哈希

Ruby:合并嵌套哈希,ruby,hash,merge,nested,Ruby,Hash,Merge,Nested,我想合并一个嵌套哈希 a = {:book=> [{:title=>"Hamlet", :author=>"William Shakespeare" }]} b = {:book=> [{:title=>"Pride and Prejudice", :author=>"Jane Austen" }]} 我希望合并为: {:book=> [{:title=>"Hamlet",

我想合并一个嵌套哈希

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"}]}
实现这一目标的最佳方法是什么

a[:book] = a[:book] + b[:book]


a[:book]为了多样性,只有当您想以相同的方式合并散列中的所有键时,才可以这样做:

a.merge(b) { |k, x, y| x + y }
将块传递给
Hash#merge
时,
k
是要合并的键,其中键同时存在于
a
b
中,
x
a[k]
的值,
y
b[k]
的值。块的结果成为键
k
的合并哈希中的值


不过,我认为在您的具体案例中,nkm的答案更好。

我找到了一种更通用的深度合并算法,并像这样使用它:

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)

对于rails 3.0.0+或更高版本,有一个函数可以完全满足您的要求。

回答您的问题有点晚,但我不久前编写了一个相当丰富的深度合并实用程序,现在由Daniel Deleo在Github上维护:

它将完全按照您的需要合并阵列。根据文档中的第一个示例:

如果你有两个这样的散列:

   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}
room1   =   {
    :shelf  =>  {
        :row2   =>  [
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}
它不会合并:y(因为int和array不可合并)-使用bang(!)语法会导致源代码被覆盖。。当发现不可合并的实体时,使用非bang方法将保留dest的内部值。它将把:x中包含的数组添加到一起,因为它知道如何合并数组。它处理包含任何数据结构的哈希的任意深度合并


现在Daniel的github repo上有更多文档了。

我认为Jon M的答案是最好的,但是当你在一个散列中合并一个nil/未定义的值时,它失败了。 此更新解决了以下问题:

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)

在我看来,所有的答案都过于复杂了。以下是我最终得出的结论:

# @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或任何许可证来添加Jon M和koendc的答案,下面的代码将处理散列的合并,和:nil如上所述,但它也将合并两个散列中存在的任何数组(使用相同的密钥):


对于递归合并,这里有一个更好的解决方案,它使用细化,并具有bang方法,以及块支持。这段代码在纯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
执行
使用HashRecursive
后,您可以使用默认的
哈希::合并
哈希::合并就好像它们没有被修改过一样。您可以像以前一样使用这些方法使用

新的事情是,您可以将boolean
recursive
(第二个参数)传递给这些修改过的方法,它们将递归地合并散列


关于简单用法的示例写在。下面是一个高级示例

这个问题中的示例很糟糕,因为它与递归合并无关。下面的行将符合问题的示例:

a.merge!(b) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
让我给你一个更好的例子来展示上面代码的威力。想象两个房间,每个房间都有一个书架。每个书架上有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"
            }
        ]
    }
}
我们将把书从第二个房间的书架移到第一个房间书架上的同一排。首先,我们将在不设置
recursive
标志的情况下执行此操作,即使用未修改的
Hash::merge

room1.merge!(room2) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
puts room1
输出将告诉我们,第一个房间中的搁板如下所示:

   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}
room1   =   {
    :shelf  =>  {
        :row2   =>  [
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}
正如你所看到的,没有递归
迫使我们扔掉了珍贵的书籍

现在我们将做同样的事情,但是将
recursive
标志设置为true。您可以作为第二个参数传递
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


就这样。还可以看看我的递归版本的
Hash::each
Hash::each_pair
)。

NoMethodError:undefined方法“+”for{:color=>“red”}:Hash看起来您正试图将此答案与包含其他键的哈希一起使用-
{:color=>“red”}
不在您的示例中。正如我在回答中所说的,只有当你想以相同的方式合并散列中的所有键时,这才有效。也许你可以将你正在使用的散列全部添加到问题中?这实际上是一个非常方便的技巧!不知道
Hash#merge
采用了可选块。如果将键的散列合并到数组中,您也可以这样做来合并空列表:
a.merge(b){| k,x,y | x+(y?y:[])}
这不起作用。它将用新阵列替换现有阵列。它似乎适用于Rails 4:否。虽然Rails 3.2.18确实有
deep\u merge
方法,但它只接受版本4.0中的块。2@mirelonOP没有要求阻塞,所以答案是正确的。然而,问题不是关于Rails,是吗?因此,这是一个有点令人恼火的答案,因为这个答案的得票最多。这在这种特殊情况下会起作用,但考虑到这个问题的标题及其在搜索结果中的位置,我认为我们需要一个通用的递归合并解决方案。我没有看到
nil
值的问题。举一个有问题的例子。nil.deep_merge({'one'=>'two'})引发'nil'的'no method'deep_merge'。注意:对于散列中的数组,它只覆盖现有数组值。请用@Dan回答,以获得更精确的阵列处理。我建议标记或接受。
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