Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sum ruby hash values

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.

like image 629
JordanC Avatar asked Nov 21 '13 22:11

JordanC


People also ask

How do you combine hashes in Ruby?

We can merge two hashes using the merge() method. When using the merge() method: Each new entry is added to the end. Each duplicate-key entry's value overwrites the previous value.

How do you use the sum method in Ruby?

Ruby | Enumerable sum() function The sum() of enumerable is an inbuilt method in Ruby returns the sum of all the elements in the enumerable. If a block is given, the block is applied to the enumerable, then the sum is computed. If the enumerable is empty, it returns init.


2 Answers

If you have simple key/value hash

{1 => 42, 2 => 42}.values.sum
 => 84 
like image 97
m4tm4t Avatar answered Sep 29 '22 17:09

m4tm4t


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 hwith 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.

like image 23
Cary Swoveland Avatar answered Sep 29 '22 19:09

Cary Swoveland