Безопасно присвойте значение вложенному хешу, используя Hash#dig или оператор Lonely (&.)
h = {
data: {
user: {
value: "John Doe"
}
}
}
Чтобы присвоить значение вложенному хешу, мы можем использовать
h[:data][:user][:value] = "Bob"
Однако, если какая-либо часть в середине отсутствует, это вызовет ошибку.
Что-то вроде
h.dig(:data, :user, :value) = "Bob"
не будет работать, так как нет Hash#dig=
доступно еще.
Чтобы безопасно присвоить значение, мы можем сделать
h.dig(:data, :user)&.[]=(:value, "Bob") # or equivalently
h.dig(:data, :user)&.store(:value, "Bob")
Но есть ли лучший способ сделать это?
7 ответов
Это не без его предостережений (и не работает, если вы получаете хэш из другого места), но общее решение таково:
hash = Hash.new {|h,k| h[k] = h.class.new(&h.default_proc) }
hash[:data][:user][:value] = "Bob"
p hash
# => { :data => { :user => { :value => "Bob" } } }
И, основываясь на ответе @rellampec, который не вызывает ошибок:
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'}}}
Интересный :
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
в любом случае возникнет исключение, если нет []
или же []=
методы.
Я нашел простое решение для установки значения вложенного хэша, даже если родительский ключ отсутствует, даже если хэш уже существует. Дано:
x = { gojira: { guitar: { joe: 'charvel' } } }
Предположим, вы хотели включить барабан Марио, чтобы получить:
x = { gojira: { guitar: { joe: 'charvel' }, drum: { mario: 'tama' } } }
Я закончил тем, что исправлял хэш:
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
использование:
x.nested_set([:instrument, :drum, :mario], 'tama')
использование для вашего примера:
h.nested_set([:data, :user, :value], 'Bob')
какие-то предостережения, которые я пропустил? Есть ли лучший способ написать код без ущерба для читаемости?
вы можете использовать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"}}}
В поисках ответа на аналогичный вопрос я случайно наткнулся на интерфейс, похожий на ответ @niels-kristian, но хотел также поддерживать параметр определения пространства имен, например 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
Вложенные хэши похожи на xpath и противоположны
dig
является
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"}}}}
Более похожая на ruby-helper версия ответа @niels-kristian
Вы можете использовать его как:
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