Ruby: объединить вложенный хэш - PullRequest
46 голосов
/ 21 февраля 2012

Я хотел бы объединить вложенный хэш.

a = {:book=>
    [{:title=>"Hamlet",
      :author=>"William Shakespeare"
      }]}

b = {:book=>
    [{:title=>"Pride and Prejudice",
      :author=>"Jane Austen"
      }]}

Я бы хотел, чтобы слияние было:

{:book=>
   [{:title=>"Hamlet",
      :author=>"William Shakespeare"},
    {:title=>"Pride and Prejudice",
      :author=>"Jane Austen"}]}

Как лучше всего это сделать?

Ответы [ 9 ]

51 голосов
/ 25 сентября 2012

Для rails 3.0.0+ или более поздней версии есть функция deep_merge для ActiveSupport , которая делает именно то, что вы просите.

46 голосов
/ 21 февраля 2012

Я нашел более общий алгоритм глубокого слияния здесь и использовал его так:

class ::Hash
    def deep_merge(second)
        merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
        self.merge(second, &merger)
    end
end

a.deep_merge(b)
33 голосов
/ 14 мая 2015

Чтобы добавить ответы Jon M и koendc, приведенный ниже код будет обрабатывать слияния хэшей, и: nil, как указано выше, но он также объединяет любые массивы, присутствующие в обоих хешах (с одним и тем же ключом):

class ::Hash
    def deep_merge(second)
        merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
        self.merge(second.to_h, &merger)
    end
end

a.deep_merge(b)
10 голосов
/ 21 февраля 2012

Для разнообразия - и это будет работать только в том случае, если вы хотите объединить все ключи в вашем хэше одинаково - вы можете сделать это:

a.merge(b) { |k, x, y| x + y }

Когда вы передаете блок в Hash#merge, k - это объединяемый ключ, где ключ существует в a и b, x - это значение a[k], а y - это значение b[k].Результатом блока становится значение в объединенном хэше для ключа k.

Я думаю, что в вашем конкретном случае ответ nkm лучше.

6 голосов
/ 12 августа 2013

Немного поздно, чтобы ответить на ваш вопрос, но я недавно написал довольно богатую утилиту глубокого слияния, которая теперь поддерживается Дэниелом Делео на Github: https://github.com/danielsdeleo/deep_merge

Она объединит ваши массивы именно так, как вы хотите,Из первого примера в документах:

Итак, если у вас есть два хеша, подобные этому:

   source = {:x => [1,2,3], :y => 2}
   dest =   {:x => [4,5,'6'], :y => [7,8,9]}
   dest.deep_merge!(source)
   Results: {:x => [1,2,3,4,5,'6'], :y => 2}

Он не будет сливаться: y (потому что int и массив не считаются объединяемыми)- использование синтаксиса bang (!) приводит к перезаписи источника. При использовании метода non-bang внутренние значения dest оставляются в покое, когда обнаруживается необратимая сущность.Он сложит вместе массивы, содержащиеся в: x, потому что знает, как объединять массивы.Он обрабатывает произвольно глубокое объединение хэшей, содержащих любые структуры данных.

Множество других документов по репозиторию Daniel github сейчас ..

4 голосов
/ 04 мая 2015

Все ответы выглядят для меня слишком сложными. Вот что я в итоге придумал:

# @param tgt [Hash] target hash that we will be **altering**
# @param src [Hash] read from this source hash
# @return the modified target hash
# @note this one does not merge Arrays
def self.deep_merge!(tgt_hash, src_hash)
  tgt_hash.merge!(src_hash) { |key, oldval, newval|
    if oldval.kind_of?(Hash) && newval.kind_of?(Hash)
      deep_merge!(oldval, newval)
    else
      newval
    end
  }
end

P.S. использовать как публичную, WTFPL или любую другую лицензию

1 голос
/ 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 (второй аргумент) этим модифицированным методам, и они будут рекурсивно объединять хэши.


Пример для простого использования записан на этот ответ . Вот расширенный пример.

Пример в этом вопросе плох, потому что он не имеет ничего общего с рекурсивным слиянием. Следующая строка соответствует примеру вопроса:

a.merge!(b) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}

Позвольте мне привести вам лучший пример, чтобы показать силу кода выше. Представьте себе две комнаты, в каждой из которых есть одна книжная полка. На каждой книжной полке 3 ряда, и в настоящее время на каждой книжной полке 2 книги. Код:

room1   =   {
    :shelf  =>  {
        :row1   =>  [
            {
                :title  =>  "Hamlet",
                :author =>  "William Shakespeare"
            }
        ],
        :row2   =>  [
            {
                :title  =>  "Pride and Prejudice",
                :author =>  "Jane Austen"
            }
        ]
    }
}

room2   =   {
    :shelf  =>  {
        :row2   =>  [
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}

Мы собираемся переместить книги с полки во второй комнате в те же ряды на полке в первой комнате. Сначала мы сделаем это без установки флага recursive, то есть так же, как с использованием немодифицированного Hash::merge!:

room1.merge!(room2) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
puts room1

Вывод скажет нам, что полка в первой комнате будет выглядеть так:

room1   =   {
    :shelf  =>  {
        :row2   =>  [
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}

Как видите, отсутствие recursive заставило нас выбросить наши драгоценные книги.

Теперь мы сделаем то же самое, но с установкой флага recursive на true . Вы можете передать в качестве второго аргумента либо recursive=true, либо просто true:

room1.merge!(room2, true) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
puts room1

Теперь вывод скажет нам, что мы на самом деле переместили наши книги:

room1   =   {
    :shelf  =>  {
        :row1   =>  [
            {
                :title  =>  "Hamlet",
                :author =>  "William Shakespeare"
            }
        ],
        :row2   =>  [
            {
                :title  =>  "Pride and Prejudice",
                :author =>  "Jane Austen"
            },
            {
                :title  =>  "The Great Gatsby",
                :author =>  "F. Scott Fitzgerald"
            }
        ],
        :row3   =>  [
            {
                :title  =>  "Catastrophe Theory",
                :author =>  "V. I. Arnol'd"
            }
        ]
    }
}

Это последнее исполнение можно переписать следующим образом:

room1 = room1.merge(room2, recursive=true) do |k, v1, v2|
    if v1.is_a?(Array) && v2.is_a?(Array)
        v1+v2
    else
        v2
    end
end
puts room1

или

block = Proc.new {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
room1.merge!(room2, recursive=true, &block)
puts room1

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

0 голосов
/ 23 сентября 2014

Я думаю, что ответ Джона М - лучший, но он терпит неудачу, когда вы объединяете хеш с нулевым / неопределенным значением.Это обновление решает проблему:

class ::Hash
    def deep_merge(second)
        merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
        self.merge(second, &merger)
    end
end

a.deep_merge(b)
0 голосов
/ 21 февраля 2012
a[:book] = a[:book] + b[:book]

Или

a[:book] <<  b[:book].first
...