Слияние многомерных хэшей в Ruby - PullRequest
6 голосов
/ 07 апреля 2011

У меня есть два хэша, которые имеют структуру, похожую на эту:

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 ]

10 голосов
/ 07 апреля 2011

Существующий в Ruby 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"}}}
8 голосов
/ 01 декабря 2014

Вы можете сделать это в одну строку:

merged_hash = hash_a.merge(hash_b){|k,hha,hhb| hha.merge(hhb){|l,hhha,hhhb| hhha.merge(hhhb)}}

Если вы хотите немедленно merge получить результат в hash_a, просто замените метод merge на метод merge!

Если вы используете рельсы 3 или рельсы 4 рамки, это еще проще:

merged_hash = hash_a.deep_merge(hash_b)

или

hash_a.deep_merge!(hash_b)
7 голосов
/ 07 апреля 2011

Если вы измените первую строку recursive_merge на

merged_hash = merge_to.clone

работает как положено:

recursive_merge(hash_a, hash_b)    
->    {:a=>{:b=>{:c=>"d", :x=>"y"}}}

Изменение хэша при его перемещении затруднительно, вам нужна «рабочая область» для накопления результатов.

2 голосов
/ 07 апреля 2011

Попробуйте это решение для исправления обезьян:

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
0 голосов
/ 07 марта 2018

Вот еще лучшее решение для рекурсивного слияния , которое использует уточнения и имеет метод взрыва наряду с поддержкой блоков .Этот код работает на pure 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!, как если бы они не были изменены.Вы можете использовать blocks с этими методами, как и раньше.

Новым является то, что вы можете передать логический 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" } } }

Для примера advanced посмотрите этот ответ .

Также взгляните на мою рекурсивную версиюHash::each (Hash::each_pair) здесь .

0 голосов
/ 14 июня 2017

Чтобы объединить одно с другим, как предложено в заявке, вы можете изменить функцию @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 хеша

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...