Group hashes by keys and sum the values

35,691

Solution 1

ar = [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3}, {"Dry Goods"=>2}]
p ar.inject{|memo, el| memo.merge( el ){|k, old_v, new_v| old_v + new_v}}
#=> {"Vegetable"=>15, "Dry Goods"=>5}

Hash.merge with a block runs the block when it finds a duplicate; inject without a initial memo treats the first element of the array as memo, which is fine here.

Solution 2

Simply use:

array = [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3}, {"Dry Goods"=>2}]
array.inject{|a,b| a.merge(b){|_,x,y| x + y}}

Solution 3

ar = [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3}, {"Dry Goods"=>2}]

While the Hash.merge technique works fine, I think it reads better with an inject:

ar.inject({}) { |memo, subhash| subhash.each { |prod, value| memo[prod] ||= 0 ; memo[prod] += value } ; memo }
=> {"Dry Goods"=>5, "Vegetable"=>15}

Better yet, if you use Hash.new with a default value of 0:

ar.inject(Hash.new(0)) { |memo, subhash| subhash.each { |prod, value| memo[prod] += value } ; memo }
=> {"Dry Goods"=>5, "Vegetable"=>15}

Or if inject makes your head hurt:

result = Hash.new(0)
ar.each { |subhash| subhash.each { |prod, value| result[prod] += value } }
result
=> {"Dry Goods"=>5, "Vegetable"=>15}

Solution 4

I'm not sure that a hash is what you want here, because I don't multiple entries in each hash. so I'll start by changing your data representation a little.

ProductCount=Struct.new(:name,:count)
data = [ProductCount.new("Vegetable",10),
        ProductCount.new("Vegetable",5),
        ProductCount.new("Dry Goods",3),
        ProductCount.new("Dry Goods",2)]

If the hashes can have multiple key-value pairs, then what you probably want to do is

data = [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3>}, {"Dry Goods"=>2}]
data = data.map{|h| h.map{|k,v| ProductCount.new(k,v)}}.flatten

Now use the facets gem as follows

require 'facets'
data.group_by(&:name).update_values{|x| x.map(&:count).sum}

The result is

{"Dry Goods"=>5, "Vegetable"=>15}

Solution 5

If have two hashes with multiple keys:

h1 = { "Vegetable" => 10, "Dry Goods" => 2 }
h2 = { "Dry Goods" => 3, "Vegetable" => 5 }
details = {}
(h1.keys | h2.keys).each do |key|
  details[key] = h1[key].to_i + h2[key].to_i
end
details
Share:
35,691
blastula
Author by

blastula

Updated on March 29, 2020

Comments

  • blastula
    blastula about 4 years

    I have an array of hashes:

    [{"Vegetable"=>10}, {"Vegetable"=>5}, {"Dry Goods"=>3>}, {"Dry Goods"=>2}]
    

    I need to use inject here I think but I've really been struggling.

    I want a new hash that reflects the sum of the previous hash's duplicate keys:

    [{"Vegetable"=>15}, {"Dry Goods"=>5}]
    

    I'm in control of the code that outputs this hash so I can modify it if necessary. The results were mainly hashes because this could end up nested any number of levels deep and then it's easy to call flatten on the array but not flatten the keys/values of the hash too:

    def recipe_pl(parent_percentage=nil)
      ingredients.collect do |i|
    
        recipe_total = i.recipe.recipeable.total_cost 
        recipe_percentage = i.ingredient_cost / recipe_total
    
        if i.ingredientable.is_a?(Purchaseitem)
          if parent_percentage.nil?
            {i.ingredientable.plclass => recipe_percentage}
          else
            sub_percentage = recipe_percentage * parent_percentage
            {i.ingredientable.plclass => sub_percentage}
          end
        else
          i.ingredientable.recipe_pl(recipe_percentage)
        end
      end
    end 
    
  • blastula
    blastula over 13 years
    Thank you! Structs I am aware of but need to really get my hands dirty with, facets was completely unknown to me. I have added the original code that outputs the hashes because I can probably do something simpler there.
  • blastula
    blastula over 13 years
    Thanks, this answers the question and I did not know that about merge. Much appreciated.
  • PJP
    PJP over 13 years
    +1 This is one of those jewels that needs to be in the Ruby books, but isn't.
  • Windix
    Windix almost 11 years
    A veritable cornucopia of worthy suggestions
  • appleLover
    appleLover about 10 years
    could you explain the naming here, what does memo and el mean?
  • steenslag
    steenslag about 10 years
    @appleLover memo and el don't mean anything, you could exchange them with any word. They are variable names; I chose these for memo =>I remember, el => element
  • RubyMiner
    RubyMiner over 8 years
    Can u please help me understand this code, please mention the values for k, old_v, new_v. I got confused
  • steenslag
    steenslag over 8 years
    {|k, old_v, new_v| old_v + new_v} runs only when the hash already has a certain key (with a value: old_val). The key ("Vegeteable" and the old_value (10) and the new_value (5) are send to the block; the result of the block is then stored as the value (15) for that key.
  • Josh
    Josh almost 8 years
    Thanks for using underscore _ for the unused variable
  • wrydere
    wrydere about 7 years
    How might this be used to merge values rather than keys? For example if my array contains: [{"Vegetable"=>10,"Department"=>"Produce"}, {"Fruit"=>20, "Department"=>"Produce"}] If I wanted to get the total count of all items in the Produce department...
  • Pedro Cavalheiro
    Pedro Cavalheiro over 6 years
    If you need default values, you can change inject to reduce and define a starting hash. Example: p ar.reduce({ "Other" => 0 }) { |memo, el| memo.merge(el){ |k, old_v, new_v| old_v + new_v } }
  • Christian
    Christian over 5 years
    +1 Thanks! This answer solved my problem to merge hashes using a.merge(b) { |key, value_a, value_b | value_a + value_b }