¿Cuál es la mejor manera de convertir una matriz en un hash en Ruby?
Pregunta
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}
Solución
NOTA:Para obtener una solución concisa y eficiente, consulte La respuesta de Marc-André Lafortune abajo.
Esta respuesta se ofreció originalmente como una alternativa a los enfoques que utilizan aplanar, que fueron los más votados al momento de escribir este artículo.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! Soluciones usando aplanar ¡No conservará las claves o valores de la matriz!
Basándonos en la popular respuesta 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 uniforme (p. ej.['k1','v1,'k2','v2']).Lo que es peor es que una matriz diferente que se aplanara a una longitud uniforme simplemente nos daría silenciosamente un Hash con valores incorrectos.
Si desea utilizar claves o valores de matriz, puede utilizar mapa:
h3 = Hash[a3.map {|key, value| [key, value]}]
puts "h3: #{h3.inspect}"
Esto conserva la clave Array:
h3: {["orange", "seedless"]=>3, "apple"=>1, "banana"=>2}
Otros consejos
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}"
Usando Array#flatten(1)
limita la recursividad así Array
Las claves y los valores funcionan como se esperaba.
La mejor manera es usar Array#to_h
:
[ [:apple,1],[:banana,2] ].to_h #=> {apple: 1, banana: 2}
Tenga en cuenta que to_h
también acepta un bloque:
[:apple, :banana].to_h { |fruit| [fruit, "I like #{fruit}s"] }
# => {apple: "I like apples", banana: "I like bananas"}
Nota: to_h
acepta un bloque en Ruby 2.6.0+;para rubíes tempranos puedes usar mi backports
gema y require 'backports/2.6.0/enumerable/to_h'
to_h
sin bloque se introdujo en Ruby 2.1.0.
Antes de Ruby 2.1, se podía utilizar el formato menos legible. Hash[]
:
array = [ [:apple,1],[:banana,2] ]
Hash[ array ] #= > {:apple => 1, :banana => 2}
Finalmente, tenga cuidado con cualquier solución que utilice flatten
, esto podría crear problemas con valores que son matrices en sí mismas.
Actualizar
Ruby 2.1.0 se lanza hoy.y yo vengo con Array#to_h
(Notas de lanzamiento y ruby-doc), que resuelve el problema de convertir un Array
a un Hash
.
Ejemplo de documentos Ruby:
[[:foo, :bar], [1, 2]].to_h # => {:foo => :bar, 1 => 2}
Editar:Vi las respuestas publicadas mientras escribía, Hash[a.flatten] parece el camino a seguir.Debo haber perdido esa parte de la documentación cuando estaba pensando en la respuesta.Pensé que las soluciones que he escrito se pueden utilizar como alternativas si es necesario.
La segunda forma es más sencilla:
a = [[:apple, 1], [:banana, 2]]
h = a.inject({}) { |r, i| r[i.first] = i.last; r }
a = matriz, h = hash, r = hash del valor de retorno (en el que acumulamos), i = elemento de la matriz
La forma más sencilla que se me ocurre de hacer la primera forma es algo como esto:
a = [:apple, 1, :banana, 2]
h = {}
a.each_slice(2) { |i| h[i.first] = i.last }
También puedes 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}
Agregando a la respuesta pero usando matrices anónimas y anotando:
Hash[*("a,b,c,d".split(',').zip([1,2,3,4]).flatten)]
Desmontando esa respuesta, comenzando desde adentro:
"a,b,c,d"
es en realidad una cadena.split
sobre 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 lo transforma a:
["a",1,"b",2,"c",3,"d",4]
y luego:
*["a",1,"b",2,"c",3,"d",4]
desenrolla eso en"a",1,"b",2,"c",3,"d",4
que podemos utilizar como argumentos para la 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}
Resumen y TL;DR:
Esta respuesta espera ser un resumen completo de la información de otras respuestas.
La versión muy corta, dados los datos 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
Siguen la discusión y los detalles.
Configuración:variables
Para mostrar los datos que usaremos desde el principio, 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 a1
y a2
:
(Nota:Supongo que apple
y banana
estaban destinados a representar variables.Como lo han hecho otros, usaré cadenas de aquí en adelante para que la entrada y los resultados coincidan).
a1 = [ 'apple', 1 , 'banana', 2 ] # flat input
a2 = [ ['apple', 1], ['banana', 2] ] # key/value paired input
Claves y/o valores multivalor, como a3
:
En algunas otras respuestas, se presentó otra posibilidad (que amplío aquí): las claves y/o los valores pueden ser matrices por sí solos:
a3 = [ [ 'apple', 1 ],
[ 'banana', 2 ],
[ ['orange','seedless'], 3 ],
[ 'pear', [4, 5] ],
]
Matriz desequilibrada, como a4
:
Por si acaso, pensé en agregar uno para el 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, a trabajar:
Comenzando con una matriz inicialmente plana, a1
:
Algunos han sugerido usar #to_h
(que apareció en Ruby 2.1.0 y puede ser respaldado a versiones anteriores).Para una matriz inicialmente plana, esto no funciona:
a1.to_h # => TypeError: wrong element type String at 0 (expected array)
Usando Hash::[]
combinado con el operador de salpicaduras hace:
Hash[*a1] # => {"apple"=>1, "banana"=>2}
Entonces esa es la solución para el caso simple representado por a1
.
Con una matriz de matrices de pares clave/valor, a2
:
Con una variedad de [key,value]
escriba matrices, hay dos maneras de hacerlo.
Primero, Hash::[]
todavía funciona (como lo hizo con *a1
):
Hash[a2] # => {"apple"=>1, "banana"=>2}
Y luego también #to_h
funciona ahora:
a2.to_h # => {"apple"=>1, "banana"=>2}
Entonces, dos respuestas fáciles para el caso de una matriz anidada simple.
Esto sigue siendo cierto incluso con submatrices 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 púas (las estructuras anómalas dan problemas):
Si recibimos datos de entrada que no están balanceados, tendremos problemas con #to_h
:
a4.to_h # => ArgumentError: wrong array length at 3 (expected 2, was 1)
Pero Hash::[]
todavía funciona, solo configurando nil
como el valor de durian
(y cualquier otro elemento de matriz en a4 que sea solo una matriz de 1 valor):
Hash[a4] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "durian"=>nil}
Aplanamiento: uso de nuevas variables a5
y a6
Algunas otras respuestas mencionadas flatten
, con o sin 1
argumento, 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 a4
como datos base debido al problema de equilibrio que tuvimos, que apareció con a4.to_h
.me imagino llamando flatten
podría ser un enfoque que alguien podría usar para intentar resolver eso, que podría verse como el siguiente.
flatten
sin argumentos (a5
):
Hash[*a5] # => {"apple"=>1, "banana"=>2, "orange"=>"seedless", 3=>"durian"}
# (This is the same as calling `Hash[*a4.flatten]`.)
A primera vista, esto parece funcionar, pero nos hizo empezar con el pie izquierdo con las naranjas sin semillas, lo que también nos hizo 3
a llave y durian
a valor.
Y esto, como ocurre con a1
, simplemente no funciona:
a5.to_h # => TypeError: wrong element type String at 0 (expected array)
Entonces a4.flatten
no es útil para nosotros, solo queremos usar Hash[a4]
El flatten(1)
caso (a6
):
Pero ¿qué pasa con el aplanamiento sólo parcial?Vale la pena señalar que llamar Hash::[]
usando splat
en la matriz parcialmente aplanada (a6
) es no lo mismo que llamar Hash[a4]
:
Hash[*a6] # => ArgumentError: odd number of arguments for Hash
Matriz preaplanada, todavía anidada (forma alternativa de obtener a6
):
Pero, ¿y si así fuera como obtuvimos la matriz en primer lugar?(Es decir, en comparación con a1
, fueron nuestros datos de entrada; solo que esta vez algunos de los datos pueden ser matrices u otros objetos). Hemos visto que Hash[*a6]
no funciona, pero ¿qué pasaría si todavía quisiéramos obtener el comportamiento donde último elemento (¡importante!ver más abajo) actuó como clave para un nil
¿valor?
En tal situación, todavía hay una manera de hacer esto, usando Enumerable#each_slice
para volver a la clave/valor pares como elementos en la matriz exterior:
a7 = a6.each_slice(2).to_a
# => [["apple", 1], ["banana", 2], [["orange", "seedless"], 3], ["durian"]]
Tenga en cuenta que esto termina proporcionándonos una nueva matriz que no es "idéntico" a a4
, pero tiene la mismos valores:
a4.equal?(a7) # => false
a4 == a7 # => true
Y así podemos volver a utilizar 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 señalar que el each_slice(2)
La solución sólo hace que las cosas vuelvan a la normalidad si último La clave era la que faltaba 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 aspectos 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 mostrar la estructura aquí;no hay ningún requisito conceptual para esto.)
Entonces el each_slice
La solución para una entrada plana desequilibrada solo funciona si el bit desequilibrado está al final.
Comidas para llevar:
- Siempre que sea posible, establezca entradas para estas cosas como
[key, value]
pares (una submatriz para cada elemento de la matriz exterior). - Cuando realmente puedas hacer eso, tampoco
#to_h
oHash::[]
Ambos funcionarán. - Si no puedes,
Hash::[]
combinado con el splat (*
) trabajará, siempre que las entradas estén equilibradas. - Con un desequilibrado y departamento matriz como entrada, la única forma en que esto funcionará razonablemente es si el último
value
El artículo es el único que falta.
Nota al margen:Estoy publicando esta respuesta porque siento que hay valor que agregar: algunas de las respuestas existentes tienen información incorrecta y ninguna (que leí) dio una respuesta tan completa como me esfuerzo por dar aquí.Espero que sea de ayuda.Sin embargo, doy gracias a quienes me precedieron, varios de los cuales me inspiraron para partes de esta respuesta.
si tiene una matriz que se parece a esta:
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 que 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]}
No estoy seguro 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
Si los valores numéricos son índices secuenciales, entonces podríamos tener formas más simples...Aquí está el envío de mi código. Mi 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}