Hasmukh Rathod
Hasmukh Rathod

Reputation: 1118

Ruby Array of hashes: Better way to update deeply hash

I have an array consisting of hashes in the following form:

array = [
          {"id": 1, "items": [{"item_code": 1, "qty": 2},{"item_code": 2, "qty": 2}]},
          {"id": 2, "items": [{"item_code": 3, "qty": 2},{"item_code": 4, "qty": 2}]},
          {"id": 1, "items": [{"item_code": 5, "qty": 2},{"item_code": 6, "qty": 2}]},
          {"id": 2, "items": [{"item_code": 7, "qty": 2},{"item_code": 8, "qty": 2}]}
        ]

Do we have any convenient way to merge items according to ids:

Expected output:

array = [
          {"id": 1, "items": [
                               {"item_code": 1, "qty": 2},
                               {"item_code": 2, "qty": 2},
                               {"item_code": 5, "qty": 2},
                               {"item_code": 6, "qty": 2}
                             ]
          },
          {"id": 2, "items": [
                               {"item_code": 3, "qty": 2},
                               {"item_code": 4, "qty": 2},
                               {"item_code": 7, "qty": 2},
                               {"item_code": 8, "qty": 2}
                             ]
          }
       ]

I tried with group_by, which doesn't satisfy the output format.

Upvotes: 0

Views: 110

Answers (5)

Cary Swoveland
Cary Swoveland

Reputation: 110675

array.each_with_object({}) { |g,h| h.update(g[:id]=>g[:items]) { |_,o,n| o+n } }.
      map { |k,v| { id: k, items: v } }
  #=> [{:id=>1, :items=>[{:item_code=>1, :qty=>2}, {:item_code=>2, :qty=>2},
  #                      {:item_code=>5, :qty=>2}, {:item_code=>6, :qty=>2}]},
  #    {:id=>2, :items=>[{:item_code=>3, :qty=>2}, {:item_code=>4, :qty=>2},
  #                      {:item_code=>7, :qty=>2}, {:item_code=>8, :qty=>2}]}]

The first step is:

array.each_with_object({}) { |g,h| h.update(g[:id]=>g[:items]) { |_,o,n| o+n } }
  #=> {1=>[{:item_code=>1, :qty=>2}, {:item_code=>2, :qty=>2},
  #        {:item_code=>5, :qty=>2}, {:item_code=>6, :qty=>2}],
  #    2=>[{:item_code=>3, :qty=>2}, {:item_code=>4, :qty=>2},
  #        {:item_code=>7, :qty=>2}, {:item_code=>8, :qty=>2}]}

This makes use of the form of Hash#update (a.k.a. merge!) that employs a block to determine the values of keys that are present in both hashes being merged. See the doc for the definitions of the three block variables.

Alternatively, one could write the following.

array.each_with_object({}) { |g,h| h.update(g[:id]=>g) { |_,o,n|
  o.merge(n) { |k,oo,nn| k == :items ? oo+nn : oo } } }.values

Upvotes: 1

yoones
yoones

Reputation: 2474

Firstly I map the ids, then for each id, I make a hash with two keys: the id and a flattened map of each entry's items:

result = array.group_by { |e| e[:id] }.map { |id, entries| {id: id, items: entries.flat_map { |entry| entry[:items] }} }

Upvotes: 1

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

There is no need for group_by. Go straight with each_with_object directly from the scratch.

array.
  each_with_object({}) do |hash, acc|
    acc[hash[:id]] ?
      acc[hash[:id]][:items] |= hash[:items] : 
      acc[hash[:id]] = hash
  end.values
#⇒ [{:id=>1, :items=>[{:item_code=>1, :qty=>2},
#                     {:item_code=>2, :qty=>2},
#                     {:item_code=>5, :qty=>2},
#                     {:item_code=>6, :qty=>2}]},
#   {:id=>2, :items=>[{:item_code=>3, :qty=>2},
#                     {:item_code=>4, :qty=>2},
#                     {:item_code=>7, :qty=>2},
#                     {:item_code=>8, :qty=>2}]}]

Upvotes: 2

Ursus
Ursus

Reputation: 30056

You can combine group_by and transform_values! (for the latter you need Ruby 2.4.0 and later, otherwise you can use each_with_object as @Jagdeep pointed out)

array.group_by { |item| item[:id] }
    .transform_values! { |v| v.flat_map { |subitem| subitem[:items] } }
    .map { |(id, items)| Hash["id", id, "items", items] }

Upvotes: 1

Jagdeep Singh
Jagdeep Singh

Reputation: 4920

Use group_by and each_with_object:

ary.group_by { |elem| elem[:id] }.
    each_with_object([]) do |(id, grouped_ary), out|
                           out << { id: id, items: grouped_ary.map { |h| h[:items] }.reduce(:|) }
                         end
 => [{:id=>1, :items=>[{:item_code=>1, :qty=>2},
                       {:item_code=>2, :qty=>2},
                       {:item_code=>5, :qty=>2},
                       {:item_code=>6, :qty=>2}]},
     {:id=>2, :items=>[{:item_code=>3, :qty=>2},
                       {:item_code=>4, :qty=>2}, 
                       {:item_code=>7, :qty=>2},
                       {:item_code=>8, :qty=>2}]}] 

Upvotes: 2

Related Questions