Как лучше всего преобразовать массив в хеш в Ruby


123

В Ruby задан массив в одной из следующих форм ...

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

... как лучше всего преобразовать это в хеш в форме ...

{apple => 1, banana => 2}

Ответы:


91

ПРИМЕЧАНИЕ . Краткое и эффективное решение см. В ответе Марка-Андре Лафортуна ниже.

Этот ответ изначально предлагался в качестве альтернативы подходам с использованием 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 ']). Что еще хуже, другой массив, который сглаживается до одинаковой длины, просто молча дает нам хэш с неправильными значениями.

Если вы хотите использовать ключи или значения массива, вы можете использовать карту :

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]} верно, это фактически эквивалент a3.dup.
Cluster

2
Почему бы вместо использования карты просто не указать глубину выравнивания? Например: h3 = Hash[*a3.flatten(1)]вместо этого h3 = Hash[*a3.flatten]выдает ошибку.
Jeff McCune

3
Этот ответ неэффективен. Это тоже устарело. Смотрите мой ответ.
Marc-André Lafortune

1
Да, думаю, у Марка-Андре to_hлучше.
B Seven

1
@ Marc-André Lafortune, спасибо, я обновил свой ответ, чтобы направлять пользователей на ваш.
Stew

145

Просто используйте 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
О, красноречие! Вот почему я люблю Руби
iGbanam

11
ВНИМАНИЕ: ответы с использованием flatten вызовут проблемы, если вам нужны ключи или значения массива.
Stew

Ниже я опубликовал альтернативное решение, которое позволит избежать проблем с ключами или значениями массива.
Stew

5
Лучше не пытаться найти универсальное решение для этого. Если ваши ключи и значения объединены в пары, как в [[ключ1, значение1], [ключ2, значение2]], просто передайте его в Hash [] без увеличения веса. Хеш [a2] == Хеш [* a2.flatten]. Если массив уже сплющен, как в, [key1, value1, key2, value2], тогда просто префикс var с *, Hash [* a1]
Cluster

8
FWIW, если вам действительно нужна (больше) универсальная версия, вы также можете использовать Hash[*ary.flatten(1)], которая сохранит ключи и значения массива. Их flattenразрушает рекурсивный метод , которого достаточно легко избежать.
brymck

81

Лучше всего использовать 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"}

Примечание : to_hпринимает блок в Ruby 2.6.0+; для ранних рубинов вы можете использовать мой backportsдрагоценный камень и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!
кодирование пристрастилось

3
Мне этот to_hметод нравится больше, чем приведенные выше ответы, потому что он выражает намерение преобразования после работы с массивом.
B Seven

1
@BSeven В ядре Ruby 1.9 нет ни того, Array#to_hни другого Enumerable#to_h.
Iron Savior

Что делать, если у меня есть массив как [[apple, 1], [banana, 2], [apple, 3], [banana, 4]]и я хочу получить результат как {"apple" =>[1,3], "banana"=>[2,4]}?
nishant

@NishantKumar, это другой вопрос.
Marc-André Lafortune


9

Изменить: видел ответы, опубликованные, когда я писал, 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
+1 для a.inject({})однострочника, который позволяет более гибко присваивать значения.
Крис Блум

Также возможно h = {}a.each_slice(2).inject({}) { |h,i| h[i.first] = i.last; h }
исключить

Вы могли бы сделатьa.each_slice(2).to_h
Конор О'Брайен

6

Вы также можете просто преобразовать 2D-массив в хэш, используя:

1.9.3p362 :005 > a= [[1,2],[3,4]]

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

1.9.3p362 :006 > h = Hash[a]

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

4

Резюме и TL; DR:

Этот ответ надеется быть исчерпывающим обобщением информации из других ответов.

Очень короткая версия, учитывая данные из вопроса и пару дополнений:

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

Обсуждение и подробности следуют далее.


Настройка: переменные

Чтобы показать данные, которые мы будем использовать заранее, я создам несколько переменных, представляющих различные возможности для данных. Они попадают в следующие категории:

Основываясь на том , что непосредственно в вопросе, как a1и a2:

(Примечание: я предполагаю, что appleи bananaбыли предназначены для представления переменных. Как и другие, я буду использовать строки с этого момента, чтобы ввод и результаты могли совпадать.)

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}

Сглаживание - использование новых переменных a5иa6

Упомянутые несколько других ответов 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):

Но как насчет частичного сглаживания? Стоит отметить, что вызов Hash::[]using splatв частично сглаженном массиве ( a6) - это не то же самое, что вызов 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
}

(Примечание: я использую awesome_print's apтолько для того, чтобы упростить отображение структуры здесь; для этого нет никаких концептуальных требований.)

Таким образом, each_sliceрешение проблемы небалансного плоского входа работает только в том случае, если несимметричный бит находится в самом конце.


Take-сувенирной продукции:

  1. По возможности настраивайте ввод этих [key, value]элементов в виде пар (подмассив для каждого элемента внешнего массива).
  2. Когда вы действительно можете это сделать, один #to_hили Hash::[]оба будут работать.
  3. Если вы не можете этого сделать, Hash::[]комбинация со splat ( *) будет работать, пока входы сбалансированы .
  4. При несбалансированном и плоском массиве на входе единственный способ, которым это будет работать разумно, - это если последний value элемент будет единственным, который отсутствует.

Боковое примечание: я публикую этот ответ, потому что чувствую, что есть ценность, которую нужно добавить - некоторые из существующих ответов содержат неверную информацию, и ни один (который я прочитал) не дал столь полного ответа, как я пытаюсь сделать здесь. Я надеюсь, что это поможет. Тем не менее, я благодарю тех, кто был до меня, некоторые из которых вдохновили меня на части этого ответа.


3

Добавляем к ответу, но используя анонимные массивы и аннотируя:

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

Разбирая этот ответ, начиная с внутренней стороны:

  • "a,b,c,d" на самом деле строка.
  • split по запятым в массив.
  • zip это вместе со следующим массивом.
  • [1,2,3,4] это фактический массив.

Промежуточный результат:

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

Flatten затем преобразует это в:

["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}

Это также работает без splat ( *) и flatten: 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

-1

Если числовые значения являются индексами seq, тогда у нас могут быть более простые способы ... Вот мой код, My Ruby немного ржавый

   input = ["cat", 1, "dog", 2, "wombat", 3]
   hash = Hash.new
   input.each_with_index {|item, index|
     if (index%2 == 0) hash[item] = input[index+1]
   }
   hash   #=> {"cat"=>1, "wombat"=>3, "dog"=>2}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.