1

I have a two-dimensional array as follows:

ary = [["a", 10],["a", 20],["b", 9],["b",7],["c",12]]

I want to sum the numeric values with the same key, building a hash like:

desired_result = {"a"=>30, "b"=>16, "c"=>12}

I can use a hash with a default (0) and a loop as follows:

rslt = Hash.new(0)
ary.each do |line|
  rslt[line[0]] += line[1]
end

But I want to avoid the loop and use enumeration functions. I came up with the following (quite ugly) expression:

rslt = ary.group_by {|a| a[0]}.map {|k,v| [k, v.map {|v| v[1]}.reduce(:+)]}.to_h

which is much harder to read than the loop-version.

Is there a way to do this more elegantly without a loop?

sawa
  • 165,429
  • 45
  • 277
  • 381

4 Answers4

3

You could use each_with_object (or inject) and pass in a hash where new keys get initialized with a value of zero:

ary.each_with_object(Hash.new(0)){ |(k, v), count| count[k] += v }
# => {"a"=>30, "b"=>16, "c"=>12}
jerhinesmith
  • 15,214
  • 17
  • 62
  • 89
  • 2
    I suggest you edit to elaborate,"new keys get initialized with a value of zero" and provide a link to the doc for `Hash::new`. – Cary Swoveland Mar 23 '19 at 19:12
2

After Enumerable#group_by I'd suggest to chain Hash#transform_values:

ary.group_by(&:first).transform_values { |v| v.sum(&:last) }
#=> {"a"=>30, "b"=>16, "c"=>12}
iGian
  • 11,023
  • 3
  • 21
  • 36
  • Thank you! I really like this one - concise and no necessity for either defaults or re-casting to hash. Did not know about transform values... – Marek Radebeul Mar 24 '19 at 08:57
1
ary = [["a", 10],["a", 20],["b", 9],["b",7],["c",12]]

h=ary.group_by{|x|x[0]}.each_with_object({}) do |(k,v),h|
  h[k]=v.sum{|y|y[1]}
end

p h

{"a"=>30, "b"=>16, "c"=>12}
Rajagopalan
  • 5,465
  • 2
  • 11
  • 29
1
ary.group_by(&:first).map {|k, v| [k, v.sum(&:last)]}.to_h
Sajad Rastegar
  • 3,014
  • 3
  • 25
  • 36