从数组中动态创建哈希表

3

我希望动态创建一个哈希表,但不想覆盖来自数组的键。每个数组都有一个包含应该创建的嵌套键的字符串。然而,我遇到了覆盖键的问题,因此只有最后一个键存在。

data = {}

values = [
  ["income:concessions",  0, "noi", "722300",  "purpose", "refinancing"],
  ["fees:fee-one", "0" ,"income:gross-income", "900000", "expenses:admin", "7500"],
  ["fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
]

应该如何呈现:

{ 
  "income" => {
    "concessions" => 0,
    "gross-income" => "900000"
    },
    "expenses" => {
      "admin" => "7500",
      "other" => "0"
    } 
  "noi" => "722300", 
  "purpose" => "refinancing", 
  "fees" => {
    "fee-one" => 0,
    "fee-two" => 0
    },

  "address" => {
    "zip" => "10019"
  }
}

这是我目前拥有的代码,当我合并时如何避免覆盖键?
values.each do |row|
  Hash[*row].each do |key, value|
    keys = key.split(':')

    if !data.dig(*keys)
      hh = keys.reverse.inject(value) { |a, n| { n => a } }
      a = data.merge!(hh)
    end

  end
end
4个回答

4
您提供的代码可以进行修改,以在发生冲突时合并哈希表而不是覆盖它们:
values.each do |row|
  Hash[*row].each do |key, value|
    keys = key.split(':')

    if !data.dig(*keys)
      hh = keys.reverse.inject(value) { |a, n| { n => a } }
      data.merge!(hh) { |_, old, new| old.merge(new) }
    end
  end
end

但是这段代码只适用于两层嵌套。

顺便提一下,我注意到问题中标记了 ruby-on-rails 标签。有一个名为 deep_merge 的方法可以解决这个问题:

values.each do |row|
  Hash[*row].each do |key, value|
    keys = key.split(':')

    if !data.dig(*keys)
      hh = keys.reverse.inject(value) { |a, n| { n => a } }
      data.deep_merge!(hh)
    end
  end
end

3
values.flatten.each_slice(2).with_object({}) do |(f,v),h|
  k,e = f.is_a?(String) ? f.split(':') : [f,nil]
  h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
end 
  #=> {"income"=>{"concessions"=>0, "gross-income"=>"900000"},
  #    "noi"=>"722300",
  #    "purpose"=>"refinancing",
  #    "fees"=>{"fee-one"=>"0", "fee-two"=>"0"},
  #    "expenses"=>{"admin"=>"7500", "other"=>"0"},
  #    "address"=>{"zip"=>"10019"}}

步骤如下所示。
values = [
  ["income:concessions",  0, "noi", "722300",  "purpose", "refinancing"],
  ["fees:fee-one", "0" ,"income:gross-income", "900000", "expenses:admin", "7500"],
  ["fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
]

a = values.flatten
  #=> ["income:concessions", 0, "noi", "722300", "purpose", "refinancing",
  #    "fees:fee-one", "0", "income:gross-income", "900000", "expenses:admin", "7500",
  #    "fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
enum1 = a.each_slice(2)
  #=> #<Enumerator: ["income:concessions", 0, "noi", "722300",
  #    "purpose", "refinancing", "fees:fee-one", "0", "income:gross-income", "900000",
  #    "expenses:admin", "7500", "fees:fee-two", "0", "address:zip", "10019",
  # "expenses:other","0"]:each_slice(2)>

我们可以通过将此枚举器转换为数组来查看它将生成哪些值。
enum1.to_a
  #=> [["income:concessions", 0], ["noi", "722300"], ["purpose", "refinancing"],
  #    ["fees:fee-one", "0"], ["income:gross-income", "900000"],
  #    ["expenses:admin", "7500"], ["fees:fee-two", "0"],
  #    ["address:zip", "10019"], ["expenses:other", "0"]]

接下来,

enum2 = enum1.with_object({})
  #=> #<Enumerator: #<Enumerator:
  #     ["income:concessions", 0, "noi", "722300", "purpose", "refinancing",
  #      "fees:fee-one", "0", "income:gross-income", "900000", "expenses:admin", "7500",
  #      "fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
  #      :each_slice(2)>:with_object({})>
enum2.to_a
  #=> [[["income:concessions", 0], {}], [["noi", "722300"], {}],
  #    [["purpose", "refinancing"], {}], [["fees:fee-one", "0"], {}],
  #    [["income:gross-income", "900000"], {}], [["expenses:admin", "7500"], {}],
  #    [["fees:fee-two", "0"], {}], [["address:zip", "10019"], {}],
  #    [["expenses:other", "0"], {}]]
enum2可以被看作是一个复合枚举器(虽然Ruby没有这样的概念)。生成的哈希表最初为空,如所示,但随着enum2生成更多元素,哈希表将被填充。
第一个值由enum2生成并传递给块,块值通过称为数组分解的过程被赋值。
(f,v),h = enum2.next
  #=> [["income:concessions", 0], {}]
f #=> "income:concessions"
v #=> 0
h #=> {}

我们现在进行块计算。
f.is_a?(String)
  #=> true
k,e = f.is_a?(String) ? f.split(':') : [f,nil]
  #=> ["income", "concessions"]
e.nil?
  #=> false
h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
  #=> {"concessions"=>0}

h[k]表示如果h没有键k,则等于nil。在这种情况下(h[k] || {}) #=> {}。如果h有键k(且h[k]不是nil),则(h[k] || {}) #=> h[k]

现在enum2生成第二个值并传递给块。

(f,v),h = enum2.next
  #=> [["noi", "722300"], {"income"=>{"concessions"=>0}}]
f #=> "noi"
v #=> "722300"
h #=> {"income"=>{"concessions"=>0}}

请注意,哈希值h已更新。请记住,在生成enum2的所有元素后,它将由块返回。现在我们执行块计算。
f.is_a?(String)
  #=> true
k,e = f.is_a?(String) ? f.split(':') : [f,nil]
  #=> ["noi"]
e #=> nil
e.nil?
  #=> true
h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
  #=> "722300"
h #=> {"income"=>{"concessions"=>0}, "noi"=>"722300"}

剩下的计算方式类似。

谢谢您的赞美之词。我很高兴您觉得它有帮助。 - Cary Swoveland

2

merge 默认情况下会覆盖重复的键。

{ "income"=> { "concessions" => 0 } }.merge({ "income"=> { "gross-income" => "900000" } } 完全覆盖了原始的"income"值。你需要进行递归合并,在出现重复时不仅合并顶层哈希,还要合并嵌套的值。

merge 接受一个块,您可以在重复事件中指定该块应执行的操作。来自文档:

merge!(other_hash){|key, oldval, newval| block} → hsh

将 other_hash 的内容添加到 hsh 中。如果没有指定块,则使用来自 other_hash 的值覆盖具有重复键的条目;否则,通过调用带有键、其在 hsh 中的值以及其在 other_hash 中的值的块来确定每个重复键的值

使用此方法,您可以在一行代码中定义一个简单的 recursive_merge

def recursive_merge!(hash, other)
  hash.merge!(other) { |_key, old_val, new_val| recursive_merge!(old_val, new_val) }
end

values.each do |row|
  Hash[*row].each do |key, value|
    keys = key.split(':')
  
    if !data.dig(*keys)
      hh = keys.reverse.inject(value) { |a, n| { n => a } }
      a = recursive_merge!(data, hh)
    end
  end
end

几行额外的代码可以让你得到更强大的解决方案,它可以覆盖非哈希重复键,甚至像merge一样接受块。
def recursive_merge!(hash, other, &block)
  hash.merge!(other) do |_key, old_val, new_val|
    if [old_val, new_val].all? { |v| v.is_a?(Hash) }
      recursive_merge!(old_val, new_val, &block)
    elsif block_given?
      block.call(_key, old_val, new_val)
    else
      new_val
    end
  end
end

h1 = { a: true, b: { c: [1, 2, 3] } }
h2 = { a: false,  b: { x: [3, 4, 5] } }
recursive_merge!(h1, h2) { |_k, o, _n| o } # => { a: true,  b: { c: [1, 2, 3],  x: [3, 4, 5] } }

注意:如果你使用Rails,这个方法会产生与ActiveSupport的Hash#deep_merge相同的结果。


1
这是我处理此事的方式:
def new_h 
  Hash.new{|h,k| h[k] = new_h}
end 

values.flatten.each_slice(2).each_with_object(new_h) do |(k,v),obj|
  keys =  k.is_a?(String) ? k.split(':') : [k]
  if keys.count > 1
    set_key = keys.pop
    obj.merge!(keys.inject(new_h) {|memo,k1| memo[k1] = new_h})
      .dig(*keys)
      .merge!({set_key => v})
  else
    obj[k] = v
  end
end
#=> {"income"=>{
       "concessions"=>0, 
       "gross-income"=>"900000"}, 
    "noi"=>"722300", 
    "purpose"=>"refinancing", 
    "fees"=>{
        "fee-one"=>"0", 
        "fee-two"=>"0"}, 
    "expenses"=>{
        "admin"=>"7500", 
        "other"=>"0"}, 
    "address"=>{
        "zip"=>"10019"}
    }

解释如下:
  • 定义一个方法(new_h),用于设置带有默认new_h的新Hash,可以在任何级别上实现(Hash.new{|h,k| h[k] = new_h})
  • 首先展开Array (values.flatten)
  • 然后将每两个元素作为伪键值对分组(.each_slice(2))
  • 然后使用累加器迭代这些键值对,在添加每个新元素时都默认为Hash (.each_with_object(new_h.call) do |(k,v),obj|)
  • 按冒号拆分伪键(keys = k.is_a?(String) ? k.split(':') : [k])
  • 如果进行了拆分,则创建父键(obj.merge!(keys.inject(new_h.call) {|memo,k1| memo[k1] = new_h.call}))
  • 合并最后一个子键和值(obj.dig(*keys.merge!({set_key => v}))
  • 否则将单个键设置为相应的值(obj[k] = v)
这个深度是无限的,只要深度链没有被打破,比如说在这种情况下 [["income:concessions:other",12],["income:concessions", 0]] 后面的值将优先生效(注意:这适用于所有答案,例如被接受的答案前者胜出,但由于不准确的数据结构仍会丢失值)。

repl.it 示例


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