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?
Pour Rails 3.0.0+ ou version ultérieure, il existe la fonction deep_merge pour ActiveSupport qui fait exactement ce que vous demandez.
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)
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)
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.
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 ..
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
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 .
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)