简体   繁体   中英

Summing up values inside an array of hashes in Ruby

I have the following array of hashes:

books = [
  {'author' => 'John Doe', 'month' => 'June', 'sales' => 500},
  {'author' => 'George Doe', 'month' => 'June', 'sales' => 600},
  {'author' => 'John Doe', 'month' => 'July', 'sales' => 700},
  {'author' => 'George Doe', 'month' => 'July', 'sales' => 800}
]

Out of this, how can I get an array, with the following:

[
  {'author' => 'John Doe', 'total_sales' => 1200},
  {'author' => 'George Doe', 'total_sales' => 1400}
]

You can use each_with_object with an appropriate default:

default = Hash.new {|h,k| h[k] = { 'author' => k, 'total_sales' => 0 } }
books.each_with_object(default) do |h, memo|
  memo[h['author']]['total_sales'] += h['sales']
end.values

Outputs:

[
  {"author"=>"John Doe", "total_sales"=>1200}, 
  {"author"=>"George Doe", "total_sales"=>1400}
]

You could use the form of Hash#update (aka merge! ) that employs a block to determine the values of keys that are present in both hashes being merged:

books.each_with_object({}) do |g,h|
  h.update(g['author']=>g['sales']) { |_,o,n| o+n }
end.map { |k,v| { 'author'=>k, 'total_sales'=>v } } 
  #=> [{"author"=>"John Doe", "total_sales"=>1200},
  #    {"author"=>"George Doe", "total_sales"=>1400}] 

See the doc for a description of the block variables _ , o and n . (I used _ for the common key, mainly to signal to the reader that it is not used in the block calculation, a common Ruby convention.)

Consider stopping after the hash has been constructed:

books.each_with_object({}) do |g,h|
  h.update(g['author']=>g['sales']) { |_,o,n| o+n }
end
  #=> {"John Doe"=>1200, "George Doe"=>1400}  

You can use Enumerable#group_by to get an array of array. Then you can merge the hashes of each array using Enumerable#reduce and Hash#merge .

group_books = books.group_by { |h| h['author'] }.values
result = group_books.map do |ary|
  ary.reduce do |new_hash, h|
    new_hash.merge(h) { |k, v1, v2| k == 'sales' ? v1 + v2 : v1 }
  end
end

EDIT : I forgot to remove the key month with Hash#delete .

result.map { |h| h.delete('month') }

I would use something like this:

sales = ->(hash) { hash['sales'] }
books.group_by { |book| book.slice('author') }
     .map { |result, books| result.merge('total_sales' => books.sum(&sales)) }
#=> [
#   {"author"=>"John Doe", "total_sales"=>1200},
#   {"author"=>"George Doe", "total_sales"=>1400}
# ]

The code mostly speaks for itself. I first group_by a slice of the hash. Then merge the sum of the sales values from books .

I've extracted the sum block to a lambda and saved that in the sales variable to not clutter the map block with nested blocks.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM