Reputation: 1118
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
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
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
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
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
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