nrave
nrave

Reputation: 41

Add hash in array if hash with this value doesn't exist, otherwise expand existing one

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

Answers (3)

Cary Swoveland
Cary Swoveland

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

Yakov
Yakov

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

Sebasti&#225;n Palma
Sebasti&#225;n Palma

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

Related Questions