2

I have an array of hashes with the keys being countries and the values being number of days.

I would like to aggregate over the hashes and sum the values for the countries that are the same.

the array could look like this countries = [{"Country"=>"Brazil", "Duration"=>731/1 days}, {"Country"=>"Brazil", "Duration"=>365/1 days}]

I would like this to return something on the lines of: [{"Country" => "Brazil", "Duration"=>1096/1 days}]

I tried the other questions on SO like this one

countries.inject{|new_h, old_h| new_h.merge(old_h) {|_, old_v, new_v| old_v + new_v}}

Produces {"Country"=>"BrazilBrazil", "Duration"=>1096/1 days}

Is there a way to selectively only merge specific values?

legendary_rob
  • 12,792
  • 11
  • 56
  • 102

2 Answers2

4

This uses the form of Hash::new that creates a creates an empty hash with a default value (here 0). For a hash h created that way, h[k] returns the default value if the hash does not have a key k. The hash is not modified.

countries = [{"Country"=>"Brazil",    "Duration"=>"731/1 days"},
             {"Country"=>"Argentina", "Duration"=>"123/1 days"},
             {"Country"=>"Brazil",    "Duration"=>"240/1 days"},
             {"Country"=>"Argentina", "Duration"=>"260/1 days"}]

countries.each_with_object(Hash.new(0)) {|g,h| h[g["Country"]] += g["Duration"].to_i }.
  map { |k,v| { "Country"=>k, "Duration"=>"#{v}/1 days" } }
    #=> [{"Country"=>"Brazil",    "Duration"=>"971/1 days"},
    #    {"Country"=>"Argentina", "Duration"=>"383/1 days"}]

The first hash passed to the block and assigned to the block variable g.

g = {"Country"=>"Brazil", "Duration"=>"731/1 days"}

At this time h #=> {}. We then compute

h[g["Country"]] += g["Duration"].to_i
  #=> h["Brazil"] += "971/1 days".to_i
  #=> h["Brazil"] = h["Brazil"] + 971
  #=> h["Brazil"] = 0 + 971 # h["Brazil"]

See String#to_i for an explanation of why "971/1 days".to_i returns 971.

h["Brazil"] on the right of the equality returns the default value of 0 because h does not (yet) have a key "Brazil". Note that h["Brazil"] on the right is syntactic sugar for h.[]("Brazil"), whereas on the left it is syntactic sugar for h.[]=(h["Brazil"] + 97). It is Hash#[] that returns the default value when the hash does not have the given key. The remaining steps are similar.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • `Hash.new(0)` will convert the duration to seconds. You may want to try do `Hash.new(0.days)` and then inside the map block you can do `"Duration" => v` – legendary_rob Jul 04 '17 at 08:47
  • 1
    @TheLegend, thanks for the suggestion. I don't know Rails so I gave a pure-Ruby solution. – Cary Swoveland Jul 04 '17 at 19:39
2

You may update your code as follows:

countries.inject do |new_h, old_h| 
    new_h.merge(old_h) do |k, old_v, new_v|
        if k=="Country" then old_v else old_v + new_v end
    end 
end
#  => {"Country"=>"Brazil", "Duration"=>1096} 

where you basically use the k (for key) argument to switch among different merging policies.

metaphori
  • 2,681
  • 1
  • 21
  • 32