xander-miller
xander-miller

Reputation: 529

How do I use an array of named indices (keys) to set a value in a nested hash?

Given any nested hash, for example:

{ canada: 
  { ontario: 
    { ottawa: :me}, 
    manitoba: 
    { winnipeg: nil}}, 
  united_states: 
  { district_of_coloumbia: 
    { washington: nil}}}

how can I use any array of keys [:canada, :ontario, :ottawa] or [:united_states, :district_of_columbia, :washington] to get or set a value.

Basically, my problem is how do I change [:canada, :ontario, :ottawa] into a getter or setter of the format hash[:canada][:ontario][:ottawa] when I don't know the length of the array of keys.

so I can do something like:

hash[:canada][:ontario][:ottawa] = nil
hash[:canada][:manitoba][:winnipeg] = :me

I made a getter using recursion:

def reindex(h, index_array)
  i = index_array.shift
  result = index_array.empty? ? h[i] : reindex(h[i], index_array)
  result
end

But I feel like I'm over thinking this and there should be a simpler way.

Upvotes: 4

Views: 117

Answers (5)

ptd
ptd

Reputation: 3053

I think recursion is your best option. I wouldn't consider it "overthinking" I'd do:

def getter(hash, array)
  return hash[array[0]] if array.count == 1
  getter(hash[array[0]], array[1..-1], item)
end

def setter(hash, array, item)
  return hash[array[0]] = item if array.count == 1
  setter(hash[array[0]], array[1..-1], item)
end

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110725

Yes, recursion is an option. Here's how it could be implemented.

Code

def get(hash, arr)
  case arr.size
  when 1 then hash[arr.first]
  else get(hash[arr.first], arr[1..-1])
  end
end

def set(hash, arr, val)
  case arr.size
  when 1 then hash[arr.first] = val 
  else set(hash[arr.first], arr[1..-1], val)
  end
end

Example

hash = {
  canada: {
    ontario: 
      { ottawa: :me }, 
    manitoba: 
      { winnipeg: nil }
  }, 
  united_states: {
    district_of_columbia: 
      { washington: nil }
  }
}

arr_can = [:canada, :ontario, :ottawa]
arr_us  = [:united_states, :district_of_columbia, :washington]

get(hash, arr_can) #=> :me
get(hash, arr_us)  #=> nil

set(hash, arr_can, 'cat')
set(hash, arr_us,  'dog')

hash
  # => {:canada=>{:ontario=> {:ottawa=>"cat"},
  #               :manitoba=>{:winnipeg=>nil}},
  #     :united_states=>
  #              {:district_of_columbia=>{:washington=>"dog"}}
  #    } 

Upvotes: 0

infiton
infiton

Reputation: 121

class Hash
  def deep_fetch(*path)
    path.reduce(self) do |mem, key|
      mem[key] if mem
    end
  end

  def deep_assign(*path, val)
    key = path.shift

    if path.empty?
      self[key] = val
    else
      if self[key].is_a?(Hash)
        self[key].deep_assign(*path, val)
      else
        self[key] = path.reverse.inject(val) { |a, n| {n => a} }
      end
    end

    self
  end
end

Upvotes: 2

Rustam Gasanov
Rustam Gasanov

Reputation: 15791

Much simpler approach(in my opinion) is to access elements successively with :[]:

keys = [:canada, :ontario, :ottawa]
hash = { canada: { ontario: { ottawa: :me}, manitoba: { winnipeg: nil} }, united_states: { district_of_coloumbia: { washington: nil } } }

# get
p keys.inject(hash) { |h, k| h.public_send(:[], k) }
#=> :me

# set
last = keys[0..-2].inject(hash) { |h, k| h.public_send(:[], k) }
last.public_send(:[]=, keys[-1], 'other')
p hash #=> {:canada=>{:ontario=>{:ottawa=>"other"}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}

Wrapped in methods:

def get_by_keys(hash, keys)
  keys.inject(hash) { |h, k| h.public_send(:[], k) }
end

def set_by_keys(hash, keys, v)
  last = keys[0..-2].inject(hash) { |h, k| h.public_send(:[], k) }
  last.public_send(:[]=, keys[-1], v)
  hash
end

keys = [:canada, :ontario, :ottawa]
hash = { canada: { ontario: { ottawa: :me}, manitoba: { winnipeg: nil} }, united_states: { district_of_coloumbia: { washington: nil } } }

p get_by_keys(hash, keys) #=> :me
p set_by_keys(hash, keys, 'other') #=> {:canada=>{:ontario=>{:ottawa=>"other"}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}

Upvotes: 2

Denis Odorcic
Denis Odorcic

Reputation: 11

countries = {:canada=>{:ontario=>{:ottawa=>:me}, :manitoba=>{:winnipeg=>nil}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}
hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
hash.merge!(countries)

hash[:canada][:ontario][:ottawa] = nil
hash[:canada][:manitoba][:winnipeg] = :me

hash
=> {:canada=>{:ontario=>{:ottawa=>nil}, :manitoba=>{:winnipeg=>:me}}, :united_states=>{:district_of_coloumbia=>{:washington=>nil}}}

Upvotes: 1

Related Questions