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}
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.