Как получить уникальные элементы из массива хэшей в Ruby?

У меня есть массив хэшей, и я хочу получить уникальные значения. Вызов Array.uniq не дает мне того, что я ожидаю.

a = [{:a => 1},{:a => 2}, {:a => 1}]
a.uniq # => [{:a => 1}, {:a => 2}, {:a => 1}]

Где я ожидал:

[{:a => 1}, {:a => 2}]

При поиске в сети я не придумал решение, которым я был доволен. Людям рекомендуется переопределять Hash.eql? и Hash.hash, так как это запрос Array.uniq.

Изменить: Где я столкнулся с этим в реальном мире, хеши были немного сложнее. Они были результатом разобранного JSON, который имел несколько полей, некоторые из которых были также значениями хэшей. У меня был массив этих результатов, которые я хотел отфильтровать уникальные значения.

Мне не нравится решение redefine Hash.eql? и Hash.hash, потому что мне нужно либо переопределить Hash глобально, либо переопределить его для каждой записи в моем массиве. Изменение определения Hash для каждой записи было бы громоздким, тем более, что внутри каждой записи могут быть вложенные хэши.

Изменение Hash во всем мире имеет некоторый потенциал, особенно если это было сделано временно. Я хотел бы создать еще один класс или вспомогательную функцию, которая завершает сохранение старых определений и восстанавливает их, но я думаю, что это добавляет больше сложности, чем это действительно необходимо.

Использование inject кажется хорошей альтернативой переопределению Hash.

Ответ 1

Я могу получить то, что хочу, вызывая inject

a = [{:a => 1},{:a => 2}, {:a => 1}]
a.inject([]) { |result,h| result << h unless result.include?(h); result }

Это вернет:

[{:a=>1}, {:a=>2}]

Ответ 2

Ruby 1.8.7+ вернет только то, что вы ожидали:

[{:a=>1}, {:a=>2}, {:a=>1}].uniq
#=> [{:a=>1}, {:a=>2}] 

Ответ 3

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

Что я имею в виду:

у вас есть массив:

[{:x=>1},{:x=>2},{:x=>3},{:x=>2},{:x=>1}]

вы его сортируете (#sort_by {|t| t[:x]}) и получите следующее:

[{:x=>1}, {:x=>1}, {:x=>2}, {:x=>2}, {:x=>3}]

теперь немного измененная версия ответа Аарона Хинни:

your_array.inject([]) do |result,item| 
  result << item if !result.last||result.last[:x]!=item[:x]
  result
end

Я также пробовал:

test.inject([]) {|r,h| r<<h unless r.find {|t| t[:x]==h[:x]}; r}.sort_by {|t| t[:x]}

но он очень медленный. вот мой ориентир:

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

результаты:

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)

Ответ 4

Предполагая, что ваши хеши всегда представляют собой пары с одним ключом, это будет работать:

a.map {|h| h.to_a[0]}.uniq.map {|k,v| {k => v}}

Hash.to_a создает массив массивов с ключом, поэтому первая карта получает вас:

[[:a, 1], [:a, 2], [:a, 1]]

uniq on Arrays делает то, что вы хотите, давая вам:

[[:a, 1], [:a, 2]]

а затем вторая карта снова помещает их обратно как хэши.

Ответ 5

Вы можете использовать (проверено в 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}]

Ответ 6

Ответ, который вы даете, аналогичен приведенному ниже здесь. Он переопределяет методы hash и eql? для хэшей, которые должны появляться в массиве, а затем корректно ведет себя uniq.

Ответ 8

Метод pipe на массивах (доступный с версии 1.8.6) выполняет команду set union (возврат массива), поэтому следующий способ - получить уникальные элементы любого массива a:

[] | a