2

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?

nrave
  • 41
  • 5
  • What type have roles? Are they strings? – Yakov May 25 '20 at 11:29
  • @Yakov yes, roles are the strings – nrave May 25 '20 at 11:45
  • What is to be done if two hashes with the same value for `:id` have different values for `:name`? – Cary Swoveland May 25 '20 at 12:42
  • @CarySwoveland id - it's the real id of Project, so we can't have two projects with the same id but with different names. I have validations, so the id-name pair is only one. – nrave May 25 '20 at 12:48
  • I don't know how you are using those hashes but in general one does not include keys (here `:name`) whose values are determined by the value of another key (here `:id`). Consider removing the key `:name` and adding a hash `id_names = { 1=>'First', 5=>'Five' }`. You might even find it easier to use a single hash, with the keys the id, rather than an array of hashes: `{ 1=>{ name: 'First', roles: ['user', 'compliance_lead'], 5=>{ name: 'Five', roles: ['financial_lead'] } }`. That would speed lookups by key. Food for thought only. – Cary Swoveland May 25 '20 at 13:30

3 Answers3

2

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.

Yakov
  • 3,033
  • 1
  • 11
  • 22
  • I suggest using [`find`](https://ruby-doc.org/core-2.7.1/Enumerable.html#method-i-find) over [`find_index`](https://ruby-doc.org/core-2.7.1/Array.html#method-i-find_index) for this scenario. eg. `project = projects_with_roles.find { ... }` then `if project` followed by `project[:roles] |= new_project[:roles]` – 3limin4t0r May 25 '20 at 14:24
  • Sure, it will be better. Thanks – Yakov May 25 '20 at 14:30
2

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"]}]
Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
1
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).

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100