Bruno Fernandes
Bruno Fernandes

Reputation: 145

How to group and sum values in array of arrays with the same structure

I have this array of arrays in the data attribute for each activity corresponding to the class attendance by hour.

I need the total attendance of each activity by hour. Is it possible to group and sum it?

Example: The correct result for the hour 7 is 50+60 = 110

[{:name=>"cardio", 
:data=>[["06", 999], ["07", 50], ["08", 0], ["09", 154], ["10", 1059], ["11", 90], ["12", 30], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]}, 
:name=>"swimming", 
:data=>[["06", 0], ["07", 60], ["08", 0], ["09", 0], ["10", 90], ["11", 50], ["12", 0], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]}]

Expected result:

:data=>[["06", 999], ["07", 110], ["08", 0], ["09", 154], ["10", 1149], ["11", 140], ["12", 30], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]

Upvotes: 3

Views: 1046

Answers (4)

Mayur Shah
Mayur Shah

Reputation: 3449

How to group and sum values in array of arrays with the same structure

You should do it like this :

[{:name=>"cardio", 
:data=>[["06", 999], ["07", 50], ["08", 0], ["09", 154], ["10", 1059], ["11", 90], ["12", 30], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]}, 
:name=>"swimming", 
:data=>[["06", 0], ["07", 60], ["08", 0], ["09", 0], ["10", 90], ["11", 50], ["12", 0], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]}]

Try this one:

act.map{|h| h[:data]}.flatten(1).group_by(&:first).map { |k,v| [k, v.map(&:last).inject(:+)] }
# => [["06", 999], ["07", 110], ["08", 0], ["09", 154], ["10", 1149], ["11", 140], ["12", 30], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]] 

Hope this will help you

Upvotes: 2

Cary Swoveland
Cary Swoveland

Reputation: 110675

Case 1: The values of :data are arrays of the same size whose elements (two-element arrays) are ordered the same by their first elements

arr = [{ :name=>"cardio",   :data=>[["06", 999], ["07", 50], ["08", 0]] }, 
       { :name=>"swimming", :data=>[["06",   0], ["07", 60], ["08", 0]] }]

a, b = arr.map { |h| h[:data].transpose }.transpose
  #=> [[["06", "07", "08"], ["06", "07", "08"]], [[999, 50, 0], [0, 60, 0]]]
{ :data=>a.first.zip(b.transpose.map { |col| col.reduce(:+) }) }
  #=> {:data=>[["06", 999], ["07", 110], ["08", 0]]}

Case 2: The values of :data are arrays which may differ in size and whose elements (two-element arrays) may not be ordered the same by their first element

arr = [{ :name=>"cardio",   :data=>[["05", 999], ["07", 50], ["08",  0]] }, 
       { :name=>"swimming", :data=>[["08", 300], ["04", 33], ["07", 60]] }] 

{ :data=>arr.flat_map { |g| g[:data] }.
    each_with_object(Hash.new(0)) { |(f,v),h| h[f] += v }.
    sort.
    to_a }
  #=>  {:data=>[["04", 33], ["05", 999], ["07", 110], ["08", 300]]}

Note:

  • depending on requirements, sort may not be required
  • the second method could be used regardless of whether the values of :data are defined in parallel
  • the second method uses the form of Hash::new which takes an argument (the default value) which here is zero. This is sometimes called a counting hash. See the doc for details.

Upvotes: 4

Aetherus
Aetherus

Reputation: 8888

actions=[{:name=>"cardio", 
:data=>[["06", 999], ["07", 50], ["08", 0], ["09", 154], ["10", 1059], ["11", 90], ["12", 30], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]}, 
{:name=>"swimming", 
:data=>[["06", 0], ["07", 60], ["08", 0], ["09", 0], ["10", 90], ["11", 50], ["12", 0], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]}]

actions.map{|act| act[:data]}.reduce({}) do |acc, data|
  acc.merge(data.to_h) {|k, v1, v2| v1 + v2}
end.to_a

This solution takes advantage of the fact that Ruby's Hash keeps the insertion order.

The merit of this algorithm is that it allows missing keys in either data array.

Upvotes: 0

dawg
dawg

Reputation: 103774

Given:

act=[{:name=>"cardio", 
:data=>[["06", 999], ["07", 50], ["08", 0], ["09", 154], ["10", 1059], ["11", 90], ["12", 30], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]}, 
{:name=>"swimming", 
:data=>[["06", 0], ["07", 60], ["08", 0], ["09", 0], ["10", 90], ["11", 50], ["12", 0], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]}]

You can do:

> act.map{ |h| h[:data] }
       .flatten(1)
       .group_by{ |h,n| h }
       .map { |k,v| [k, v.map(&:last).sum] }
=> [["06", 999], ["07", 110], ["08", 0], ["09", 154], ["10", 1149], ["11", 140], ["12", 30], ["13", 0], ["14", 0], ["15", 0], ["16", 0], ["17", 0], ["18", 0], ["19", 0], ["20", 0], ["21", 0], ["22", 0], ["23", 0]]

Or,

> act.map{|h| h[:data]}
     .flatten(1)
     .each_with_object(Hash.new(0)) {|e,h| h[e[0]]+=e[1]}.to_a

works too.

Upvotes: 1

Related Questions