如何展平哈希,使每个键成为唯一值?

Posted

技术标签:

【中文标题】如何展平哈希,使每个键成为唯一值?【英文标题】: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) 而无需定义它。 【参考方案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 "#indentParsing #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

以上是关于如何展平哈希,使每个键成为唯一值?的主要内容,如果未能解决你的问题,请参考以下文章

已更新或删除的行值要么不能使该行成为唯一行,要么改变了多个行,如何解决

如何在数组元素中搜索哈希键中的匹配项

sql server"已更新或删除的行值要么不能使该行成为唯一行,要么改变了多个行" 解决方案

如何使MongoDB的列成为SQL Server中发生的主键,避免重复记录?

如何使我的观点之一成为唯一的风景

如何使外键成为必需?