0

I have an array of objects that I'd like to group by field1 and sum by field2. An example would be a class product that has a title field and a price field. In an array of products, I have multiple gloves with different prices, and multiple hats with different prices. I'd like to have an array with distinct titles, that aggregate all the prices under the same title.

There's an obvious solution with iterating over the array and using a hash, but I was wondering if there was a "ruby way" of doing something like this? I've seen a lot of examples where Ruby has some unique functionality that applies well to certain scenarios and being a Ruby newbie I'm curious about this.

Thanks

1
  • Can you show some examples of your data layout? Commented Feb 23, 2018 at 3:06

2 Answers 2

3

There's a method transform_values added in ruby 2.4 or if you require 'active_support/all', with this you can do something like so:

products = [
  {type: "hat", price: 1, name: "fedora"},
  {type: "hat", price: 2, name: "sombrero"},
  {type: "glove", price: 3, name: "mitten"},
  {type: "glove", price: 4, name: "wool"}
]

result = products
  .group_by { |product| product[:type] }
  .transform_values { |vals| vals.sum { |val| val[:price] } }
# => {"hat"=>3, "glove"=>7}
Sign up to request clarification or add additional context in comments.

Comments

1

It's a little unclear to me from the question as asked what your data looks like, so I ended up with this:

Product = Struct.new(:title, :price)

products = [
  Product.new("big hat", 1),
  Product.new("big hat", 2),
  Product.new("small hat", 3),
  Product.new("small hat", 4),
  Product.new("mens glove", 8),
  Product.new("mens glove", 9),
  Product.new("kids glove", 1),
  Product.new("kids glove", 2)
]

Given that data, this is how I'd go about building a data structure which contains the sum of all the prices for a given title:

sum_by_title = products.inject({}) do |sums, product|
  if sums[product.title]
    sums[product.title] += product.price
  else
    sums[product.title] = product.price
  end
  sums
end

This produces:

{"big hat"=>3, "small hat"=>7, "mens glove"=>17, "kids glove"=>3}

To explain:

Ruby inject takes an initial value and passes that to the iteration block as a "memo". Here, {} is the initial value. The return value from the block is passed into the next iteration as the memo.

The product.title is used as a hash key and the running sum is stored in the hash value. An if statement is necessary because the first time a product title is encountered, the stored value for that title is nil and cannot be incremented.


I probably wouldn't ship this code due to the hidden magic of the hash default value constructor but it's possible to write the same code without the if statement:

sum_by_title = products.inject(Hash.new { 0 }) do |sums, product|
  sums[product.title] += product.price
  sums
end

Hope you enjoy Ruby as much as I do!

1 Comment

1. Initialize the hash in inject with Hash.new {0} to avoid ifs 2. Use each_with_object instead of inject to avoid an ugly return. That said: products.each_with_object(Hash.new {0}) { |product, sums| sums.tap { |s| s.[product.title] += product.price } }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.