Reputation: 41
I want to create an array with roles for projects. I have an array of hashes, like that:
projects_with_roles =
[
{ id: 1, name: 'First', roles: ['user', 'compliance_lead'] },
{ id: 5, name: 'Five', roles: ['financial_lead'] }
]
I want to add role to roles array, if hash with project id already exist, otherwise - add new hash.
projects_with_roles << { id: 5, name: 'Five', roles: ['technical_lead'] }
projects_with_roles << { id: 10, name: 'Ten', roles: ['user'] }
projects_with_roles =
[
{ id: 1, name: 'First', roles: ['user', 'compliance_lead'] },
{ id: 5, name: 'Five', roles: ['financial_lead', 'technical_lead'] },
{ id: 10, name: 'Ten', roles: ['user'] }
]
How I can do that?
Upvotes: 2
Views: 1688
Reputation: 110685
projects_with_roles = [
{ id: 1, name: 'First', roles: ['user', 'compliance_lead'] },
{ id: 5, name: 'Five', roles: ['financial_lead'] }
]
projects_to_add = [
{ id: 5, name: 'Five', roles: ['technical_lead'] },
{ id: 10, name: 'Ten', roles: ['user'] }
]
(projects_with_roles + projects_to_add).each_with_object({}) do |g,h|
h.update([g[:id], g[:name]]=>g[:roles]) { |_,o,n| o|n }
end.map { |(id,name),roles| { id: id, name: name, roles:roles } }
#=> [{:id=>1, :name=>"First", :roles=>["user", "compliance_lead"]},
# {:id=>5, :name=>"Five", :roles=>["financial_lead", "technical_lead"]},
# {:id=>10, :name=>"Ten", :roles=>["user"]}]
This does not mutate projects_with_roles
. If that is desired set projects_with_roles
equal to the above calculation.
This uses the form of Hash#update (a.k.a. merge!
) which employs the block { |_,o,n| o|n }
to determine the values of keys that are present in both hashes being merged. See the doc for an explanation of the values of the block's three block variables (_
, o
and n
). (I've represented the first, the common key, with an underscore to signal that it is not used in the block calculation.
Note that the intermediate calculation is as follows:
(projects_with_roles + projects_to_add).each_with_object({}) do |g,h|
h.update([g[:id], g[:name]]=>g[:roles]) { |_,o,n| o|n }
end
#=> {[1, "First"]=>["user", "compliance_lead"],
# [5, "Five"]=>["financial_lead", "technical_lead"],
# [10, "Ten"]=>["user"]}
By building a hash and then converting it to an array of hashes the computational complexity is kept to nearly O(projects_with_roles.size + projects_to_add.size)
as hash key lookups are close to O(1)
.
Upvotes: 1
Reputation: 3201
You need to find the item with the same id
and change the roles list or add the new item. Here is the simplified solution:
projects_with_roles = [
{ id: 1, name: 'First', roles: ['user'] },
{ id: 5, name: 'Five', roles: ['financial_lead', 'technical_lead'] },
]
new_project = { id: 5, name: 'Five', roles: ['user'] }
project = projects_with_roles.find { |project| project[:id] == new_project[:id] }
if project
project[:roles] |= new_project[:roles]
else
projects_with_roles << new_project
end
This operator |=
adds a new value to an array only if the value is not present in the array. It allows us to get away from adding duplications to the roles list.
Upvotes: 2
Reputation: 33420
This is a common hash reduce scenario. What you can do is to concat (sum) both arrays and group them by their id, after that you can map the result and reduce the hash values, merging them and making a single array from their roles:
projects_with_roles = [{ id: 1, name: 'First', roles: ['user', 'compliance_lead'] }, { id: 5, name: 'Five', roles: ['financial_lead'] }]
roles = [{ id: 5, name: 'Five', roles: ['technical_lead'] }, { id: 10, name: 'Ten', roles: ['user'] }]
(projects_with_roles + roles)
.group_by { |e| e[:id] }
.map do |_, val|
val.reduce({}) do |x, y|
x.merge(y) do |key, oldval, newval|
key == :roles ? oldval + newval : oldval
end
end
end
# [{:id=>1, :name=>"First", :roles=>["user", "compliance_lead"]},
# {:id=>5, :name=>"Five", :roles=>["financial_lead", "technical_lead"]},
# {:id=>10, :name=>"Ten", :roles=>["user"]}]
Upvotes: 2