web-dev-qa-db-fra.com

Ruby: fusionne le hachage imbriqué

Je voudrais fusionner un hachage imbriqué.

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

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

J'aimerais que la fusion soit:

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

Quelle est la façon la plus simple de faire cela?

43
user1223862

Pour Rails 3.0.0+ ou version ultérieure, il existe la fonction deep_merge pour ActiveSupport qui fait exactement ce que vous demandez.

50
xlembouras

J'ai trouvé un algorithme de fusion profonde plus générique ici et je l'ai utilisé comme suit:

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)
43
Jon M

Pour ajouter aux réponses de Jon M et de koendc, le code ci-dessous gérera les fusions de hachages et: nil comme ci-dessus, mais il unira également les tableaux présents dans les deux hachages (avec la même clé):

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)
31
Dan

Pour des raisons de variété - et cela ne fonctionnera que si vous souhaitez fusionner toutes les clés de votre hachage de la même manière - vous pouvez le faire:

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

Lorsque vous passez un bloc à Hash#merge, k est la clé en cours de fusion. La clé existe dans a et b, x est la valeur de a[k] et y est celle de b[k]. Le résultat du bloc devient la valeur dans le hachage fusionné pour la clé k

Je pense cependant que dans votre cas particulier, la réponse de nkm est meilleure.

10
Russell

Un peu tard pour répondre à votre question, mais j’ai écrit un utilitaire de fusion profonde assez riche qui est maintenant maintenu par Daniel Deleo sur Github: https://github.com/danielsdeleo/deep_merge

Il va fusionner vos tableaux exactement comme vous le souhaitez. Du premier exemple dans la documentation:

Donc, si vous avez deux hachages comme ceci:

   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}

Il ne fusionnera pas: y (car int et array ne sont pas considérés comme pouvant être fusionnés) - l'utilisation de la syntaxe bang (!) Provoque l'écrasement de la source. L'utilisation de la méthode non-bang laissera les valeurs internes de dest uniquement lorsqu'une entité non fusionnable est a trouvé. Il ajoutera les tableaux contenus dans: x ensemble car il sait comment fusionner des tableaux. Il gère la fusion arbitraire et profonde de hachages contenant toutes les structures de données.

Beaucoup plus de documents sur le repo github de Daniel maintenant ..

6
Steve Midgley

Toutes les réponses me paraissent trop compliquées. Voici ce que je suis finalement venu avec:

# @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. utiliser comme licence publique, WTFPL ou autre

3
akostadinov

Voici une solution encore meilleure pour fusion récursive qui utilise raffinements et a méthode bang avec prise en charge de blocs. Ce code fonctionne sur pur 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

Une fois que using HashRecursive a été exécuté, vous pouvez utiliser les valeurs par défaut Hash::merge et Hash::merge! comme si elles n’avaient pas été modifiées. Vous pouvez utiliser blocs avec ces méthodes comme auparavant.

La nouveauté est que vous pouvez passer la valeur booléenne recursive (deuxième argument) à ces méthodes modifiées, qui fusionneront les hachages de manière récursive.


Exemple pour une utilisation simple est écrit à cette réponse . Voici un exemple avancé.

L'exemple de cette question est mauvais car cela n'a rien à voir avec la fusion récursive. La ligne suivante correspondrait à l'exemple de la question:

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

Permettez-moi de vous donner un meilleur exemple pour montrer la puissance du code ci-dessus. Imaginez deux salles, chacune ayant une étagère. Il y a 3 rangées sur chaque étagère et chaque étagère a actuellement 2 livres. Code:

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"
            }
        ]
    }
}

Nous allons déplacer les livres de l’étagère de la deuxième pièce vers les mêmes rangées sur l’étagère de la première pièce. Tout d’abord, nous allons le faire sans définir l’indicateur recursive, c’est-à-dire comme si vous utilisiez Hash::merge! non modifié:

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

La sortie nous dira que l'étagère dans la première pièce ressemblerait à ceci:

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

Comme vous pouvez le constater, le fait de ne pas avoir recursive nous a obligés à jeter nos précieux livres.

Nous allons maintenant faire la même chose, mais avec recursive flag mis à true. Vous pouvez passer comme second argument soit recursive=true, soit juste true:

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

Maintenant, la sortie nous dira que nous avons réellement déplacé nos livres:

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"
            }
        ]
    }
}

Cette dernière exécution pourrait être réécrite comme suit:

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

ou

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

C'est tout. Regardez aussi ma version récursive de Hash::each (Hash::each_pair) ici .

1
MOPO3OB

Je pense que la réponse de Jon M est la meilleure, mais elle échoue lorsque vous fusionnez dans un hachage avec une valeur nulle/indéfinie . Cette mise à jour résout le problème:

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
koendc