¿Cómo obtengo los elementos únicos de una matriz de hashes en Ruby?
Pregunta
Tengo una serie de hashes, y quiero que los valores únicos salgan de ella. Llamar a Array.uniq
no me da lo que espero.
a = [{:a => 1},{:a => 2}, {:a => 1}]
a.uniq # => [{:a => 1}, {:a => 2}, {:a => 1}]
Donde esperaba:
[{:a => 1}, {:a => 2}]
Al buscar en la red, no encontré una solución con la que estuviera contento. La gente recomendó redefinir Hash.eql?
y Hash.hash
, ya que eso es lo que Array.uniq
está consultando.
Editar: Donde me encontré con esto en el mundo real, los hashes eran un poco más complejos. Fueron el resultado de JSON analizado que tenía varios campos, algunos de los cuales los valores también eran hashes. Tenía una serie de esos resultados que quería filtrar los valores únicos.
No me gusta la solución redefinir Hash.eql?
y Hash.hash
, porque tampoco tendría que redefinir Hash
a nivel mundial , o redefinirlo para cada entrada en mi matriz. Cambiar la definición de Hash
para cada entrada sería engorroso, especialmente porque puede haber hashes anidados dentro de cada entrada.
Cambiar Hash
globalmente tiene cierto potencial, especialmente si se hizo temporalmente. Me gustaría crear otra clase o función de ayuda que envolviera guardar las definiciones anteriores y restaurarlas, pero creo que esto agrega más complejidad de la que realmente se necesita.
Utilizar inyectar
parece ser una buena alternativa para redefinir Hash
.
Solución
Puedo obtener lo que quiero llamando a inject
a = [{:a => 1},{:a => 2}, {:a => 1}]
a.inject([]) { |result,h| result << h unless result.include?(h); result }
Esto devolverá:
[{:a=>1}, {:a=>2}]
Otros consejos
Ruby 1.8.7+ devolverá lo que esperaba:
[{:a=>1}, {:a=>2}, {:a=>1}].uniq
#=> [{:a=>1}, {:a=>2}]
He tenido una situación similar, pero los hashes tenían claves. Usé el método de clasificación.
Lo que quiero decir:
tienes una matriz:
[{:x=>1},{:x=>2},{:x=>3},{:x=>2},{:x=>1}]
lo ordena ( #sort_by {| t | t [: x]}
) y obtiene esto:
[{:x=>1}, {:x=>1}, {:x=>2}, {:x=>2}, {:x=>3}]
ahora una versión de respuesta un poco modificada por Aaaron Hinni:
your_array.inject([]) do |result,item|
result << item if !result.last||result.last[:x]!=item[:x]
result
end
También he intentado:
test.inject([]) {|r,h| r<<h unless r.find {|t| t[:x]==h[:x]}; r}.sort_by {|t| t[:x]}
pero es muy lento. Aquí está mi punto de referencia:
test=[]
1000.times {test<<{:x=>rand}}
Benchmark.bmbm do |bm|
bm.report("sorting: ") do
test.sort_by {|t| t[:x]}.inject([]) {|r,h| r<<h if !r.last||r.last[:x]!=h[:x]; r}
end
bm.report("inject: ") {test.inject([]) {|r,h| r<<h unless r.find {|t| t[:x]==h[:x]}; r}.sort_by {|t| t[:x]} }
end
resultados:
Rehearsal ---------------------------------------------
sorting: 0.010000 0.000000 0.010000 ( 0.005633)
inject: 0.470000 0.140000 0.610000 ( 0.621973)
------------------------------------ total: 0.620000sec
user system total real
sorting: 0.010000 0.000000 0.010000 ( 0.003839)
inject: 0.480000 0.130000 0.610000 ( 0.612438)
Suponiendo que sus hashes sean siempre pares clave-valor, esto funcionará:
a.map {|h| h.to_a[0]}.uniq.map {|k,v| {k => v}}
Hash.to_a crea una matriz de matrices de valor-clave, por lo que el primer mapa te muestra:
[[:a, 1], [:a, 2], [:a, 1]]
uniq en Arrays hace lo que quieres, dándote:
[[:a, 1], [:a, 2]]
y luego el segundo mapa los vuelve a unir como hashes nuevamente.
Puedes usar (probado en ruby ??1.9.3),
[{a: 1},{a: 2},{a:1}].uniq => [{a:1},{a: 2}]
[{a: 1,b: 2},{a: 2, b: 2},{a: 1, b: 3}].uniq_by {|v| v[:a]} => [{a: 1,b: 2},{a: 2, b: 2}]
La respuesta que das es similar a la que analizamos aquí . Anula los métodos hash
y eql?
en los hashes que aparecerán en la matriz, lo que hace que uniq
se comporte correctamente.
El método de canalización en matrices (disponible desde 1.8.6) realiza la unión de conjuntos (devolviendo una matriz), por lo que la siguiente es otra forma posible de obtener elementos únicos de cualquier matriz a
:
[] | a