Unique on an array of hashes based on value - ruby

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=>"green", :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

Related

To find out uniq values from an array of hashes based on an input hash

I have an array and a hash:
a = [
{ :color => "blue", :name => "wind" },
{ :color => "red", :name => "fire" },
{ :color => "white", :name => "wind" },
{ :color => "yellow", :name => "wind" },
{ :color => "green", :name => nil },
{ :color => "black", :name => "snow" }
]
b = { blue: 'blue', white: 'white', red: 'red', green: 'green', black: 'black' }
I need to find out unique names based on the input hash to get this:
['wind', 'fire', 'snow']
I've tried:
names = a.map { |i| [i[:color], i[:name]] }
.delete_if { |key, value| value.nil? }
resultant_names = []
b.values.each do |c|
if names[c]
resultant_names << names[c]
end
end
resultant_names.uniq
I need a better approach for this. This one has too many loops.
While your result does not make sense to me (e.g. it is missing snow) this will work
a.map(&:values).reverse.to_h.values_at(*b.values).compact.uniq
#=> ["wind","fire"]
To break it down:
a.map(&:values).reverse.to_h
#=> {"white"=>"wind", "green"=>nil, "yellow"=>"wind", "red"=>"fire", "blue"=>"wind"}
You'll notice snow is missing because when we reverse the list ["white","wind"] will overwrite ["white","snow"] when converted to a Hash
Then we just collect the values for the given colors from
b.values
#=> ["blue", "white", "red", "green"]
a.map(&:values).reverse.to_h.values_at(*b.values)
#=> ["wind", "wind", "fire", nil]
Then Array#compact will remove the nil elements and Array#uniq will make the list unique.
If snow was intended you could skip the reversal
a.map(&:values).to_h.values_at(*b.values).compact.uniq
#=> ["wind", "snow", "fire"]
Either way this is a strange data structure and these answers are only to help with the problem provided as the duplicate colors can cause differing results based on the order in a.
I believe you want 'snow' to be in your output array, as there is no other logical explanation. Your code would work if you were to add .to_h on the end of line 2, but as you note, it is not very clean or efficient. Also, by converting to a Hash, as a result of duplicate keys, you would potentially lose data.
Here's a tighter construct that avoids the data loss problem:
def unique_elements(a, b)
color_set = b.values.to_set
a.map { |pair| pair[:name] if color_set.include?(pair[:color]) }.compact.uniq
end
First we take the values of b and convert them to a set, so that we can efficiently determine if a given element is a member of the set.
Next we map over a choosing the names of those members of a for which the [:color] is included in our color set.
Finally we eliminate nils (using compact) and choose unique values.
>> unique_elements(a, b)
#> ["wind", "fire", "snow"]
I would begin by converting a to a more useful data structure.
h = a.each_with_object({}) { |g,h| h[g[:color]] = g[:name] }
#=> {"blue"=>"wind", "red"=>"fire", "white"=>"wind", "yellow"=>"wind",
# "green"=>nil, "black"=>"snow"}
We may then simply write
h.values_at(*b.values).compact.uniq
# => ["wind", "fire", "snow"]
This approach has several desireable characteristics:
the creation of h makes the method easier to read
debugging and testing is made easier by creating h as a separate step
h need only be created once even if several values of b are to be evaluated (in which case we may wish to make h an instance variable).
h could be chained to the second statement but I've chosen not to do so for the reasons given above (especially the last one).

Isolating and displaying a specific element within a hash

I am currently having trouble writing a test that addresses the eligibility_settings of a record I have. I am having trouble pulling out one of the specific elements from this hash.
Specifically I want to test that by making a change elsewhere in a different function that changes the min age of a specific player, and so what I am really trying to test is the eligibility_settings.min_age. But i'm having trouble within my test isolating that out.
My hash looks like this
{
:name => "player1",
:label => "redTeam_1_1",
:group => "adult",
:teamId => 7,
:eligibility_settings => {
"min_age" => 18,
"player_gender" => "female",
"union_players_only" => true
}
}
However when I try looping through this hash, I am having trouble isolating that one element.
i've tried something like
team.get_players.first.map do |settings, value|
value.tap do |x, y|
y[3]
end
end
However It seems like what i've been trying, and my approach has not been working quite right.
Would anyone have any idea what I could do with this?
Although #SergioTulentsev gave the proper response, in the future if you are going to be looping through hashes, below is one way to iterate through the keys and grab the value you want.
hash = {
:name => "player1",
:label => "redTeam_1_1",
:group => "adult",
:teamId => 7,
:eligibility_settings => {
"min_age" => 18,
"player_gender" => "female",
"union_players_only" => true
}
}
hash.map do |settings, value|
p hash[:eligibility_settings]['min_age'] if settings == :eligibility_settings
end # output 18

How to detect value in array of hashes

I have array of hashes:
#array = [{:id => "1", :status=>"R"},
{:id => "1", :status=>"R"},
{:id => "1", :status=>"B"},
{:id => "1", :status=>"R"}]
How to detect, does it contain in hashes with the value of status "B"? Like in simply array:
#array = ["R","R","B","R"]
puts "Contain B" if #array.include?("B")
Use any?:
#array.any? { |h| h[:status] == "B" }
Arrays (enumerables actually) have a detect method. It returns a nil if it doesn't detect anything, so you can use it like Andrew Marshall's any.
#array = [{:id => "1", :status=>"R"},
{:id => "1", :status=>"R"},
{:id => "1", :status=>"B"},
{:id => "1", :status=>"R"}]
puts "Has status B" if #array.detect{|h| h[:status] == 'B'}
Just to add to what steenslag said:
detect doesn't always return nil.
You can pass in a lambda to execute (call) if detect does not 'detect' (find) an item. In other words, you can tell detect what to do if it can't detect (find) something.
To add to your example:
not_found = lambda { "uh oh. couldn't detect anything!"}
# try to find something that isn't in the Enumerable object:
#array.detect(not_found) {|h| h[:status] == 'X'}
will return "uh oh. couldn't detect anything!"
This means that you don't have to write this kind of code:
if (result = #array.detect {|h| h[:status] == 'X'}).nil?
# show some error, do something here to handle it
# (this would be the behavior you'd put into your lambda)
else
# deal nicely with the result
end
That's one major difference between any? and detect -- you can't tell any? what to do if it doesn't find any items.
This is in the Enumerable class. ref: http://ruby-doc.org/core/classes/Enumerable.html#M003123

Reverse a hash in Ruby

How would I reverse the elements in the hash, keeping the same values and keys, but reversing their order in the hash.
Like so:
{ "4" => "happiness", "10" => "cool", "lala" => "54", "1" => "spider" }
And convert that to:
{ "1" => "spider", "lala" => "54", "10" => "cool", "4" => "happiness" }
Or, perhaps I could run a each loop backwards, starting from the last element in the hash, rather than the first?
You could convert the Hash to an Array, reverse that, and then convert it back to a Hash:
reversed_h = Hash[h.to_a.reverse]
Hash#to_a gives you an array of arrays, the inner arrays are simple [key,value] pairs, then you reverse that array using Array#reverse, and Hash[] converts the [key,value] pairs back into a Hash.
Ruby 2.1 adds an Array#to_h method so you can now say:
reversed_h = h.to_a.reverse.to_h
In Ruby 2.1+ you can combine reverse_each and to_h:
{foo: 1, bar: 2}.reverse_each.to_h
#=> {:bar=>2, :foo=>1}
In pure ruby, you can do it by hash.map(&:reverse).to_h or hash.reverse_each.to_h
In rails, you can do it by hash.invert
hash = { "4" => "happiness", "10" => "cool", "lala" => "54", "1" => "spider" }
reversed_hash = Hash[hash.to_a.reverse]
h = { "4" => "happiness", "10" => "cool", "lala" => "54", "1" => "spider" }
p Hash[h.reverse_each.map{|e| e}]
#=> {"1"=>"spider", "lala"=>"54", "10"=>"cool", "4"=>"happiness"}
But this leaves a bad taste (just like the other answers, which work fine just like this one). If you have to do this, it could be an indication that a Hash was not the best choice.
Alternatively, you can use reduce and merge to add the item to the front of a new hash:
hash = { "4" => "happiness", "10" => "cool", "lala" => "54", "1" => "spider" }
hash.reduce({}){ |memo, object| Hash[*object].merge(memo) }
but, that's crazy :D
reversed_h = Hash[h.to_a.collect(&:reverse)]
In Ruby 1.8.7, the order of elements in a hash is documented to be not under our control, so none of the above methods work. In Ruby 1.9.3, things work and are documented in the way that the other answers rely upon.
$ irb1.8
h = { "4" => "happiness", "10" => "cool", "lala" => "54", "1" => "spider" }
Hash[h.to_a().reverse()]
=> {"lala"=>"54", "1"=>"spider", "10"=>"cool", "4"=>"happiness"}
quit
$ irb1.9.1
h = { "4" => "happiness", "10" => "cool", "lala" => "54", "1" => "spider" }
Hash[h.to_a().reverse()]
=>{"1"=>"spider", "lala"=>"54", "10"=>"cool", "4"=>"happiness"}
The Ruby 1.8.7 way was ingrained so firmly for me that I misunderstood the question for quite some time. I thought it requested a way to Hash#invert: ie to transform the hash such that the range maps to the domain. That method discards duplicates. Luís Ramalho proffers a method that doesn't, but it's a bit clunky. This is a little shorter:
$ irb
def invertWithDuplicates(original)
inverse = Hash.new() { |hash, key| hash[key] = []; }
original.each_pair() { |key, value| inverse[value].push(key); }
return inverse
end
h = { "4" => "happiness", "10" => "cool", "lala" => "54", "1" => "cool" }
invertWithDuplicates(h)
=> {"happiness"=>["4"], "cool"=>["1", "10"], "54"=>["lala"]}
Sorry to drift away from the OP's intended topic, though I submit that this does fit the post's title "Reverse a hash in Ruby".
if need:
hash = {:a => :x, :b => :y, :c => :y, :d => :z}
to:
{:x => [:a], :y => [:b, c], :z => [:d] }
can:
h={};hash.to_a.each{|e|h[e[1]]||=[];h[e[1]]<<e[0]};h

Create hash using block (Ruby)

Can I create a Ruby Hash from a block?
Something like this (although this specifically isn't working):
foo = Hash.new do |f|
f[:apple] = "red"
f[:orange] = "orange"
f[:grape] = "purple"
end
In Ruby 1.9 (or with ActiveSupport loaded, e.g. in Rails), you can use Object#tap, e.g.:
foo = Hash.new.tap do |bar|
bar[:baz] = 'qux'
end
You can pass a block to Hash.new, but that serves to define default values:
foo = Hash.new { |hsh, key| hsh[key] = 'baz qux' }
foo[:bar] #=> 'baz qux'
For what it's worth, I am assuming that you have a larger purpose in mind with this block stuff. The syntax { :foo => 'bar', :baz => 'qux' } may be all you really need.
I cannot understand why
foo = {
:apple => "red",
:orange => "orange",
:grape => "purple"
}
is not working for you?
I wanted to post this as comment but i couldn't find the button, sorry
Passing a block to Hash.new specifies what happens when you ask for a non-existent key.
foo = Hash.new do |f|
f[:apple] = "red"
f[:orange] = "orange"
f[:grape] = "purple"
end
foo.inspect # => {}
foo[:nosuchvalue] # => "purple"
foo # => {:apple=>"red", :orange=>"orange", :grape=>"purple"}
As looking up a non-existent key will over-write any existing data for :apple, :orange and :grape, you don't want this to happen.
Here's the link to the Hash.new specification.
What's wrong with
foo = {
apple: 'red',
orange: 'orange',
grape: 'purple'
}
As others have mentioned, simple hash syntax may get you what you want.
# Standard hash
foo = {
:apple => "red",
:orange => "orange",
:grape => "purple"
}
But if you use the "tap" or Hash with a block method, you gain some extra flexibility if you need. What if we don't want to add an item to the apple location due to some condition? We can now do something like the following:
# Tap or Block way...
foo = {}.tap do |hsh|
hsh[:apple] = "red" if have_a_red_apple?
hsh[:orange] = "orange" if have_an_orange?
hsh[:grape] = "purple" if we_want_to_make_wine?
}

Resources