Safely assign value to nested hash using Hash#dig or Lonely operator(&.)
Asked Answered
A

7

27
h = {
  data: {
    user: {
      value: "John Doe" 
    }
  }
}

To assign value to the nested hash, we can use

h[:data][:user][:value] = "Bob"

However if any part in the middle is missing, it will cause error.

Something like

h.dig(:data, :user, :value) = "Bob"

won't work, since there's no Hash#dig= available yet.

To safely assign value, we can do

h.dig(:data, :user)&.[]=(:value, "Bob")    # or equivalently
h.dig(:data, :user)&.store(:value, "Bob")

But is there better way to do that?

Arsonist answered 5/1, 2016 at 20:16 Comment(4)
For what it's worth, this has been discussed here (and rejected by Matz (for the time being): bugs.ruby-lang.org/issues/11747Starrstarred
@JordanRunning and yet it works in Ruby 2.5!Gruelling
@MikeSzyndel What, specifically, works in Ruby 2.5? There's still no Hash/Array#bury or equivalent method.Starrstarred
I just successfully used h.dig(:data, :user)&.store(:value, "Bob") to edit a complicated Hash (parsed JSON file). No bury, but this method is clean and useful enough for my liking :)Gruelling
S
16

It's not without its caveats (and doesn't work if you're receiving the hash from elsewhere), but a common solution is this:

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

hash[:data][:user][:value] = "Bob"
p hash
# => { :data => { :user => { :value => "Bob" } } }
Starrstarred answered 5/1, 2016 at 20:53 Comment(3)
Unfortunately, the hash was from somewhere else. But thanks for the snippet!Arsonist
You can also set the default proc for existing hashes: hsh.default_proc = proc { |h,k| h[k] = Hash.new(&h.default_proc) }Tropine
be careful of using this when caching the hash value using Rails.cache.write, it will throw out the error: "can't dump hash with default proc"Shadwell
L
8

And building on @rellampec's answer, ones that does not throw errors:

def dig_set(obj, keys, value)
  key = keys.first
  if keys.length == 1
    obj[key] = value
  else
    obj[key] = {} unless obj[key]
    dig_set(obj[key], keys.slice(1..-1), value)
  end
end

obj = {d: 'hey'}
dig_set(obj, [:a, :b, :c], 'val')
obj #=> {d: 'hey', a: {b: {c: 'val'}}} 
Lustihood answered 24/4, 2020 at 12:33 Comment(0)
D
3

interesting one:

def dig_set(obj, keys, value)
  if keys.length == 1
    obj[keys.first] = value
  else
    dig_set(obj[keys.first], keys.slice(1..-1), value)
  end
end

will raise an exception anyways if there's no [] or []= methods.

Denmark answered 28/4, 2019 at 5:7 Comment(0)
D
1

I found a simple solution to set the value of a nested hash, even if a parent key is missing, even if the hash already exists. Given:

x = { gojira: { guitar: { joe: 'charvel' } } }

Suppose you wanted to include mario's drum to result in:

x = { gojira: { guitar: { joe: 'charvel' }, drum: { mario: 'tama' } } }

I ended up monkey-patching Hash:

class Hash

    # ensures nested hash from keys, and sets final key to value
    # keys: Array of Symbol|String
    # value: any
    def nested_set(keys, value)
      raise "DEBUG: nested_set keys must be an Array" unless keys.is_a?(Array)

      final_key = keys.pop
      return unless valid_key?(final_key)
      position = self
      for key in keys
        return unless valid_key?(key)
        position[key] = {} unless position[key].is_a?(Hash)
        position = position[key]
      end
      position[final_key] = value
    end

    private

      # returns true if key is valid
      def valid_key?(key)
        return true if key.is_a?(Symbol) || key.is_a?(String)
        raise "DEBUG: nested_set invalid key: #{key} (#{key.class})"
      end
end

usage:

x.nested_set([:instrument, :drum, :mario], 'tama')

usage for your example:

h.nested_set([:data, :user, :value], 'Bob')

any caveats i missed? any better way to write the code without sacrificing readability?

Draughty answered 10/1, 2019 at 6:2 Comment(0)
U
0

Searching for an answer to a similar question I developmentally stumbled upon an interface similar to @niels-kristian's answer, but wanted to also support a namespace definition parameter, like an xpath.

def deep_merge(memo, source)
  # From: http://www.ruby-forum.com/topic/142809
  # Author: Stefan Rusterholz
  merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
  memo.merge!(source, &merger)
end

# Like Hash#dig, but for setting a value at an xpath
def bury(memo, xpath, value, delimiter=%r{\.})
  xpath = xpath.split(delimiter) if xpath.respond_to?(:split)
  xpath.map!{|x|x.to_s.to_sym}.push(value)
  deep_merge(memo, xpath.reverse.inject { |memo, field| {field.to_sym => memo} })
end

Nested hashes are sort of like xpaths, and the opposite of dig is bury.

irb(main):014:0> memo = {:test=>"value"}
=> {:test=>"value"}
irb(main):015:0> bury(memo, 'test.this.long.path', 'value')
=> {:test=>{:this=>{:long=>{:path=>"value"}}}}
irb(main):016:0> bury(memo, [:test, 'this', 2, 4.0], 'value')
=> {:test=>{:this=>{:long=>{:path=>"value"}, :"2"=>{:"4.0"=>"value"}}}}
irb(main):017:0> bury(memo, 'test.this.long.path.even.longer', 'value')
=> {:test=>{:this=>{:long=>{:path=>{:even=>{:longer=>"value"}}}, :"2"=>{:"4.0"=>"value"}}}}
irb(main):018:0> bury(memo, 'test.this.long.other.even.longer', 'other')
=> {:test=>{:this=>{:long=>{:path=>{:even=>{:longer=>"value"}}, :other=>{:even=>{:longer=>"other"}}}, :"2"=>{:"4.0"=>"value"}}}}
Unwish answered 5/5, 2021 at 3:36 Comment(0)
T
0

A more ruby-helper-like version of @niels-kristian answer

You can use it like:

a = {}
a.bury!([:a, :b], "foo")
a # => {:a => { :b => "foo" }}
class Hash
  def bury!(keys, value)
    key = keys.first
    if keys.length == 1
      self[key] = value
    else
      self[key] = {} unless self[key]
      self[key].bury!(keys.slice(1..-1), value)
    end
    self
  end
end
Theophany answered 28/1, 2022 at 8:7 Comment(0)
S
0

you can use merge!

[37] pry(main)> h = {
  data: {
    user: {
      value: "John Doe"
    }
  }
=> {:data=>{:user=>{:value=>"John Doe"}}}
[38] pry(main)> h.merge!(data: {user: {value: "John Foo"}})
=> {:data=>{:user=>{:value=>"John Foo"}}}
[39] pry(main)> h
=> {:data=>{:user=>{:value=>"John Foo"}}}
[40] pry(main)> h.merge!(data: nil)
=> {:data=>nil}
[41] pry(main)> h
=> {:data=>nil}
[42] pry(main)> h.merge!(data: {user: {value: "John Foo2"}})
=> {:data=>{:user=>{:value=>"John Foo2"}}}
Scarcity answered 10/3, 2023 at 16:59 Comment(1)
You should give an example with 2 nested keys so as to show the difference between merge and deep_merge. Note that deep_merge is provided by Rails.Neall

© 2022 - 2024 — McMap. All rights reserved.