Home > other >  Ruby transform hash into a running total of values
Ruby transform hash into a running total of values

Time:10-23

I have a hash

{:a => 2, :b => 8, :c => 10, :d => 40 }

I want to modify the hash so that the values represent a running total.

{:a => 2, :b => 10, :c => 20, :d => 60 }

I've gone down a few paths, but haven't found the right answer. Some of the things I've tried.

data.inject({}) { |result, (k,v)| result[k] = v  result.values.compact.sum; result }

=> {:a=>2, :b=>10, :c=>22, :d=>74}

I'm obviously not quite grasping how the result is accumulating in that inject. I'm using compact due to the first iteration of k being nil.

I can get the values as an array like so

data.inject([]) { |result, element| result << result.last.to_i   element.last }

=> [2, 10, 20, 60]

I can't work out how to modify this to work similarly with a hash.

I can do it like this, but it doesn't seem like the right/most efficient way to do it.

totals = data.inject([]) { |result, element| result << result.last.to_i   element.last }

=> [2, 10, 20, 60]

data.map.with_index { |(k,v),i| [k,totals[i]] }.to_h

=> {:a=>2, :b=>10, :c=>20, :d=>60}

Appreciate any help I can get here.

CodePudding user response:

Given that you know the order of the keys you can simply write

h= { :a => 2, :b => 8, :c => 10, :d => 40 }
cumulative = 0
h.transform_values { |v| cumulative  = v }
  #=> {:a=>2, :b=>10, :c=>20, :d=>60}

See Hash#transform_values.

CodePudding user response:

Hashes and Ordering

Hashes are not guaranteed to be in sorted order, so your premise that you can keep a running total are (in principle) invalid unless you are sorting the keys before passing the values to #inject or #reduce. However, MRI Ruby often keeps a Hash in insertion order, but trust that at your own risk.

Solution

Setting aside the sort-order of your Hash, which you could address with #sort or #sort_by and a little refactoring if you choose, I would modify the original Hash in place like this in Ruby 3.0.2:

hash = {:a => 2, :b => 8, :c => 10, :d => 40 }

# Note that positional argument `_1` is the
# accumulator, and `_2` is the value.
hash = hash.keys.zip(
  hash.values.inject([]) { _1 << _2   _1&.last.to_i }
).to_h;
hash
#=> {:a=>2, :b=>10, :c=>20, :d=>60}

If you don't want to assign back to the same Hash, or want to assign the result somewhere else, you can do that too. However, as I understood the question to be about in-place modification of your Hash, assigning the result of the expression back to hash seemed like the easiest thing to do.

CodePudding user response:

Your "inject and map.with_index" is close but you can combine them:

data.inject({}) { |h, (k, v)| h.merge(k => v   h.values.last.to_i) }

or with less copying:

data.inject({}) { |h, (k, v)| h.merge!(k => v   h.values.last.to_i) }

or perhaps:

data.each_with_object({}) { |(k, v), h| h[k] = v   h.values.last.to_i }

h starts off empty so h.values can be empty so h.values.last.nil? will happen, hence the #to_i call to "hide" the nil test and simplify the block.

  • Related