How do I get the unique elements from an array of hashes in Ruby?

25,787

Solution 1

I can get what I want by calling inject

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

This will return:

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

Solution 2

Ruby 1.8.7+ will return just what you have expected:

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

Solution 3

I've had a similar situation, but hashes had keys. I used sorting method.

What I mean:

you have an array:

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

you sort it (#sort_by {|t| t[:x]}) and get this:

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

now a bit modified version of answer by Aaaron Hinni:

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

I've also tried:

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

but it's very slow. here is my benchmark:

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

results:

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)

Solution 4

You can use (tested in 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}]

Solution 5

Assuming your hashes are always single key-value pairs, this will work:

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

Hash.to_a creates an array of key-value arrays, so the first map gets you:

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

uniq on Arrays does what you want, giving you:

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

and then the second map puts them back together as hashes again.

Share:
25,787

Related videos on Youtube

Aaron Hinni
Author by

Aaron Hinni

Agile software craftsman in St. Louis, MO currently specializing in full stack typescript/javascript.

Updated on July 09, 2022

Comments

  • Aaron Hinni
    Aaron Hinni almost 2 years

    I have an array of hashes, and I want the unique values out of it. Calling Array.uniq doesn't give me what I expect.

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

    Where I expected:

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

    In searching around on the net, I didn't come up with a solution that I was happy with. Folks recommended redefining Hash.eql? and Hash.hash, since that is what Array.uniq is querying.

    Edit: Where I ran into this in the real world, the hashes were slightly more complex. They were the result of parsed JSON that had multiple fields, some of which the values were hashes as well. I had an array of those results that I wanted to filter out the unique values.

    I don't like the redefine Hash.eql? and Hash.hash solution, because I would either have to redefine Hash globally, or redefine it for each entry in my array. Changing the definition of Hash for each entry would be cumbersome, especially since there may be nested hashes inside of each entry.

    Changing Hash globally has some potential, especially if it were done temporarily. I'd want to build another class or helper function that wrapped saving off the old definitions, and restoring them, but I think this adds more complexity than is really needed.

    Using inject seems like a good alternative to redefining Hash.

  • Aaron Hinni
    Aaron Hinni over 15 years
    That is one of the solutions I found on the net. I didn't like that I needed to redefine hash, just to call uniq.
  • Mark Reid
    Mark Reid over 15 years
    If the vanilla Hash and Array classes don't do what you need you should really consider defining your own classes that implement the required behaviour. Can you describe what it is you are trying to model with arrays of hashes?
  • edthix
    edthix over 15 years
    much more better I think than the one link I posted above
  • yoniLavi
    yoniLavi about 10 years
    @SunnyRGupta, what version of Ruby are you using?
  • Sunny R Gupta
    Sunny R Gupta about 10 years
    ruby 1.9.3p448 (2013-06-27 revision 41675) [x86_64-darwin13.2.0]
  • yoniLavi
    yoniLavi about 10 years
    @SunnyRGupta, I don't know what to say. I just tested again, and it works perfectly. $ irb 1.9.3p448 :001 > a = [{:a => 1},{:a => 2}, {:a => 1}] => [{:a=>1}, {:a=>2}, {:a=>1}] 1.9.3p448 :002 > [] | a => [{:a=>1}, {:a=>2}]