从数组数组动态创建哈希

时间:2017-08-15 19:08:25

标签: ruby-on-rails arrays ruby algorithm

我想动态创建一个Hash,而不会覆盖数组数组中的键。每个数组都有一个字符串,其中包含应创建的嵌套键。但是,我遇到了覆盖密钥的问题,因此只有最后一个密钥

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 个答案:

答案 0 :(得分: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

答案 1 :(得分: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生成并传递给块,块值由一个名为消歧分解的过程分配值。 / p>

(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]没有密钥nil,则

h等于k。在那种情况下(h[k] || {}) #=> {}。如果h确实有一个密钥kh[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"}

其余的计算方法类似。

答案 2 :(得分:2)

默认情况下,

merge会覆盖重复的密钥。

{ "income"=> { "concessions" => 0 } }.merge({ "income"=> { "gross-income" => "900000" } }完全覆盖"income"的原始值。你想要的是一个递归合并,而不是只是合并顶级哈希,而是在重复时合并嵌套值。

merge需要一个块,您可以在其中指定在复制时要执行的操作。来自文档:

  

合并!(other_hash){| key,oldval,newval |阻止}→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获得的结果。

答案 3 :(得分: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),用于在任何级别(Hash)设置默认new_h的新Hash.new{|h,k| h[k] = new_h}
  • 首先展平Arrayvalues.flatten
  • 然后将每个2个元素组合为sudo键值对(.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 Example