Sum ruby hash values

hash, inject, reduce, ruby

Solution

I offer the following observations on your `inject` code:

- none of the variables need be instance variables; local variables (no `@`) would suffice;

- `test.group_by {|i| i.type}...` should be `test.group_by {|i| i["type"]}...`

- `@sortable_additions[key]=value` should raise an exception because the hash has not been created;

- `@sorted_additions.shift` removes the first element of the hash and returns that element, but there is no variable to receive it (e.g.,, `h = @sorted_additions.shift`);

- `@additions_per_security = Hash[@sorted_additions.map { |key, value|[key, value]}]` appears to convert @sorted_additions to an array and then back to the same hash.

The following is one way to do what you you want to do.

Firstly, you will be passing date objects. To work with that we'll start by making those date objects for the dates you have in your example:

require 'date'
date1 = Date.parse("Thu, 21 Nov 2013") # => #<Date: 2013-11-21 ((2456618j,0s,0n),+0s,2299161j)>
date2 = Date.parse("Thu, 14 Nov 2013") # => #<Date: 2013-11-14 ((2456611j,0s,0n),+0s,2299161j)>
date3 = Date.parse("Thu, 20 Nov 2013") # => #<Date: 2013-11-20 ((2456617j,0s,0n),+0s,2299161j)>
date4 = Date.parse("Thu, 11 Nov 2013") # => #<Date: 2013-11-11 ((2456608j,0s,0n),+0s,2299161j)>

For testing:

test = [{"total"=>18, "type"=>"buy", "date"=>date1, "instrument_code"=>"food"},
        {"total"=>92, "type"=>"buy", "date"=>date2, "instrument_code"=>"food"},
        {"total"=>12, "type"=>"buy", "date"=>date3, "instrument_code"=>"drink"},
        {"total"=> 1, "type"=>"buy", "date"=>date4, "instrument_code"=>"food"}]

Now we calculate what we need.

test_buy = test.select {|h| h["type"] == "buy"}

earliest = test_buy.min_by {|h| h["date"]}["date"]
  # => #<Date: 2013-11-11 ((2456608j,0s,0n),+0s,2299161j)>

all_but_last = test.reject {|h| h["date"] == earliest}
 # =>  [{"total"=>18, "type"=>"buy", "date"=>date1, "instrument_code"=>"food"},
        {"total"=>92, "type"=>"buy", "date"=>date2, "instrument_code"=>"food"},
        {"total"=>12, "type"=>"buy", "date"=>date3, "instrument_code"=>"drink"}] 

or we could have used `Enumerable#select`:

all_but_last = test.select {|h| h["date"] != earliest}

Note that here and below, the values of `date1`, `date2` and `date3` will be displayed (e.g., `#<Date: 2013-11-21 ((2456618j,0s,0n),+0s,2299161j)>` will be displayed for `date1`); I've used the variable names here as placeholders to make this more readable. Also, all hashes `h`with `h["date"] = earliest` will be rejected (should there be more than one).

grouped = all_but_last.group_by {|h| h["instrument_code"]}
 # => {"food" =>[{"total"=>18, "type"=>"buy", "date"=>date1, "instrument_code"=>"food"},
                  {"total"=>92, "type"=>"buy", "date"=>date2, "instrument_code"=>"food"}],
       "drink"=>[{"total"=>12, "type"=>"buy", "date"=>date3, "instrument_code"=>"drink"}]}

keys = grouped.keys # => ["food", "drink"]

arr = keys.map {|k| [k, grouped[k].reduce(0) {|t,h| t + h["total"]}]}
  # => [["food", 110], ["drink", 12]]

Hash[arr] # => {"food"=>110, "drink"=>12} 

I have used a few temporary variables, including `test_buy`, `earliest`, `all_but_last`, `grouped`, `keys` and `arr`. You can eliminate some of these by "chaining". Here I'll show you how to get rid of some of them:

test_buy = test.select {|h| h["type"] == "buy"}
earliest = test_buy.min_by {|h| h["date"]}["date"]
grouped = test_buy.reject {|h| h["date"] == earliest}.group_by \
  {|h| h["instrument_code"]}
Hash[grouped.keys.map {|k| [k, grouped[k].reduce(0) \
  {|t,h| t + h["total"]}]}] # => {"food"=>110, "drink"=>12}

You may think this looks complicated, but after you gain experience with Ruby, it will look very natural and read easily. The extent to which you use chaining is a style preference, however.

Problem

I am trying to sum values from a ruby hash but using either inject or reduce does not return the correct answer. It seems as though these methods are overwriting the current value being stored instead of summing them. My hash look like this: ``` @test = [ {"total"=>18, "type"=>"buy", "date"=>Thu, 21 Nov 2013, "instrument_code"=>"food"}, {"total"=>92, "type"=>"buy", "date"=>Thu, 14 Nov 2013, "instrument_code"=>"food"}, {"total"=>12, "type"=>"buy", "date"=>Wed, 20 Nov 2013, "instrument_code"=>"drink"}, {"total"=>1, "type"=>"buy", "date"=>Mon, 11 Nov 2013, "instrument_code"=>"food"} ] ``` Here is my inject code that fails: ``` def additions_per_security @test.group_by { |i| i.type }.each do |key, value| if key == "buy" value.group_by{ |i| i.date }.each do |key, value| @sortable_additions[key] = value end @sorted_additions = @sortable_additions.sort_by { |key,value| key } @sorted_additions.shift @additions_per_security = Hash[@sorted_additions.map { |key, value| [key, value] }] @additions_per_security.each do |key, value| value.group_by { |i| i.instrument_code }.each do |key, value| @additions_security[key] = value.inject(0){ |result, transaction| (result += transaction.total) } end end end end return @additions_security end ``` Here is my reduce code that fails: ``` def additions_per_security @@test.group_by { |i| i.type }.each do |key, value| if key == "buy" value.group_by { |i| i.date }.each do |key, value| @sortable_additions[key] = value end @sorted_additions = @sortable_additions.sort_by { |key,value| key } @sorted_additions.shift @additions_per_security = Hash[@sorted_additions.map { |key, value| [key, value] }] @additions_per_security.each do |key, value| value.group_by { |i| i.instrument_code }.each do |key, value| @additions_security[key] = value.map { |p| p.total }.reduce(0,:+) end end end end return @additions_security end ``` I have a hash and I want to sum the totals for all keys except the first date. I am currently getting the following: ``` {"drink"=>12.0, "food"=>92} ``` My expected result will look like this: ``` {"drink"=>12.0, "food"=>110} ``` Thanks in advance for any advice.

Original source