Sebastian Peter
Sebastian Peter

Reputation: 195

Use an Array to access nested Hash keys

I have an Array of strings which represent a path of nested hash keys e.g.

["foo", "bar", "baz"]

I want to create a function which takes in the Array and a value and sets the value for the key provided in the Array, so when I call it with the example, it sets the value for

Hash["foo"]["bar"]["baz"].

Is there a method that can do that. Is there a way to chain the elements of an Array into calls for hash keys with #inject ?

Any help is highly appreciated.

To specify the question:

I have the following code:

def anonymize_data_hash(data, path=[])
  if data.is_a?(Hash)
    data.each do |key, value|
      anonymize_data_hash(value, path + [key])
    end
  elsif data.is_a?(Array)
    data.each do |value|
      anonymize_data_hash(value, path)
    end
  else
    path = path.clone
    key = path.shift
    path = (path + [data]).map(&:to_s)

    send("#{key}")[path] = "xxxxxx"
    save
  end
end

the anonymize_data_hash method sends an method(attribute) call to a mode which is a serialized hash. Path is an array of strings. In order make the above function work I need to turn the array of string into a nested Hash call.

The Hash already exists I need to access it with the values given in the Array. Thank You for Your help.

Upvotes: 0

Views: 2455

Answers (2)

wteuber
wteuber

Reputation: 1238

Simple Lime's solution requires a well-formatted hash to begin with. If addition to the hash depends on that, you highly depend on the input data and its order to determine if a value is added or not (e.g. "zzzzzz" in the example). You might want to (re-)define the hash's default_proc to create the structure of the hash on the fly. Here is an example:

def nest(array, value)
  hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
  hash.dig(*array[0..-2])[array.fetch(-1)] = value
  hash
end

nest ["foo", "bar", "baz"], :test
# => {"foo"=>{"bar"=>{"baz"=>:test}}}

Since the hash already exists, I suggest proper initialisation of its default_proc.

hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }

If that's not possible, you can still overwrite it later as well

hash = {}  # unchange initialisation 

hash.default_proc = -> (h, k) { h[k] = Hash.new(&h.default_proc) }

def nest(hash, array, value)
  hash.dig(*array[0..-2])[array.fetch(-1)] = value
  hash
end

nest hash, ["foo", "bar", "baz"], :test
# => {"foo"=>{"bar"=>{"baz"=>:test}}}

I hope you find this helpful too.

Upvotes: 2

Simple Lime
Simple Lime

Reputation: 11050

You could make use of the newer method Hash#dig (ruby 2.3+) to access the nested hash, and then set the value on that:

# ideally this might be a method on Hash, so you wouldn't need to pass it in
def deep_set(hash, path, value)
  *path, final_key = path
  to_set = path.empty? ? hash : hash.dig(*path)

  return unless to_set
  to_set[final_key] = value
end

hash = {
  "foo" => {
    "bar" => { }
  }
}

deep_set(hash, ["foo", "bar", "baz"], "xxxxxx")
deep_set(hash, ["baz"], "yyyyyy")
deep_set(hash, ["foo", "baz", "bar"], "zzzzzz")
puts hash
# => {"foo"=>{"bar"=>{"baz"=>"xxxxxx"}}, "baz"=>"yyyyyy"}

Upvotes: 9

Related Questions