Слияние многомерных хэшей в Ruby
У меня есть два хэша, которые имеют структуру, похожую на эту:
hash_a = { :a => { :b => { :c => "d" } } }
hash_b = { :a => { :b => { :x => "y" } } }
Я хочу объединить их вместе, чтобы получить следующий хеш:
{ :a => { :b => { :c => "d", :x => "y" } } }
Функция слияния заменит значение: a в первом хеше на значение: a во втором хеше. Итак, я написал свою собственную функцию рекурсивного слияния, которая выглядит так:
def recursive_merge( merge_from, merge_to )
merged_hash = merge_to
first_key = merge_from.keys[0]
if merge_to.has_key?(first_key)
merged_hash[first_key] = recursive_merge( merge_from[first_key], merge_to[first_key] )
else
merged_hash[first_key] = merge_from[first_key]
end
merged_hash
end
Но я получаю ошибку во время выполнения: can't add a new key into hash during iteration
, Как лучше всего объединить эти хэши в Ruby?
6 ответов
Если вы измените первую строку recursive_merge на
merged_hash = merge_to.clone
работает как положено:
recursive_merge(hash_a, hash_b)
-> {:a=>{:b=>{:c=>"d", :x=>"y"}}}
Изменение хэша при его перемещении проблематично, вам нужна "рабочая область" для накопления ваших результатов.
Руби существует Hash#merge
позволяет блочную форму для разрешения дубликатов, делая это довольно просто. Я добавил функциональность для объединения нескольких конфликтующих значений на "листьях" вашего дерева в массив; Вы можете выбрать один или другой вместо этого.
hash_a = { :a => { :b => { :c => "d", :z => 'foo' } } }
hash_b = { :a => { :b => { :x => "y", :z => 'bar' } } }
def recurse_merge(a,b)
a.merge(b) do |_,x,y|
(x.is_a?(Hash) && y.is_a?(Hash)) ? recurse_merge(x,y) : [*x,*y]
end
end
p recurse_merge( hash_a, hash_b )
#=> {:a=>{:b=>{:c=>"d", :z=>["foo", "bar"], :x=>"y"}}}
Или, как чистая обезьяна-патч:
class Hash
def merge_recursive(o)
merge(o) do |_,x,y|
if x.respond_to?(:merge_recursive) && y.is_a?(Hash)
x.merge_recursive(y)
else
[*x,*y]
end
end
end
end
p hash_a.merge_recursive hash_b
#=> {:a=>{:b=>{:c=>"d", :z=>["foo", "bar"], :x=>"y"}}}
Вы можете сделать это в одну строку:
merged_hash = hash_a.merge(hash_b){|k,hha,hhb| hha.merge(hhb){|l,hhha,hhhb| hhha.merge(hhhb)}}
Если вы хотите немедленно merge
результат в hash_a, просто замените метод слияния на метод merge!
Если вы используете рельсы 3 или рельсы 4, это еще проще:
merged_hash = hash_a.deep_merge(hash_b)
или же
hash_a.deep_merge!(hash_b)
Попробуйте это решение для исправления обезьян:
class Hash
def recursive_merge(hash = nil)
return self unless hash.is_a?(Hash)
base = self
hash.each do |key, v|
if base[key].is_a?(Hash) && hash[key].is_a?(Hash)
base[key].recursive_merge(hash[key])
else
base[key]= hash[key]
end
end
base
end
end
Чтобы объединить одно с другим, как это было предложено в заявке, вы можете изменить функцию @Phrogz
def recurse_merge( merge_from, merge_to )
merge_from.merge(merge_to) do |_,x,y|
(x.is_a?(Hash) && y.is_a?(Hash)) ? recurse_merge(x,y) : x
end
end
Если есть дубликат ключа, он будет использовать только содержимое merge_from
гашиш
Вот еще лучшее решение для рекурсивного слияния, которое использует уточнения и имеет метод взрыва наряду с поддержкой блоков. Этот код работает на чистом Ruby.
module HashRecursive
refine Hash do
def merge(other_hash, recursive=false, &block)
if recursive
block_actual = Proc.new {|key, oldval, newval|
newval = block.call(key, oldval, newval) if block_given?
[oldval, newval].all? {|v| v.is_a?(Hash)} ? oldval.merge(newval, &block_actual) : newval
}
self.merge(other_hash, &block_actual)
else
super(other_hash, &block)
end
end
def merge!(other_hash, recursive=false, &block)
if recursive
self.replace(self.merge(other_hash, recursive, &block))
else
super(other_hash, &block)
end
end
end
end
using HashRecursive
После using HashRecursive
был выполнен, вы можете использовать по умолчанию Hash::merge
а также Hash::merge!
как будто они не были изменены. Вы можете использовать блоки с этими методами, как и раньше.
Новым является то, что вы можете передать логическое значение recursive
(второй аргумент) к этим модифицированным методам, и они будут рекурсивно объединять хэши.
Пример использования для ответа на вопрос. Это очень легко:
hash_a = { :a => { :b => { :c => "d" } } }
hash_b = { :a => { :b => { :x => "y" } } }
puts hash_a.merge(hash_b) # Won't override hash_a
# output: { :a => { :b => { :x => "y" } } }
puts hash_a # hash_a is unchanged
# output: { :a => { :b => { :c => "d" } } }
hash_a.merge!(hash_b, recursive=true) # Will override hash_a
puts hash_a # hash_a was changed
# output: { :a => { :b => { :c => "d", :x => "y" } } }
Для расширенного примера взгляните на этот ответ.
Также взгляните на мою рекурсивную версию Hash::each
(Hash::each_pair
) здесь.