简体   繁体   中英

How do I group and add values from nested hashes and arrays with same key?

I am trying to get the sum of points and average grade for each student inside this combination of hashes and arrays but all my attempts only return the general sum for all entries. Any ideas?

student_data = 
  {"ST4"=>[{:student_id=>"ST4", :points=> 5, :grade=>5}, 
           {:student_id=>"ST4", :points=>10, :grade=>4}, 
           {:student_id=>"ST4", :points=>20, :grade=>5}], 
   "ST1"=>[{:student_id=>"ST1", :points=>10, :grade=>3}, 
           {:student_id=>"ST1", :points=>30, :grade=>4}, 
           {:student_id=>"ST1", :points=>45, :grade=>2}], 
   "ST2"=>[{:student_id=>"ST2", :points=>25, :grade=>5}, 
           {:student_id=>"ST2", :points=>15, :grade=>1}, 
           {:student_id=>"ST2", :points=>35, :grade=>3}], 
   "ST3"=>[{:student_id=>"ST3", :points=> 5, :grade=>5}, 
           {:student_id=>"ST3", :points=>50, :grade=>2}]}

The desired hash can be obtained thusly.

student_data.transform_values do |arr|
  points, grades = arr.map { |h| h.values_at(:points, :grade) }.transpose
  { :points=>points.sum, :grades=>grades.sum.fdiv(grades.size) }
end
  #=> {"ST4"=>{:points=>35, :grades=>4.666666666666667},
  #    "ST1"=>{:points=>85, :grades=>3.0},
  #    "ST2"=>{:points=>75, :grades=>3.0},
  #    "ST3"=>{:points=>55, :grades=>3.5}} 

The first value passed to the block is the value of the first key, 'ST4' and the block variable arr is assigned that value:

a = student_data.first
  #=> ["ST4",
  #    [{:student_id=>"ST4", :points=> 5, :grade=>5},
  #     {:student_id=>"ST4", :points=>10, :grade=>4},
  #     {:student_id=>"ST4", :points=>20, :grade=>5}]
  #   ] 
arr = a.last
  #=> [{:student_id=>"ST4", :points=> 5, :grade=>5},
  #    {:student_id=>"ST4", :points=>10, :grade=>4},
  #    {:student_id=>"ST4", :points=>20, :grade=>5}]

The block calculations are as follows. The first value of arr passed by map to the inner block is

h = arr.first
  #=> {:student_id=>"ST4", :points=>5, :grade=>5} 
h.values_at(:points, :grade)
  #=> [5, 5] 

After the remaining two elements of arr are passed to the block we have

b = arr.map { |h| h.values_at(:points, :grade) }
  #=> [[5, 5], [10, 4], [20, 5]] 

Then

points, grades = b.transpose
  #=> [[5, 10, 20], [5, 4, 5]] 
points
  #=> [5, 10, 20] 
grades
  #=> [5, 4, 5] 

We now simply form the hash that is the value of 'ST4' .

c = points.sum
  #=> 35 
d = grades.sum
  #=> 14 
e = grades.size
  #=> 3 
f = c.fdiv(d)
  #=> 4.666666666666667 

The value of 'ST4' in student_data therefore maps to the hash

{ :points=>c, :grades=>f }
  #=> {:points=>35, :grades=>4.666666666666667} 

The mappings of the remaining keys of student_data are computed similarly.

See Hash#transform_values , Enumerable#map , Hash#values_at , Array#transpose , Array#sum and Integer#fdiv .

Whatever you expect can be achieved as below,

student_data.values.map do |z|
  z.group_by { |x| x[:student_id] }.transform_values do |v|
    { 
      points: v.map { |x| x[:points] }.sum, # sum of points
      grade: (v.map { |x| x[:grade] }.sum/v.count.to_f).round(2) # average of grades
    }
  end
end

As exact expected output format is not specified, obtained in following way,

=> [
  {"ST4"=>{:points=>35, :grade=>4.67}},
  {"ST1"=>{:points=>85, :grade=>3.0}},
  {"ST2"=>{:points=>75, :grade=>3.0}},
  {"ST3"=>{:points=>55, :grade=>3.5}}
]

For Ruby 2.6 using Object#then orObject#yield_self for Ruby 2.5

student_data.transform_values { |st| st
  .each_with_object(Hash.new(0)) { |h, hh|  hh[:sum_points] += h[:points]; hh[:sum_grade] += h[:grade]; hh[:count] += 1.0 }
  .then{ |hh| {tot_points: hh[:sum_points], avg_grade: hh[:sum_grade]/hh[:count] } }
}


How it works?

Given the array for each student:

 st = [{:student_id=>"ST4", :points=> 5, :grade=>5}, {:student_id=>"ST4", :points=>10, :grade=>4}, {:student_id=>"ST4", :points=>20, :grade=>5}]

First build a hash adding and counting using Enumerable#each_with_object with a Hash#default set at zero (Hash.new(0) )

 step1 = st.each_with_object(Hash.new(0)) { |h, hh| hh[:sum_points] += h[:points]; hh[:sum_grade] += h[:grade]; hh[:count] += 1.0 } #=> {:sum_points=>35, :sum_grade=>14, :count=>3.0}

Then use then! ( yield_self for Ruby 2.5)

 step2 = step1.then{ |hh| {tot_points: hh[:sum_points], avg_grade: hh[:sum_grade]/hh[:count] }} #=> {:tot_points=>35, :avg_grade=>4.666666666666667}

Put all together using Hash#transform_values as in the first snippet of code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM