5
\$\begingroup\$

I have an array of hashes of form:

{ description: 'string', b: float, c: float }

I need to get an array of merged hashes so that if hashes have same value at key description sum values of b and c, (of course keeping value of a unaltered), otherwise (if value of description is unique) just add hash to array.

Current solution:

array_of_hashes.inject([]) do |lines, line|
  if lines.none? { |l| l[:description] == line[:description] }
    lines << line
  else
    lines
      .find { |l| l[:description] == line[:description] }
      .merge!(line) { |key, old_v, new_v| key == :description ? old_v : old_v + new_v }
    lines
  end
end

I am 100% sure there's a nicer solution.

\$\endgroup\$
3
\$\begingroup\$

Besides the code being a little more complex than it needs to be, I see two issues:

  • a performance problem: the .find { |l| l[:description] == line[:description] } is called for each element in the array of hashes, leading to an O(n^2) time algorithm (because the array lookup takes linear time)
  • a more subtle thing: the initial hashes are mutated by your code. When you do old_v + new_v that modifies the very hashes that have been provided as an input to your function/code. This might not be a problem for you, but I think it's good practice to avoid mutability where possible.

To solve these things, let's use a hash of hashes. We'll have a hash that has the :description values as key (thus allowing to look things up in constant time) and a hash containing the values to be cummulated.

Also, to save the verification of whether a certain key already exists or not, I will use the new {|hash, key| block } syntax when creating the outside hash.

Here's the code:

results = Hash.new { |h, k| h[k] = {b: 0, c: 0} }
array_of_hashes.each do |h|
  cummulative_hash = results[h[:description]]
  cummulative_hash[:b] += h[:b]
  cummulative_hash[:c] += h[:c]
end

At this point, results looks like this:

{"string"=>{:b=>3, :c=>6}, "other_string"=>{:b=>1, :c=>2}}

This can be easily converted to your array format, like this:

results.map{|k, v| {description: k}.merge v}

=> [{:description=>"string", :b=>3, :c=>6},
    {:description=>"other_string", :b=>1, :c=>2}]

In the example above I hardcoded the :b and :c keys. For a more generic treatment of the keys to be cummulated, here's the (slightly adjusted) code:

results = Hash.new { |h, k| h[k] = {} }
array_of_hashes.each do |h|
  cummulative = results[h[:description]]
  h.each do |k, v|
    cummulative[k] = (cummulative[k] || 0) + h[k] if k != :description
  end
end
results.map{|k, v| {description: k}.merge v}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks a ton for your answer! cummulative_hash = results[h[:description]] is an awesome thing as well as default proc. My concern here is that I would like to get it done within single cycle of iteration, and both of solution you've provided suggest 2 iterations. \$\endgroup\$ – Andrey Deineko Oct 26 '17 at 8:08
  • \$\begingroup\$ @AndreyDeineko Is it absolutely necessary that the output is an array? If so, I don't know how you can do it in less than two iterations with reasonable performance. If, however, it's acceptable to output a hash, then just use the code without the last line. \$\endgroup\$ – Cristian Lupascu Oct 30 '17 at 8:29
0
\$\begingroup\$

Use Enumerable#group_by to divide the array_of_hashes into groups by description. Then replace each group with a single hash that contains the sum.

array_of_hashes = [
  {description: 'small', a: 1, b: 0.2, c: 0.3},
  {description: 'large', a: 100, b: 200, c: 300},
  {description: 'small', a: 4, b: 0.5, c: 0.6},
  {description: 'large', a: 400, b: 500, c: 600},
  {description: 'unique', a: 'hi', b: true, c: false},
]

answer = array_of_hashes.group_by {|h| h[:description]}.values
answer.map! {|first, *rest|
  if rest.empty?
    first
  else
    first.dup.tap {|sum|
      rest.each {|h| sum[:b] += h[:b]; sum[:c] += h[:c]}}
  end
}

require 'pp'
pp answer

Output is

[{:description=>"small", :a=>1, :b=>0.7, :c=>0.8999999999999999},
 {:description=>"large", :a=>100, :b=>700, :c=>900},
 {:description=>"unique", :a=>"hi", :b=>true, :c=>false}]

In this code, if a group has only one hash (with a unique description), we just add hash to answer without duplicating the hash or doing sums (0 + true would fail). If a group has more than one hash, we put the sums in a duplicate of the first hash. Unfortunately, the answer doesn't preserve the values of a: 4 and a: 400.

| improve this answer | |
\$\endgroup\$

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.