【问题标题】:How to flatten a hash, making each key a unique value?如何展平哈希,使每个键成为唯一值?
【发布时间】:2018-07-27 22:26:14
【问题描述】:

我想使用嵌套散列和数组获取散列,并将其展平为具有唯一值的单个散列。我一直试图从不同的角度来解决这个问题,但后来我让它变得比它需要的复杂得多,让自己迷失在正在发生的事情中。

示例源哈希:

{
  "Name" => "Kim Kones",
  "License Number" => "54321",
  "Details" => {
    "Name" => "Kones, Kim",
    "Licenses" => [
      {
        "License Type" => "PT",
        "License Number" => "54321"
      },
      {
        "License Type" => "Temp",
        "License Number" => "T123"
      },
      {
        "License Type" => "AP",
        "License Number" => "A666",
        "Expiration Date" => "12/31/2020"
      }
    ]
  }
}

所需的哈希示例:

{
  "Name" => "Kim Kones",
  "License Number" => "54321",
  "Details_Name" => "Kones, Kim",
  "Details_Licenses_1_License Type" => "PT",
  "Details_Licenses_1_License Number" => "54321",
  "Details_Licenses_2_License Type" => "Temp",
  "Details_Licenses_2_License Number" => "T123",
  "Details_Licenses_3_License Type" => "AP",
  "Details_Licenses_3_License Number" => "A666",
  "Details_Licenses_3_Expiration Date" => "12/31/2020"
}

不管怎样,这是我放弃前最近的一次尝试。

def flattify(hashy)
    temp = {}
    hashy.each do |key, val|
        if val.is_a? String
            temp["#{key}"] = val
        elsif val.is_a? Hash
            temp.merge(rename val, key, "")
        elsif val.is_a? Array
            temp["#{key}"] = enumerate val, key
        else
        end
        print "=> #{temp}\n"
    end
    return temp
end

def rename (hashy, str, n)
    temp = {}
    hashy.each do |key, val|
        if val.is_a? String
            temp["#{key}#{n}"] = val
        elsif val.is_a? Hash
            val.each do |k, v|
                temp["#{key}_#{k}#{n}"] = v
            end
        elsif val.is_a? Array
            temp["#{key}"] = enumerate val, key
        else

        end
    end
    return flattify temp
end

def enumerate (ary, str)
    temp = {}
    i = 1
    ary.each do |x|
        temp["#{str}#{i}"] = x
        i += 1
    end
    return flattify temp
end

【问题讨论】:

  • 你以前有没有看过深度优先搜索?
  • 如果您将变量分配给“示例源哈希”(例如,h = { "Name" => "Kim Kones", ... }),将会很有帮助。这样用户就可以在答案和 cmets 中引用变量 (h) 而无需定义它。

标签: ruby hash flatten


【解决方案1】:

有趣的问题!

理论

这是解析数据的递归方法。

  • 它会跟踪它找到的键和索引。
  • 它将它们附加到 tmp 数组中。
  • 找到叶对象后,会将其写入哈希值作为值,并以连接的tmp 作为键。
  • 然后这个小散列被递归地合并回主散列。

代码

def recursive_parsing(object, tmp = [])
  case object
  when Array
    object.each.with_index(1).with_object({}) do |(element, i), result|
      result.merge! recursive_parsing(element, tmp + [i])
    end
  when Hash
    object.each_with_object({}) do |(key, value), result|
      result.merge! recursive_parsing(value, tmp + [key])
    end
  else
    { tmp.join('_') => object }
  end
end

举个例子:

require 'pp'
pp recursive_parsing(data)
# {"Name"=>"Kim Kones",
#  "License Number"=>"54321",
#  "Details_Name"=>"Kones, Kim",
#  "Details_Licenses_1_License Type"=>"PT",
#  "Details_Licenses_1_License Number"=>"54321",
#  "Details_Licenses_2_License Type"=>"Temp",
#  "Details_Licenses_2_License Number"=>"T123",
#  "Details_Licenses_3_License Type"=>"AP",
#  "Details_Licenses_3_License Number"=>"A666",
#  "Details_Licenses_3_Expiration Date"=>"12/31/2020"}

调试

这是一个带有老式调试的修改版本。它可能会帮助您了解发生了什么:

def recursive_parsing(object, tmp = [], indent="")
  puts "#{indent}Parsing #{object.inspect}, with tmp=#{tmp.inspect}"
  result = case object
  when Array
    puts "#{indent} It's an array! Let's parse every element:"
    object.each_with_object({}).with_index(1) do |(element, result), i|
      result.merge! recursive_parsing(element, tmp + [i], indent + "  ")
    end
  when Hash
    puts "#{indent} It's a hash! Let's parse every key,value pair:"
    object.each_with_object({}) do |(key, value), result|
      result.merge! recursive_parsing(value, tmp + [key], indent + "  ")
    end
  else
    puts "#{indent} It's a leaf! Let's return a hash"
    { tmp.join('_') => object }
  end
  puts "#{indent} Returning #{result.inspect}\n"
  result
end

当使用recursive_parsing([{a: 'foo', b: 'bar'}, {c: 'baz'}]) 调用时,它会显示:

Parsing [{:a=>"foo", :b=>"bar"}, {:c=>"baz"}], with tmp=[]
 It's an array! Let's parse every element:
  Parsing {:a=>"foo", :b=>"bar"}, with tmp=[1]
   It's a hash! Let's parse every key,value pair:
    Parsing "foo", with tmp=[1, :a]
     It's a leaf! Let's return a hash
     Returning {"1_a"=>"foo"}
    Parsing "bar", with tmp=[1, :b]
     It's a leaf! Let's return a hash
     Returning {"1_b"=>"bar"}
   Returning {"1_a"=>"foo", "1_b"=>"bar"}
  Parsing {:c=>"baz"}, with tmp=[2]
   It's a hash! Let's parse every key,value pair:
    Parsing "baz", with tmp=[2, :c]
     It's a leaf! Let's return a hash
     Returning {"2_c"=>"baz"}
   Returning {"2_c"=>"baz"}
 Returning {"1_a"=>"foo", "1_b"=>"bar", "2_c"=>"baz"}

【讨论】:

  • 不错,埃里克!我倾向于在开头删除result = {},最后删除result,将object.each_with_index do |element, i| 更改为object.each_with_object({}).with_index do |(element, result), i|,将object.each do |key, value| 更改为object.each_with_object({}) do |(key, value), result| 并从result = { tmp.join('_') => object } 中删除result =,但这只是风格偏好。
  • @CarySwoveland:谢谢!很高兴知道您仍在传播each_with_object 的爱;)。
  • |(element, result), i| 遍历数组,但|(key, value), result| 的哈希让我觉得有些不一致。用object.each.with_index(1).with_object({}) do |(element, i), result| 来遍历数组怎么样?
  • 添加调试代码是一个不错的选择,这在答案中很少见。创建变量result 的唯一原因是case 语句之后的puts。另一个教育机会是放弃result 并将tap 链接到case 语句,最后的puts 在其块中。毕竟,tap 的使用是我们拥有该方法的主要原因之一。
【解决方案2】:

与其他人不同,我喜欢each_with_object :-)。但我确实喜欢传递单个结果哈希,因此我不必一遍又一遍地合并和重新合并哈希。

def flattify(value, result = {}, path = [])
  case value
  when Array
    value.each.with_index(1) do |v, i|
      flattify(v, result, path + [i])
    end
  when Hash
    value.each do |k, v|
      flattify(v, result, path + [k])
    end
  else
    result[path.join("_")] = value
  end
  result
end

(一些细节取自 Eric,见 cmets)

【讨论】:

  • 只是好奇:你是修改了我的代码还是从头开始?看看是否有其他方法可以解决这个问题会很有趣。
  • @EricDuminil 从头开始​​做,然后阅读你的并采用了一些细节(case 而不是if(1) 而不是+ 1,并且路径更新效率较低,因为我没有不想看起来比较大:-)。我认为我的第一个工作版本是:eval.in/957963。所以并没有什么不同。对于“其他方式”,我猜你的意思是不像我们的那样递归?
【解决方案3】:

非递归方法,将 BFS 与数组一起用作队列。我保留值不是数组/哈希的键值对,并将数组/哈希内容推送到队列(使用组合键)。将数组转换为哈希 (["a", "b"] ↦ {1=>"a", 2=>"b"}) 感觉很整洁。

def flattify(hash)
  (q = hash.to_a).select { |key, value|
    value = (1..value.size).zip(value).to_h if value.is_a? Array
    !value.is_a?(Hash) || !value.each { |k, v| q << ["#{key}_#{k}", v] }
  }.to_h
end

我喜欢它的一件事是"#{key}_#{k}" 这样的按键组合很好。在我的其他解决方案中,我也可以使用字符串 path = '' 并使用 path + "_" + k 对其进行扩展,但这会导致前导下划线,我必须避免或使用额外的代码进行修剪。

【讨论】:

  • 它比您的其他答案可读性差得多,但它仍然是一个非常有趣的答案。感谢您寻找替代方案!
  • @EricDuminil 是的,一旦我决定滥用这样的数组(我什至不确定在迭代期间追加是否安全......是吗?),我不再关心清洁:-P
猜你喜欢
  • 1970-01-01
  • 2011-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多