Unique on an array of hashes based on value
I feel like this could be improved (a common feeling in ruby). I'm trying to uniq an array of hashes based on value. In this example, I want the colors of the elements. Moss and snow are impostors.
# remove unique array of hashes based on a hash value
a = [
{ :color => "blue", :name => "water" },
{ :color => "red", :name => "fire" },
{ :color => "white", :name => "wind" },
{ :color => "green", :name => "earth" },
{ :color => "green", :name => "moss" },
{ :color => "white", :name => "snow" }
]
# remove moss and snow
uniques = []
a.each_with_index do |r, i|
colors = uniques.collect {|e| e[:color]}
if !colors.include? r[:color]
uniques.push r
else
a[i] = nil
end
end
a.compact!
puts a
This will print
{:color=>"blue", :name=>"water"}
{:color=>"red", :name=>"fire"}
{:color=>"white", :name=>"wind"}
{:color=>"gre开发者_如何学运维en", :name=>"earth"}
Which is "correct" however I feel like this is excessive. My experience with .map .inject is limited and those advanced techniques elude me. If someone could re-factor this, it might help me understand another terse technique.
In Ruby 1.9, try the following
a.uniq! {|e| e[:color] }
I'd go with Array's reject
or select
methods:
require 'pp'
a = [
{ :color => "blue", :name => "water" },
{ :color => "red", :name => "fire" },
{ :color => "white", :name => "wind" },
{ :color => "green", :name => "earth" },
{ :color => "green", :name => "moss" },
{ :color => "white", :name => "snow" }
]
pp a.reject{ |h| %w[moss snow].include?( h[:name]) }
# >> [{:color=>"blue", :name=>"water"},
# >> {:color=>"red", :name=>"fire"},
# >> {:color=>"white", :name=>"wind"},
# >> {:color=>"green", :name=>"earth"}]
Alternately you can be positive about it and select
the ones you want to keep:
pp a.select{ |h| %w[water fire wind earth].include?( h[:name] ) }
# >> [{:color=>"blue", :name=>"water"},
# >> {:color=>"red", :name=>"fire"},
# >> {:color=>"white", :name=>"wind"},
# >> {:color=>"green", :name=>"earth"}]
You're not really dealing with hashes, it's an array that happens to contain hashes, so don't let them confuse you. Array methods like reject
and select
are core methods for filtering out unwanted, or keeping wanted, elements.
In your code sample, you're losing sight of what your objective is: You want the elements, rejecting "moss" and "snow", which are non-elements. Filter out the non-elements, and you're left with the correct/real elements in the hashes. From there you can extract the correct colors.
An additional problem to watch out for with using uniq
, is it is positional, in other words, it looks for the first unique value and rejects subsequent ones. This wasn't apparent in your code because your array was consistently the same order as you tested. If you shuffled the order though...:
2.times do
pp a.shuffle.uniq{ |h| h[:color] }
end
Pass #1...
# [{:color=>"red", :name=>"fire"},
# {:color=>"white", :name=>"wind"},
# {:color=>"green", :name=>"moss"},
# {:color=>"blue", :name=>"water"}]
Pass #2...
# [{:color=>"green", :name=>"earth"},
# {:color=>"blue", :name=>"water"},
# {:color=>"red", :name=>"fire"},
# {:color=>"white", :name=>"snow"}]
Suddenly we see that both "moss" and "snow" are sneaking into the results even though the colors are unique. Those are subtle gotcha's that you have to watch out for.
For anyone who might want an even shorter variant of the correct answer by Steve Wilhelm ,
BEWARE:
a.uniq!(&:color)
WILL NOT WORK for an array of hashes, just like
a[1].color
wouldn't work either.
For more information on the & operator, read this link, or the comments on this question which in turn have plenty of links to resources.
On the other hand, you could get the Symbol#to_proc method working using lambdas, as is explained here, though it could be just complicating things, and certainly would not be a shorter version of the correct answer. However, it is very interesting knowledge.
Thanks mukesh-kumar-gupta for the heads-up
精彩评论