8

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.

JordanC
  • 131
  • 1
  • 1
  • 10
  • 2
    your testhash is messed up or not a hash; in your reduce code you use a class variable for testhash. The amount of complexity in your method is disturbing, nobody will be able to tell you why `reduce` fails. I think with a good refactoring you'll be able to get the bugs out. – Beat Richartz Nov 21 '13 at 22:22
  • 1
    Jordan, your first problem is that `@testhash` is not a hash. If you changed the outer `{}` to `[]` it would be an array of hashes; now it is not a Ruby object. Also, `reduce` and `inject` are synonyms. Best to simplify, as well as fix, `@testhash`, by removing unnecessary hash elements. The date, in particular, is not relevant but likely to cause complications. – Cary Swoveland Nov 21 '13 at 22:22
  • Beat - you are correct, I'm missing the []. @testhash is a simplified version of the hash I'm dealing with. Its just showing the relevant key value paris for this question. Cary The date field is there because I need to sum all the values except the ones from the first date. I have updated the hash – JordanC Nov 21 '13 at 22:36
  • Reading between the lines, it appears your reference to "first date" is to be interpreted as "do not count values from the hash having the earliest value of `date` (here the last hash in @testhash)". You should also remove 'hash' from the name `@testhash`. Please edit to clarify. – Cary Swoveland Nov 21 '13 at 23:12
  • Yes that is correct. I only want the values of any dates after the first date which is why I have done the group_by 'date', sorted and removed (.shift) the earliest date – JordanC Nov 22 '13 at 00:31

3 Answers3

44

If you have simple key/value hash

{1 => 42, 2 => 42}.values.sum
 => 84 
m4tm4t
  • 2,361
  • 1
  • 21
  • 37
7

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.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • This is working great!! Thank you so much. I'll keep reading about how you have processed it until I get a better understanding. I had the "type" field in there because there are also "sell" types which I need to exclude as well. But from what you have given me I'm sure I'll be able to work out that part. Thanks a lot for your help!!! – JordanC Nov 22 '13 at 01:30
  • I'm glad you found it helpful. You'll see I put the "type" field back in. – Cary Swoveland Nov 23 '13 at 15:57
0

Try:

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"}
]

except_first_date = test.sort_by {|i| i['date'] }
except_first_date.shift

result = except_first_date.inject({}) {|m,i| m[i["instrument_code"]] = m[i["instrument_code"]].to_f + i['total'] ; m }
# => {"food"=>110.0, "drink"=>12.0}
shweta
  • 8,019
  • 1
  • 40
  • 43