如何在Ruby中将数组转换为哈希表是最佳方法?

138

在 Ruby 中,如果给定一个以下形式之一的数组...

[apple, 1, banana, 2]
[[apple, 1], [banana, 2]]

...最佳的方法是将其转换为哪种形式的哈希表?

{apple => 1, banana => 2}
12个回答

153

只需使用 Hash[*array_variable.flatten]

例如:

a1 = ['apple', 1, 'banana', 2]
h1 = Hash[*a1.flatten(1)]
puts "h1: #{h1.inspect}"

a2 = [['apple', 1], ['banana', 2]]
h2 = Hash[*a2.flatten(1)]
puts "h2: #{h2.inspect}"

使用Array#flatten(1)可限制递归,使得Array的键和值按预期工作。


4
哦,真是雄辩!这就是我喜欢 Ruby 的原因。 - Igbanam
11
警告:使用 flatten 方法会导致问题,如果您需要数组的键或值。 - Stew
1
我在下面发布了一种替代方案,可以避免数组键或值的问题。 - Stew
5
不要试图为此找到一种万能解决方案。如果你的键和值是成对出现的,例如[[key1,value1],[key2,value2]],那么只需将它传递给Hash[],不需要展开。Hash[a2] == Hash[a2.flatten]。如果数组已经展开了,例如[key1, value1, key2, value2],那么只需在变量前加上,Hash[*a1]。 - Cluster
8
如果您真的需要一个更通用的版本,您可以使用Hash [*ary.flatten(1)],它将保留数组键和值。是递归的 flatten 破坏了它们,这很容易避免。 - brymck
显示剩余6条评论

100

注意: 为了提供简洁高效的解决方案,请参见下面Marc-André Lafortune的答案

本答案最初是作为替代使用flatten方法的方案而提出的,这些方法在撰写时得到了最高的赞数。我应该澄清,我并没有打算将这个示例作为最佳实践或高效方法来呈现。原始答案如下。


警告! 使用flatten的解决方案将不会保留数组键或值!

基于@John Topley的热门答案,让我们尝试:

a3 = [ ['apple', 1], ['banana', 2], [['orange','seedless'], 3] ]
h3 = Hash[*a3.flatten]

这会抛出一个错误:

ArgumentError: odd number of arguments for Hash
        from (irb):10:in `[]'
        from (irb):10

构造函数需要一个偶数长度的数组(例如['k1','v1,'k2','v2'])。更糟糕的是,一个不同的被展平成偶数长度的数组将会默默地给我们返回一组具有不正确值的哈希表。
如果你想使用数组键或值,可以使用 map:
h3 = Hash[a3.map {|key, value| [key, value]}]
puts "h3: #{h3.inspect}"

这将保留数组键:

h3: {["orange", "seedless"]=>3, "apple"=>1, "banana"=>2}

15
这与Hash[a3]相同,因为a3 == a3.map{|k,v| [k,v]}是true,它实际上相当于a3.dup。 - Cluster
2
为什么不直接指定flatten的深度,而不是使用map呢?例如:h3 = Hash[*a3.flatten(1)] 而不是 h3 = Hash[*a3.flatten],后者会抛出错误。 - Jeff McCune
3
这个回答不够高效,而且已经过时了。请查看我的答案。 - Marc-André Lafortune
a3 = [ ['apple', 1], ['banana', 2], [['orange','seedless'], 3] ] h3 = Hash[*a3.flatten(1)] - bluexuemei
2
是的,我认为Marc-André的to_h更好。 - B Seven
1
@Marc-André Lafortune谢谢,我已经更新了我的答案以将用户引导到您的答案。 - Stew

95

最好的方法是使用Array#to_h

[ [:apple,1],[:banana,2] ].to_h  #=> {apple: 1, banana: 2}

请注意to_h也接受一个块:

[:apple, :banana].to_h { |fruit| [fruit, "I like #{fruit}s"] } 
  # => {apple: "I like apples", banana: "I like bananas"}
Note: 在 Ruby 2.6.0+ 中,to_h 可以接受一个块;对于早期版本的 Ruby,您可以使用我的 backports gem,并且require 'backports/2.6.0/enumerable/to_h'to_h没有块的形式是在 Ruby 2.1.0 中引入的。
在 Ruby 2.1 之前,可以使用不太易读的 Hash[]
array = [ [:apple,1],[:banana,2] ]
Hash[ array ]  #= > {:apple => 1, :banana => 2}

最后,要小心使用flatten的任何解决方案,因为这可能会在值本身是数组时创建问题。


4
感谢新的.to_h方法的简洁易懂! - coding addicted
3
我更喜欢 to_h 方法而不是上面的答案,因为它表达了在操作数组后转换的意图。 - B Seven
1
@BSeven,Array#to_hEnumerable#to_h都不是核心Ruby 1.9中的方法。 - Iron Savior
如果我有一个数组 [[apple, 1], [banana, 2], [apple, 3], [banana, 4]],我想要输出为 {"apple" =>[1,3], "banana"=>[2,4]},该怎么办? - nishant
@Marc-AndréLafortune 我使用了类似这样的代码 properties.map{|element| element.split '=' }.to_h,但是出现了以下错误:wrong array length at 20 (expected 2, was 3)我的创建的数组类似于这样:[key1 = value1, key2 = value2, ...]请问有人能指出我的代码哪里有问题吗?谢谢。 - Vinit Sharma
显示剩余2条评论

22

10

编辑:在我写作的时候,看到了已发布的回复,Hash[a.flatten]似乎是可行的方法。 当我思考回应时,可能错过了文档中的这一部分。如果需要,我认为我编写的解决方案可以用作替代方案。

第二种形式更简单:

a = [[:apple, 1], [:banana, 2]]
h = a.inject({}) { |r, i| r[i.first] = i.last; r }

a = 数组, h = 哈希表, r = 返回值哈希表(我们累加的那个),i = 数组中的元素

我能想到的最简洁的方式来完成第一种形式是这样的:

a = [:apple, 1, :banana, 2]
h = {}
a.each_slice(2) { |i| h[i.first] = i.last }

2
对于 a.inject({}) 这个一行代码,我给予加分,因为它允许更灵活的值赋值。 - Chris Bloom
通过使用inject,也可以从第二个示例中删除“h = {}”,最终得到“a.each_slice(2).inject({}) { |h,i| h[i.first] = i.last; h }”。 - lindes
1
你可以使用 a.each_slice(2).to_h - Conor O'Brien

6

总结:

本回答旨在综合其他回答的信息。

根据问题提供的数据和一些额外的信息,简要概括如下:

flat_array   = [  apple, 1,   banana, 2  ] # count=4
nested_array = [ [apple, 1], [banana, 2] ] # count=2 of count=2 k,v arrays
incomplete_f = [  apple, 1,   banana     ] # count=3 - missing last value
incomplete_n = [ [apple, 1], [banana   ] ] # count=2 of either k or k,v arrays


# there's one option for flat_array:
h1  = Hash[*flat_array]                     # => {apple=>1, banana=>2}

# two options for nested_array:
h2a = nested_array.to_h # since ruby 2.1.0    => {apple=>1, banana=>2}
h2b = Hash[nested_array]                    # => {apple=>1, banana=>2}

# ok if *only* the last value is missing:
h3  = Hash[incomplete_f.each_slice(2).to_a] # => {apple=>1, banana=>nil}
# always ok for k without v in nested array:
h4  = Hash[incomplete_n] # or .to_h           => {apple=>1, banana=>nil}

# as one might expect:
h1 == h2a # => true
h1 == h2b # => true
h1 == h3  # => false
h3 == h4  # => true

讨论和详细信息如下。


设置:变量

为了展示我们将要使用的数据,我将创建一些变量来代表不同的数据可能性。它们分为以下几类:

基于问题直接给出的内容,作为 a1a2

(注意:我假设 applebanana 是指代变量的。和其他人一样,我将使用字符串以便输入和结果匹配。)

a1 = [  'apple', 1 ,  'banana', 2  ] # flat input
a2 = [ ['apple', 1], ['banana', 2] ] # key/value paired input

多值键和/或值,例如a3

在其他答案中,还提出了另一种可能性(我在这里进行扩展)-键和/或值本身可以是数组:

a3 = [ [ 'apple',                   1   ],
       [ 'banana',                  2   ],
       [ ['orange','seedless'],     3   ],
       [ 'pear',                 [4, 5] ],
     ]

不平衡数组,例如a4

为了严谨起见,我想加入一个可能存在不完整输入的情况:

a4 = [ [ 'apple',                   1],
       [ 'banana',                  2],
       [ ['orange','seedless'],     3],
       [ 'durian'                    ], # a spiky fruit pricks us: no value!
     ]

现在开始工作:

从一个初始平坦的数组 a1 开始:

有些人建议使用 #to_h(这在 Ruby 2.1.0 中出现,并且可以回溯到早期版本)。对于一个初始平坦的数组,这种方法不起作用:

a1.to_h   # => TypeError: wrong element type String at 0 (expected array)

使用Hash::[]splat操作符结合使用可以实现以下功能:

Hash[*a1] # => {"apple"=>1, "banana"=>2}

对于a1所代表的简单情况,这就是解决方案。

使用键/值对数组a2

对于[key,value]类型的数组,有两种方法可行。

首先,Hash::[]仍然适用(就像*a1一样):

Hash[a2] # => {"apple"=>1, "banana"=>2}

现在还可以使用#to_h

a2.to_h  # => {"apple"=>1, "banana"=>2}

因此,对于简单的嵌套数组情况,有两个简单的答案。

即使键或值是子数组,如a3,这仍然成立:

Hash[a3] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "pear"=>[4, 5]} 
a3.to_h  # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "pear"=>[4, 5]}

榴莲有刺(异常结构会带来问题):

如果我们得到的输入数据不平衡,使用 #to_h 会遇到问题:

a4.to_h  # => ArgumentError: wrong array length at 3 (expected 2, was 1)

但是Hash::[]仍然有效,只需将nil设置为durian的值(以及a4中任何其他仅包含1个值的数组元素):

Hash[a4] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "durian"=>nil}

使用新变量 a5a6 进行平铺

其他一些答案提到了 flatten,带或不带 1 参数,因此让我们创建一些新变量:

a5 = a4.flatten
# => ["apple", 1, "banana", 2,  "orange", "seedless" , 3, "durian"] 
a6 = a4.flatten(1)
# => ["apple", 1, "banana", 2, ["orange", "seedless"], 3, "durian"] 

我选择使用a4作为基础数据,是因为我们遇到了平衡问题,这在a4.to_h中显现出来。我想调用flatten可能是某些人尝试解决这个问题的一种方法,可能会像下面这样。

flatten没有参数(a5):

Hash[*a5]       # => {"apple"=>1, "banana"=>2, "orange"=>"seedless", 3=>"durian"}
# (This is the same as calling `Hash[*a4.flatten]`.)

乍一看,这似乎有效 - 但它使我们对无籽橙子产生了错误的印象,因此也将3作为durian作为

a1一样,这也行不通:

a5.to_h # => TypeError: wrong element type String at 0 (expected array)

所以a4.flatten对我们来说没有用,我们只需要使用Hash[a4]

flatten(1)的情况(a6):

但是部分展平呢?值得注意的是,在部分展平的数组(a6)上使用splat调用Hash::[]与调用Hash[a4]不同:

Hash[*a6] # => ArgumentError: odd number of arguments for Hash

预扁平化的数组,仍然嵌套(获取 a6 的另一种方法):

但是如果这是我们最初获得数组的方式呢?也就是说,与 a1 相比,它是我们的输入数据 - 只是这次有些数据可以是数组或其他对象。我们已经看到 Hash[*a6] 不起作用,但是如果我们仍然想要获得最后一个元素作为键值对中的 ,并且值为 nil 的行为怎么办?

在这种情况下,仍然有一种方法可以实现这一点,使用 Enumerable#each_slice 将自己回到作为外部数组中元素的键/值

a7 = a6.each_slice(2).to_a
# => [["apple", 1], ["banana", 2], [["orange", "seedless"], 3], ["durian"]] 

请注意,这将使我们得到一个新的数组,它与a4不是“相同的”,但确实具有相同的值
a4.equal?(a7) # => false
a4 == a7      # => true

因此,我们可以再次使用 Hash::[]

Hash[a7] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "durian"=>nil}
# or Hash[a6.each_slice(2).to_a]

但是有个问题!

需要注意的是,each_slice(2) 的解决方案只能在最后一个键缺少值时使事情恢复正常。如果我们稍后添加了额外的键/值对:

a4_plus = a4.dup # just to have a new-but-related variable name
a4_plus.push(['lychee', 4])
# => [["apple",                1],
#     ["banana",               2],
#     [["orange", "seedless"], 3], # multi-value key
#     ["durian"],              # missing value
#     ["lychee", 4]]           # new well-formed item

a6_plus = a4_plus.flatten(1)
# => ["apple", 1, "banana", 2, ["orange", "seedless"], 3, "durian", "lychee", 4]

a7_plus = a6_plus.each_slice(2).to_a
# => [["apple",                1],
#     ["banana",               2],
#     [["orange", "seedless"], 3], # so far so good
#     ["durian",               "lychee"], # oops! key became value!
#     [4]]                     # and we still have a key without a value

a4_plus == a7_plus # => false, unlike a4 == a7

从这里得到的两个哈希值在重要方面有所不同:

ap Hash[a4_plus] # prints:
{
                     "apple" => 1,
                    "banana" => 2,
    [ "orange", "seedless" ] => 3,
                    "durian" => nil, # correct
                    "lychee" => 4    # correct
}

ap Hash[a7_plus] # prints:
{
                     "apple" => 1,
                    "banana" => 2,
    [ "orange", "seedless" ] => 3,
                    "durian" => "lychee", # incorrect
                           4 => nil       # incorrect
}

(注:我在这里使用{{link1:awesome_print}}的ap只是为了更容易地显示结构;这并没有概念上的要求。)
因此,对于不平衡的平面输入,each_slice解决方案仅在不平衡的部分位于末尾时才有效。

要点:

  1. 尽可能地将输入设置为[key, value]对(外部数组中每个项目的子数组)。
  2. 如果可以这样做,#to_hHash::[]都可以。
  3. 如果无法这样做,Hash::[]与展开符(*)结合使用将起作用,只要输入是平衡的
  4. 对于不平衡扁平化的输入数组,仅当value项是最后一个缺失的项时,它才能合理地工作。

旁注:我发布这个答案是因为我觉得有价值的内容需要被添加——一些现有的答案有错误的信息,而且没有一个(我读过的)给出像我在这里努力做的这样完整的答案。我希望这对你有帮助。尽管如此,我仍然感谢那些在我之前的人,其中几个人为这个答案的部分提供了灵感。


5
您也可以使用以下方式将二维数组转换为哈希表:
1.9.3p362 :005 > a= [[1,2],[3,4]]

 => [[1, 2], [3, 4]]

1.9.3p362 :006 > h = Hash[a]

 => {1=>2, 3=>4} 

3

追加到答案中,但使用匿名数组和注释:

Hash[*("a,b,c,d".split(',').zip([1,2,3,4]).flatten)]

从内部开始分析这个答案:

  • "a,b,c,d" 实际上是一个字符串。
  • 通过逗号拆分成数组。
  • 将其与以下数组一起压缩。
  • [1,2,3,4] 是一个实际的数组。

中间结果为:

[[a,1],[b,2],[c,3],[d,4]]

将其压缩,然后转换为:

["a",1,"b",2,"c",3,"d",4]

然后: *["a",1,"b",2,"c",3,"d",4] 将其展开为 "a",1,"b",2,"c",3,"d",4,我们可以将其用作 Hash[] 方法的参数:
Hash[*("a,b,c,d".split(',').zip([1,2,3,4]).flatten)]

这将产生:

{"a"=>1, "b"=>2, "c"=>3, "d"=>4}

这也可以不用展开符(*)和扁平化操作:Hash[("a,b,c,d".split(',').zip([1,2,3,4]))] => {"a"=>1, "b"=>2, "c"=>3, "d"=>4}。更多细节请查看我添加的答案。 - lindes

0
如果你有一个长这样的数组 -
data = [["foo",1,2,3,4],["bar",1,2],["foobar",1,"*",3,5,:foo]]

你想要每个数组的第一个元素成为哈希表的键,而剩下的元素成为值数组,那么你可以这样做 -
data_hash = Hash[data.map { |key| [key.shift, key] }]

#=>{"foo"=>[1, 2, 3, 4], "bar"=>[1, 2], "foobar"=>[1, "*", 3, 5, :foo]}

0

不确定这是否是最好的方法,但它是有效的:

a = ["apple", 1, "banana", 2]
m1 = {}
for x in (a.length / 2).times
  m1[a[x*2]] = a[x*2 + 1]
end

b = [["apple", 1], ["banana", 2]]
m2 = {}
for x,y in b
  m2[x] = y
end

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