Is there a way to check if the hashes in an array contain similar key value pairs in ruby?
For example, I have
array = [ {name: 'robert', nationality: 'asian', age: 10},
{name: 'robert', nationality: 'asian', age: 5},
{name: 'sira', nationality: 'african', age: 15} ]
I want to get the result as
array = [ {name: 'robert', nationality: 'asian', age: 15},
{name: 'sira', nationality: 'african', age: 15} ]
since there are 2 Roberts with the same citizenship.
Any help would be much appreciated.
I tried Array.uniq! {|e| e[:name] && e[:nationality] }
but I want to add both numbers to two hashes which is 10 + 5
PS: An array can have n number of hashes.
source to share
array.each_with_object(Hash.new(0)) { |g,h| h[[g[:name], g[:nationality]]] += g[:age] }.
map { |(name, nationality),age| { name:name, nationality:nationality, age:age } }
[{ :name=>"robert", :nationality=>"asian", :age=>15 },
{ :name=>"sira", :nationality=>"african", :age=>15 }]
The two steps are as follows.
a = array.each_with_object(Hash.new(0)) { |g,h| h[[g[:name], g[:nationality]]] += g[:age] }
#=> { ["robert", "asian"]=>15, ["sira", "african"]=>15 }
This uses the class method Hash :: new to create a hash with a default null value (represented by a block variable h
). Once this hash-hen is obtained, simply construct the desired hash:
a.map { |(name, nationality),age| { name:name, nationality:nationality, age:age } }
source to share
I would start with something like this:
array = [
{ name: 'robert', nationality: 'asian', age: 10 },
{ name: 'robert', nationality: 'asian', age: 5 },
{ name: 'sira', nationality: 'african', age: 15 }
]
array.group_by { |e| e.values_at(:name, :nationality) }
.map { |_, vs| vs.first.merge(age: vs.sum { |v| v[:age] }) }
#=> [
# {
# :name => "robert",
# :nationality => "asian",
# :age => 15
# }, {
# :name => "sira",
# :nationality => "african",
# :age => 15
# }
# ]
source to share
Let's take a look at what you want to accomplish from there. You have a list of some objects and you want to combine some objects together if they have the same ethnicity and name. So, we have a key with which we will merge. Let it be in programming terms.
key = proc { |x| [x[:name], x[:nationality]] }
We have defined a procedure that takes a hash and returns a "key" value. If this procedure returns the same value (according to eql?
) for two hashes, then these two hashes must be combined. So what do we mean by "merge"? You want to add ages together, so write a merge function.
merge = proc { |x, y| x.dup.tap { |x1| x1[:age] += y[:age] } }
If we have two values x
, and y
such that key[x]
and key[y]
are the same, we want to combine them, making a copy x
, and adding to it age y
. This is exactly what this procedure does. Now that we have our building blocks, we can write an algorithm.
We want to create the array at the end, after the merge using the key routine we wrote. Fortunately, Ruby has a handy feature called each_with_object
that will do something very nice for us. The method each_with_object
will execute its own block for each element of the array, passing another argument in the given value. This will come in handy here.
result = array.each_with_object({}) do |x, hsh|
# ...
end.values
Since we are using keys and values ββfor the merge, the most efficient way to do this would be a hash. Hence, we are passing in an empty hash as an additional object, which we will modify to copy the merge results. After all, we don't care about the keys anymore, so we write .values
to get only the objects themselves. Now for the final snippets.
if hsh.include? key[x]
hsh[ key[x] ] = merge.call hsh[ key[x] ], x
else
hsh[ key[x] ] = x
end
Let it break. If the hash already includes key[x]
which is the key for the object x
we are looking at, we want to concatenate x
with the value that is currently in key[x]
. Here we add ages together. This approach only works if the function merge
is what mathematicians call a semigroup, which is a fancy way of saying that an operation is associative. You don't need to worry too much about this; addition is a very good example of a semigroup, so it works here.
In any case, if the key does not exist in the hash, we want to put the current value in the hash at the key position. The resulting hash from the merge is returned and then we can get the values ββfrom it to get the desired result.
key = proc { |x| [x[:name], x[:nationality]] }
merge = proc { |x, y| x.dup.tap { |x1| x1[:age] += y[:age] } }
result = array.each_with_object({}) do |x, hsh|
if hsh.include? key[x]
hsh[ key[x] ] = merge.call hsh[ key[x] ], x
else
hsh[ key[x] ] = x
end
end.values
Now my complexity theory is a bit rusty, but if Ruby uses its hash type efficiently (which I'm pretty sure of that) then this is a merge algorithm O(n)
, which means it will take a linear amount of time to complete given the size of the problem as input ...
source to share