2

I have a data structure that looks like this:

arr = [
  {
    price: 2.0,
    unit: "meter",
    tariff_code: "4901.99",
    amount: 200
   },
   {
    price: 2.0,
    unit: "meter",
    tariff_code: "4901.99",
    amount: 200
   },
   {
    price: 14.0,
    unit: "yards",
    tariff_code: "6006.24",
    amount: 500
   },
   {
    price: 14.0,
    unit: "yards",
    tariff_code: "6006.24",
    amount: 500
  }
]

I need to group all of these by tariff_code, while summing the price and amounts that correspond with that tariff code. So my expected output should be:

[
  {
    price: 4.0,
    unit: "meter",
    tariff_code: "4901.99",
    amount: 400
   },
   {
    price: 2.0,
    unit: "yards",
    tariff_code: "6006.24",
    amount: 1000
   }
]

receipt_data[:order_items].group_by { |oi| oi[:tariff_code] }.values

The group_by statement used above will allow me to group by tariff_code but I'm unable to work out a way to sum the other values. I'm sure there is a slick one-liner way to accomplish this...

  • Seems to me like you need to use some iterator like select, which filters on unique tarrif_code values. If you're trying with each element in an array, the Array methods are what you can perform on that array. ruby-doc.org/core-2.4.1/Array.html – Matthew Apr 12 at 19:17
  • 1
    I edited your array because it contained a couple of small errors (amount: was :amount: in two places). I also assigned a variable arr to the array. That allows readers to refer to the variable in answers and comments without having to define it. – Cary Swoveland Apr 12 at 20:24
  • Thanks for that! Sorry, i was in a rush :) – Misha Krul Apr 13 at 12:03
2

More verbose:

grouped_items = arr.group_by { |oi| oi[:tariff_code] }
result = grouped_items.map do |tariff_code, code_items|
  price, amount = code_items.reduce([0, 0]) do |(price, amount), ci|
    [price + ci[:price], amount + ci[:amount]]
  end
  {
    price:       price,
    unit:        code_items.first[:unit],
    tariff_code: tariff_code,
    amount:      amount
  }
end
#[
#  {:price=>4.0, :unit=>"meter", :tariff_code=>"4901.99", :amount=>400}
#  {:price=>28.0, :unit=>"yards", :tariff_code=>"6006.24", :amount=>1000}
#]
2

Just to add to the fun, the answer which uses group_by as @cary said, and mostly copying Pavel's answer. This is very bad performancewise and use only if the array is small . Also it uses sum which is available only in Rails. (can be replaced by .map { |item| item[:price] }.reduce(:+) in pure ruby)

arr.group_by { |a| a[:tariff_code] }.map do |tariff_code, items|
  {
    price: items.sum { |item| item[:price] },
    unit: items.first[:unit],
    tariff_code: tariff_code,
    amount: items.sum { |item| item[:amount] }
  }
end

This would have been even smaller if it was an array of objects (ActiveRecord objects maybe) with methods instead of hashes.

arr.group_by(&:tariff_code).map do |tariff_code, items|
  {
    price: items.sum(&:price]),
    unit: items.first[:unit],
    tariff_code: tariff_code,
    amount: items.sum(&:amount)
  }
end
2

There are two standard ways of addressing problems of this kind. One, which I've taken, is to 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. The other way is to use Enumerable#group_by, which I expect someone will soon employ in another answer. I do not believe either approach is preferable in terms of efficiency or readability.

arr.each_with_object({}) do |g,h|
  h.update(g[:tariff_code]=>g) do |_,o,n|
    { price: o[:price]+n[:price], unit: o[:unit], amount: o[:amount]+n[:amount] }
  end
end.values
  #=> [{:price=>4.0,  :unit=>"meter", :amount=>400},
  #    {:price=>28.0, :unit=>"yards", :amount=>1000}] 

Note that the receiver of values is seen to be:

{"4901.99"=>{:price=>4.0,  :unit=>"meter", :amount=>400},
{"6006.24"=>{:price=>28.0, :unit=>"yards", :amount=>1000}} 
  • Brilliant, this works perfectly. Thank you!! – Misha Krul Apr 12 at 22:45
1

A simple approach, but its easy to add new keys for summing and to change a group key. Not sure about efficiency, but 500_000 times Benchmark of arr.map here looks good

#<Benchmark::Tms:0x00007fad0911b418 @label="", @real=1.480799000000843, @cstime=0.0, @cutime=0.0, @stime=0.0017340000000000133, @utime=1.4783359999999999, @total=1.48007>

summ_keys = %i[price amount]
grouping_key = :tariff_code
result = Hash.new { |h, k| h[k] = {} }
arr.map do |h|
  cumulative = result[h[grouping_key]]
  h.each do |k, v|
    case k
    when *summ_keys
      cumulative[k] = (cumulative[k] || 0) + h[k]
    else
      cumulative[k] = v
    end
  end
end
p result.values

# [{:price=>4.0, :unit=>"meter", :tariff_code=>"4901.99", :amount=>400},
#  {:price=>28.0, :unit=>"yards", :tariff_code=>"6006.24", :amount=>1000}]

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.