Ruby:合并嵌套哈希

67

我想合并一个嵌套的哈希表。

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

如何最好地完成这个目标?


5
我建议将Jon M的回答Dan的回答标记为被采纳的答案。 - Neonit
9个回答

63

对于Rails 3.0.0+或更高版本,ActiveSupport提供了deep_merge函数,该函数能够实现您所要求的功能。


11
这不起作用。它将用新数组替换现有数组。 这个方法在Rails 4中似乎有效:http://apidock.com/rails/v4.0.2/Hash/deep_merge - Sascha Kaestle
不行。虽然Rails 3.2.18确实有deep_merge方法,但它只从4.0.2版本开始接受块。 - mirelon
@mirelon,楼主没有要求代码块,所以这个回答是正确的。 - Janko
13
但问题不是关于Rails,是吗?所以这个答案获得最多赞的事实有点让人不爽。 - Neonit

58

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

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)

注意:对于哈希内的数组,它只覆盖现有的数组值。可以参考@Dan的回答来进行更复杂的数组处理。 - geekQ
https://github.com/rubocop/ruby-style-guide#namespace-definition - Cory Kendall

53

在Jon M和koendc的回答基础上,以下代码将处理哈希的合并及其nil值,但它也将联合两个哈希中存在的所有数组(具有相同的键):

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)

11

为了多样化,如果你想以相同的方式合并哈希表中的所有键,则可以这样做:

a.merge(b) { |k, x, y| x + y }
当您将块传递给Hash#merge方法时,k是要合并的键,其中键存在于ab中,xa[k]的值,yb[k]的值。块的结果成为合并哈希的键k的值。
我认为在您的特定情况下,nkm的答案更好。

1
NoMethodError: undefined method `+' for {:color=>"red"}:Hash未定义方法错误:{:color =>“red”}:Hash没有‘+’方法。 - user1223862
看起来你正在尝试使用包含其他键的哈希表 - {:color=>"red"} 不在你的示例中。正如我在我的答案中所说,只有当你想以相同的方式合并哈希表中的所有键时,这才有效。 - Russell
也许您可以在问题中完整添加您正在使用的哈希? - Russell
1
这实际上是一个非常方便的技巧!我不知道 Hash#merge 接受可选块。 - Damien Wilson
如果要将键的哈希合并到数组中,您可以像这样做以合并空列表:a.merge(b) { |k, x, y| x + (y ? y : []) } - Dave

8
很晚才回答你的问题,但我之前编写了一个相当丰富的深度合并实用程序,现在由Daniel Deleo在Github上维护:https://github.com/danielsdeleo/deep_merge 它会按照您想要的方式完全合并您的数组。从文档中的第一个例子开始:
因此,如果您有两个像这样的哈希:
   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和array不被视为可合并的) - 使用感叹号(!)语法会导致源覆盖..使用非感叹号方法将在找到不可合并实体时保留dest的内部值。它会将包含在:x中的数组相加,因为它知道如何合并数组。它处理包含任意数据结构的哈希任意深度合并。
丹尼尔的github仓库现在有更多文档。

4

我觉得所有的回答都太复杂了。这是我最终想到的:

# @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或其他许可证


2

这里有一个更好的解决方案,用到了细化技术来进行递归合并,并且提供了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 后,您可以像未修改过一样使用默认的 Hash::merge 和 Hash::merge!。您可以像以前一样在这些方法中使用块。

新的变化是您可以将布尔值 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

输出结果将告诉我们,第一个房间的货架看起来像这样:
room1   =   {
    :shelf  =>  {
        :row2   =>  [
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}

正如您所看到的,没有设置 recursive 强制我们丢弃了宝贵的书籍。

现在我们将使用设置 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

或者

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::each(Hash::each_pair)的递归版本在这里


1

我认为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)

我并没有看到nil值存在问题。请给出一个存在问题的例子。 - akostadinov
nil.deep_merge({'one'=>'two'}) 会引发“没有'deep_merge'方法适用于'nil'”的错误。 - user2066657

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

或者

a[:book] <<  b[:book].first

1
这在特定情况下可以工作,但考虑到这个问题的标题和它在搜索结果中的位置,我认为我们想要一个通用的递归合并解决方案。 - Matt Zukowski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接