¿Cuál es la mejor manera de convertir una matriz en un hash en Ruby?

123

En Ruby, dada una matriz en una de las siguientes formas ...

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

... cuál es la mejor manera de convertir esto en un hash en forma de ...

{apple => 1, banana => 2}
Nathan Fritz
fuente

Respuestas:

91

NOTA : Para una solución concisa y eficiente, consulte la respuesta de Marc-André Lafortune a continuación.

Esta respuesta se ofreció originalmente como una alternativa a los enfoques que usan aplanar, que fueron los más votados al momento de escribir. Debería haber aclarado que no tenía la intención de presentar este ejemplo como una mejor práctica o un enfoque eficiente. La respuesta original sigue.


¡Advertencia! ¡Las soluciones que usan flatten no conservarán las claves o valores de la matriz!

Sobre la base de la respuesta popular de @John Topley, intentemos:

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

Esto arroja un error:

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

El constructor esperaba una matriz de longitud par (por ejemplo, ['k1', 'v1,' k2 ',' v2 ']). Lo peor es que una matriz diferente que se aplanó a una longitud uniforme simplemente nos daría un Hash con valores incorrectos en silencio.

Si desea usar claves o valores de matriz, puede usar map :

h3 = Hash[a3.map {|key, value| [key, value]}]
puts "h3: #{h3.inspect}"

Esto conserva la clave de matriz:

h3: {["orange", "seedless"]=>3, "apple"=>1, "banana"=>2}
Estofado
fuente
15
Esto es lo mismo que Hash [a3], ya que a3 == a3.map {| k, v | [k, v]} es cierto, en realidad es el equivalente a a3.dup.
Cluster
2
En lugar de usar el mapa, ¿por qué no especificar la profundidad de aplanar? Por ejemplo: en h3 = Hash[*a3.flatten(1)]lugar de lo h3 = Hash[*a3.flatten]cual arrojaría un error.
Jeff McCune
3
Esta respuesta no es eficiente. También está desactualizado. Mira mi respuesta.
Marc-André Lafortune
1
Sí, creo que Marc-André to_hes mejor.
B Seven
1
@ Marc-André Lafortune gracias, he actualizado mi respuesta para dirigir a los usuarios a la suya.
Stew
145

Simplemente use Hash[*array_variable.flatten]

Por ejemplo:

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

El uso Array#flatten(1)limita la recursividad para que las Arrayclaves y los valores funcionen como se esperaba.

John Topley
fuente
44
¡Oh, la elocuencia! Es por eso que amo a Ruby
iGbanam
11
ADVERTENCIA: las respuestas que usan aplanar causarán problemas si desea claves o valores de matriz.
Stew
He publicado una solución alternativa a continuación que evitará problemas con las claves o valores de la matriz.
Stew
55
Es mejor no intentar hacer una solución general para esto. Si sus claves y valores están emparejados como en [[clave1, valor1], [clave2, valor2]], entonces simplemente páselo a Hash [] sin engordar. Hash [a2] == Hash [* a2.flatten]. Si la matriz ya está aplanada como en, [clave1, valor1, clave2, valor2], simplemente prefije la var con *, Hash [* a1]
Clúster
8
FWIW, si realmente quieres (más de una) versión única para todos, también puedes usarla Hash[*ary.flatten(1)], que conservará las claves y los valores de la matriz. Es lo recursivo lo flattenque los está destruyendo, lo cual es bastante fácil de evitar.
brymck
79

La mejor manera es usar Array#to_h:

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

Tenga en cuenta que to_htambién acepta un bloqueo:

[:apple, :banana].to_h { |fruit| [fruit, "I like #{fruit}s"] } 
  # => {apple: "I like apples", banana: "I like bananas"}

Nota : to_hacepta un bloque en Ruby 2.6.0+; para los primeros rubíes puedes usar mi backportsgema yrequire 'backports/2.6.0/enumerable/to_h'

to_h sin bloque se introdujo en Ruby 2.1.0.

Antes de Ruby 2.1, uno podría usar lo menos legible Hash[]:

array = [ [:apple,1],[:banana,2] ]
Hash[ array ]  #= > {:apple => 1, :banana => 2}

Finalmente, tenga cuidado con cualquier solución que use flatten, esto podría crear problemas con los valores que son matrices en sí.

Marc-André Lafortune
fuente
44
¡Gracias por la simplicidad del nuevo método .to_h!
Codificación adicto
3
Me gusta el to_hmétodo mejor que las respuestas anteriores porque expresa la intención de convertir después de operar en la matriz.
B Seven
1
@BSeven Ni Array#to_htampoco Enumerable#to_hestá en core ruby ​​1.9.
Iron Savior
¿Qué sucede si tengo una matriz como [[apple, 1], [banana, 2], [apple, 3], [banana, 4]]y quiero la salida como {"apple" =>[1,3], "banana"=>[2,4]}?
nishant
@NishantKumar esa es una pregunta diferente.
Marc-André Lafortune
9

Editar: Vi las respuestas publicadas mientras escribía, Hash [a.flatten] parece el camino a seguir. Debí haber perdido ese bit en la documentación cuando estaba pensando en la respuesta. Pensé que las soluciones que he escrito pueden usarse como alternativas si es necesario.

La segunda forma es más simple:

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

a = matriz, h = hash, r = hash de valor de retorno (en el que acumulamos), i = elemento en la matriz

La mejor manera en que puedo pensar en hacer la primera forma es algo como esto:

a = [:apple, 1, :banana, 2]
h = {}
a.each_slice(2) { |i| h[i.first] = i.last }
Daemin
fuente
2
+1 para el a.inject({})one-liner que permite asignaciones de valor más flexibles.
Chris Bloom
También es posible eliminar el h = {}del segundo ejemplo mediante el uso de inyectar, terminando cona.each_slice(2).inject({}) { |h,i| h[i.first] = i.last; h }
lindes
Podrías hacerloa.each_slice(2).to_h
Conor O'Brien
6

También puede simplemente convertir una matriz 2D en hash usando:

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

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

1.9.3p362 :006 > h = Hash[a]

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

Resumen y TL; DR:

Esta respuesta espera ser un resumen completo de la información de otras respuestas.

La versión muy corta, dada la información de la pregunta más un par de extras:

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

Discusión y detalles siguen.


Configuración: variables

Para mostrar los datos que usaremos por adelantado, crearé algunas variables para representar varias posibilidades para los datos. Encajan en las siguientes categorías:

Basado en lo que estaba directamente en la pregunta, como a1y a2:

(Nota: supongo que appley bananaestaban destinados a representar variables Como otros han hecho, voy a utilizar cuerdas de aquí en adelante por lo que la entrada y los resultados pueden igualar.).

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

Claves y / o valores de valores múltiples, como a3:

En algunas otras respuestas, se presentó otra posibilidad (que amplío aquí): las claves y / o valores pueden ser matrices por sí mismos:

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

Matriz desequilibrada, como a4:

En buena medida, pensé que agregaría uno para un caso en el que podríamos tener una entrada incompleta:

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

Ahora, para trabajar:

Comenzando con una matriz inicialmente plana a1:

Algunos han sugerido usar #to_h(que apareció en Ruby 2.1.0, y puede ser compatible con versiones anteriores). Para una matriz inicialmente plana, esto no funciona:

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

El uso Hash::[]combinado con el operador splat hace:

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

Entonces esa es la solución para el caso simple representado por a1.

Con una matriz de claves arrays valor / pair, a2:

Con una matriz de matrices de [key,value]tipos, hay dos formas de hacerlo.

Primero, Hash::[]todavía funciona (como lo hizo con *a1):

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

Y luego también #to_hfunciona ahora:

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

Entonces, dos respuestas fáciles para el caso de matriz anidada simple.

Esto sigue siendo cierto incluso con subconjuntos como claves o valores, como con 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]}

Pero los durianos tienen picos (las estructuras anómalas dan problemas):

Si hemos recibido datos de entrada que no están equilibrados, tendremos problemas con #to_h:

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

Pero Hash::[]aún funciona, solo estableciendo nilcomo el valor para durian(y cualquier otro elemento de matriz en a4 que sea solo una matriz de valor 1):

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

Aplanamiento: utilizando nuevas variables a5ya6

Algunas otras respuestas mencionadas flatten, con o sin 1argumento, así que creemos algunas variables nuevas:

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

Elegí usar a4como datos base debido al problema de equilibrio que tuvimos, que apareció a4.to_h. Me imagino llamandoflatten podría ser un enfoque que alguien podría usar para tratar de resolver eso, que podría ser el siguiente.

flattensin argumentos ( a5):

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

A simple vista, esto parece funcionar, pero nos puso en el camino equivocado con las naranjas sin semillas, por lo que también es 3una clave y durianun valor .

Y esto, como con a1, simplemente no funciona:

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

Así a4.flattenque no es útil para nosotros, solo queremos usarHash[a4]

El flatten(1)caso ( a6):

Pero, ¿qué pasa con el aplanamiento parcial? Vale la pena señalar que llamar Hash::[]usando splaten la matriz parcialmente aplanada ( a6) no es lo mismo que llamar Hash[a4]:

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

Matriz previamente aplanada, todavía anidada (forma alternativa de obtener a6):

Pero, ¿qué pasaría si así fue como obtuvimos la matriz en primer lugar? (Es decir, en comparación con a1nuestros datos de entrada, solo que esta vez algunos de los datos pueden ser matrices u otros objetos). Hemos visto que eso Hash[*a6]no funciona, pero ¿y si todavía quisiéramos obtener el comportamiento donde ¿El último elemento (importante! ver más abajo) actuó como clave para un nilvalor?

En tal situación, todavía hay una manera de hacer esto, utilizando Enumerable#each_slicepara volver a los pares clave / valor como elementos en la matriz externa:

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

Tenga en cuenta que esto termina obteniendo una nueva matriz que no es " idéntica " a4, pero tiene los mismos valores :

a4.equal?(a7) # => false
a4 == a7      # => true

Y así podemos usar nuevamente Hash::[]:

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

Pero hay un problema!

Es importante tener en cuenta que la each_slice(2)solución solo devuelve las cosas a la cordura si la última clave fue la que falta un valor. Si luego agregamos un par clave / valor adicional:

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

Y los dos hashes que obtendríamos de esto son diferentes en formas importantes:

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
}

(Nota: estoy usando awesome_print's ap. Sólo para que sea más fácil para mostrar la estructura aquí, no hay requisito conceptual para esto)

Entonces, la each_slicesolución para una entrada plana no balanceada solo funciona si el bit no balanceado está al final.


Para llevar:

  1. Siempre que sea posible, configure la entrada a estas cosas como [key, value]pares (una submatriz para cada elemento en la matriz externa).
  2. Cuando de hecho puedas hacer eso, uno #to_ho Hash::[]ambos funcionarán.
  3. Si no puede hacerlo, Hash::[]combinado con el splat ( *) funcionará, siempre que las entradas estén equilibradas .
  4. Con una matriz desequilibrada y plana como entrada, la única forma en que esto funcionará razonablemente es si el último value elemento es el único que falta.

Nota al margen: estoy publicando esta respuesta porque siento que hay valor para agregar: algunas de las respuestas existentes tienen información incorrecta, y ninguna (que leí) dio una respuesta tan completa como me estoy esforzando por hacer aquí. Espero que sea útil. Sin embargo, doy gracias a los que vinieron antes que yo, varios de los cuales inspiraron algunas partes de esta respuesta.

lindes
fuente
3

Anexando la respuesta pero usando matrices anónimas y anotaciones:

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

Tomando esa respuesta aparte, comenzando desde adentro:

  • "a,b,c,d" es en realidad una cadena
  • split en comas en una matriz.
  • zip eso junto con la siguiente matriz.
  • [1,2,3,4] es una matriz real

El resultado intermedio es:

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

aplanar luego transforma eso a:

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

y entonces:

*["a",1,"b",2,"c",3,"d",4] desenrolla eso en "a",1,"b",2,"c",3,"d",4

que podemos usar como argumentos del Hash[]método:

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

cuyos rendimientos:

{"a"=>1, "b"=>2, "c"=>3, "d"=>4}
StevenJenkins
fuente
Esto también funciona sin el splat ( *) y flatten: Hash[("a,b,c,d".split(',').zip([1,2,3,4]))]=> {"a"=>1, "b"=>2, "c"=>3, "d"=>4}. Más detalles en una respuesta que agregué.
lindes
0

si tiene una matriz que se ve así:

data = [["foo",1,2,3,4],["bar",1,2],["foobar",1,"*",3,5,:foo]]

y desea que los primeros elementos de cada matriz se conviertan en las claves para el hash y el resto de los elementos se conviertan en matrices de valores, entonces puede hacer algo como esto:

data_hash = Hash[data.map { |key| [key.shift, key] }]

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

No estoy seguro de si es la mejor manera, pero esto funciona:

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
Anders Sandvig
fuente
-1

Si los valores numéricos son índices seq, entonces podríamos tener formas más simples ... Aquí está mi envío de código, My Ruby está un poco oxidado

   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}
Gishu
fuente