3

I saw this piece of code in this post, because I'm trying to sum values in an array of hashes based on some criteria.

Rails sum values in an array of hashes

array = [
  {loading: 10, avg: 15, total: 25 },
  {loading: 20, avg: 20, total: 40 },
  {loading: 30, avg: 25, total: 55 }
]

sum = Hash.new(0)

array.each_with_object(sum) do |hash, sum|
  hash.each { |key, value| sum[key] += value }
end
 # => {:loading=>60, :avg=>60, :total=>120}

What I'm trying to do and I don't know how, is to sum total key if loading and avg appear with the same values more than once in this array. For instance.

 array = [
      {loading: 10, avg: 15, total: 25 },
      {loading: 20, avg: 20, total: 40 }, # See here
      {loading: 30, avg: 25, total: 55 },
      {loading: 20, avg: 20, total: 80 }, # See here
      {loading: 10, avg: 20, total: 46 }
    ]

The result would be:

[
  {loading: 10, avg: 15, total: 25 },
  {loading: 20, avg: 20, total: 120 }, # Results in this
  {loading: 30, avg: 25, total: 55 },
  {loading: 10, avg: 20, total: 46 }
]

I tried to modify this line

hash.each { |key, value| sum[key] += value }

Adding a conditional that checks if the value is repeated but I didn't succeed.

Any help, ideas or anything will be welcome.

Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
SgtPepper
  • 418
  • 6
  • 18
  • 1
    I expect you thought enclosing some hashes in `**` would be helpful to readers, but I just found it confusing. It's perfect clear what you want to do without "hey, look here". More generally, when giving examples all inputs should be valid Ruby objects (no `**`, no `etc.`, no `...`). Among other things that allows readers to simply cut-and-paste your code without the need for subsequent editing. It's commendable that you assigned a variable to the array (`array = [...)` so readers can refer to `array` without having to define it. Many askers leave inputs nameless, which is a minor pain. – Cary Swoveland May 09 '17 at 19:20

3 Answers3

4

This seems work

array.group_by { |item| [item[:loading], item[:avg]] }.values.flat_map { |items| items.first.merge(total: items.sum { |h| h[:total] }) }
=> [{:loading=>10, :avg=>15, :total=>25}, {:loading=>20, :avg=>20, :total=>120}, {:loading=>30, :avg=>25, :total=>55}, {:loading=>10, :avg=>20, :total=>46}]
Ursus
  • 29,643
  • 3
  • 33
  • 50
2

You can use Enumerable#group_by and Hash#merge! to handle this as

array.group_by {|h| [h[:loading],h[:avg]]}.values.map do |a|
  a.inject do |memo,h| 
    memo.merge!(h) do |_k,old_value,new_value| 
      old_value + new_value
    end
  end
end
#=> [{:loading=>10, :avg=>15, :total=>25}, 
#    {:loading=>40, :avg=>40, :total=>120}, 
#    {:loading=>30, :avg=>25, :total=>55}, 
#    {:loading=>10, :avg=>20, :total=>46}]

Quick Breakdown

  • array.group_by group the elements by the loading and avg values this will return a hash of {grouped_values => [objects]}
  • values [objects] from the above groupings (because we don't specifically care about the keys)
  • map into a new Array
  • inject will pass the first element as memo and will yield the second element on the first iteration. After that memo will be the return value from inject and the next element will be yielded to the block until there are no elements left.
  • merge! with block syntax allows you to handle duplicate keys. In this case you want to add the values.
engineersmnky
  • 25,495
  • 2
  • 36
  • 52
1

This uses the form of Hash#update (aka merge!) that employs a block to determine the values of keys that are present in both hashes being merged.1 See the doc for the definitions of the block variables _k, o an n. The first variable (_k) begins with an underscore (often written simply _) to signify that that block variable is not used in the block calculation. (Note that @engineersmnky dd the same.)

arr = [
  {loading: 10, avg: 15, total: 25 },
  {loading: 20, avg: 20, total: 40 },
  {loading: 30, avg: 25, total: 55 },
  {loading: 20, avg: 20, total: 80 },
  {loading: 10, avg: 20, total: 46 }
]

arr.each_with_object({}) { |g,h| h.update(g.values_at(:loading, :avg)=>g) { |_k,o,n|
  o.merge(total: o[:total]+n[:total]) } }.values
  #=> [{:loading=>10, :avg=>15, :total=>25},
  #    {:loading=>20, :avg=>20, :total=>120},
  #    {:loading=>30, :avg=>25, :total=>55},
  #    {:loading=>10, :avg=>20, :total=>46}]

Before .values is applied at the end, we will have constructed the following hash:

arr.each_with_object({}) { |g,h| h.update(g.values_at(:loading, :avg)=>g) { |_k,o,n|
  o.merge(total: o[:total]+n[:total]) } }
  #=> {[10, 15]=>{:loading=>10, :avg=>15, :total=>25},
  #    [20, 20]=>{:loading=>20, :avg=>20, :total=>120},
  #    [30, 25]=>{:loading=>30, :avg=>25, :total=>55},
  #    [10, 20]=>{:loading=>10, :avg=>20, :total=>46}}

Had @Ursus (whose answer preceded mine) taken this approach I would have used group_by. The two approaches seems to always be interchangeable. I don't think one is better; it's just a matter of personal preference. It seems that @engineersmnky couldn't make up his mind, so he used a bit of both.

1. I feel like I've written this sentence (verbatim) hundreds of times. ¯\_(ツ)_/¯

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Ouch. :P I made up my mind and chose readability. *" Of course, people do go both ways. That's the trouble. I can't make up my mind."* – engineersmnky May 09 '17 at 18:58