Last Updated: February 25, 2016
·
558
· lucasmartins

Recursive Hash Reduce, merging hashes while summing its values.

Recently I needed a quick solution for merging Hashes with different schemes, while reducing its values. In my case, I've had a big array with a lot of reports - in hashes - and I needed to reduce all its values into a merged report, pretty much like this:

hash_a = { { a: 1, b: 2, c: { d: 3, e: { f: 4, g: 5 } } } }
hash_b = { { a: 1, b: 2, c: { d: 3 } } }
reduced_hash = { { a: 2, b: 4, c: { d: 6, e: { f: 4, g: 5 } } } }

As I couldn't find anything handy that solved this, I've wrote the following HashReducer to help me out (see the gist):

module HashReducer
  # use the block to transform the addition/reduce result for each value
  def self.reduce!(base: nil, add: nil, &block)
    base = {} unless base
    add = {} unless add
    fail 'base: must be a hash!' unless base.is_a?(Hash)
    fail 'add: must be a hash!' unless add.is_a?(Hash)
    add.each do |k,v|
      if v.is_a?(Hash)
        base[k] = {} unless base[k]
        base[k] = self.reduce!(base: base[k], add: v, &block)
      else
        initialize_value_field!(base, add, k)
        base[k] += add[k] if add[k].respond_to?(:+)
        if block_given?
          base[k] = block.call(base[k])
        end
      end
    end
    base
  end

  private

  def self.initialize_value_field!(base, add, k)
    unless base[k]
      if add[k].respond_to?(:new)
        base[k] = add[k].class.new
      elsif add[k].respond_to?(:+)
        base[k] = 0
      else
        fail "Don't know how to initialize a #{add[k].class} object!"
      end
    end
  end
end

Usage is simple, you need a base hash, and a hash you want to add to it:

base = {a: 1.99}
other = {a: 1, b: 2.99}
HashReducer.reduce!(base: base, add: other)
# {a: 2.99, b: 2.99}

If you wanna get fancy, you can transform the sum result by passing a block:

HashReducer.reduce!(base: base, add: other) do |sum|
  sum.round
end
# {a: 3, b: 3}

If you know something better, please let me know!