3

I want a function that can take an array like [:a, :b, :c] and recursively set hash keys, creating what it needs as it goes.

hash = {}

hash_setter(hash, [:a, :b, :c], 'value') 
hash #=> {:a => {:b => {:c => 'value' } } }

hash_setter(hash, [:a, :b, :h], 'value2') 
hash #=> {:a => {:b => {:c => 'value', :h => 'value2' } } }

I'm aware that Ruby 2.3's dig can be used for getting in this way, though that doesnt quite get you to an answer. If there was a setter equivalent of dig that'd be what I'm looking for.

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
Ben G
  • 26,091
  • 34
  • 103
  • 170

3 Answers3

0

Code

def nested_hash(keys, v, h={})
  return subhash(keys, v) if h.empty?
  return h.merge(subhash(keys, v)) if keys.size == 1
  keys[0..-2].reduce(h) { |g,k| g[k] }.update(keys[-1]=>v)
  h
end

def subhash(keys, v)
  *first_keys, last_key = keys
  h = { last_key=>v }
  return h if first_keys.empty?
  first_keys.reverse_each.reduce(h) { |g,k| g = { k=>g } }
end

Examples

h = nested_hash([:a, :b, :c], 14)    #=> {:a=>{:b=>{:c=>14}}}
i = nested_hash([:a, :b, :d], 25, h) #=> {:a=>{:b=>{:c=>14, :d=>25}}}
j = nested_hash([:a, :b, :d], 99, i) #=> {:a=>{:b=>{:c=>14, :d=>99}}}
k = nested_hash([:a, :e], 104, j)    #=> {:a=>{:b=>{:c=>14, :d=>99}, :e=>104}}
    nested_hash([:f], 222, k)        #=> {:a=>{:b=>{:c=>14, :d=>99}, :e=>104}, :f=>222}

Observe that the value of :d is overridden in the calculation of j. Also note that:

subhash([:a, :b, :c], 12)
  #=> {:a=>{:b=>{:c=>12}}}

This mutates the hash h. If that is not desired one could insert the line

  f = Marshal.load(Marshal.dump(h))

after the line return subhash(keys, v) if h.empty? and change subsequent references to h to f. Methods from the Marshal module can be used to create a deep copy of a hash so the original hash is not be mutated.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

Solved it with recursion:

def hash_setter(hash, key_arr, val)
  key = key_arr.shift
  hash[key] = {} unless hash[key].is_a?(Hash)
  key_arr.length > 0 ? hash_setter(hash[key], key_arr, val) : hash[key] = val
end
Ben G
  • 26,091
  • 34
  • 103
  • 170
-2
def set_value_for_keypath(initial, keypath, value)
    temp = initial

    for key in keypath.first(keypath.count - 1)
        temp = (temp[key] ||= {})
    end

    temp[keypath.last] = value

    return initial
end

initial = {:a => {:b => {:c => 'value' } } }

set_value_for_keypath(initial, [:a, :b, :h], 'value2')

initial

Or if you prefer something more unreadable:

def set_value_for_keypath(initial, keypath, value)
    keypath.first(keypath.count - 1).reduce(initial) { |hash, key| hash[key] ||= {} }[keypath.last] = value
end
Alexander
  • 59,041
  • 12
  • 98
  • 151