44 votes

Comment fusionner un tableau de hachages pour obtenir un hachage de tableaux de valeurs ?

C'est le contraire de Transformer un hachage de tableaux en un tableau de hachages en Ruby .

Transformez de manière élégante et/ou efficace un tableau de hachages en un hachage où les valeurs sont des tableaux de toutes les valeurs :

hs = [
  { a:1, b:2 },
  { a:3, c:4 },
  { b:5, d:6 }
]
collect_values( hs )
#=> { :a=>[1,3], :b=>[2,5], :c=>[4], :d=>[6] }

Ce code laconique fonctionne presque, mais ne parvient pas à créer un tableau lorsqu'il n'y a pas de doublons :

def collect_values( hashes )
  hashes.inject({}){ |a,b| a.merge(b){ |_,x,y| [*x,*y] } }
end
collect_values( hs )
#=> { :a=>[1,3], :b=>[2,5], :c=>4, :d=>6 }

Ce code fonctionne, mais pouvez-vous écrire une meilleure version ?

def collect_values( hashes )
  # Requires Ruby 1.8.7+ for Object#tap
  Hash.new{ |h,k| h[k]=[] }.tap do |result|
    hashes.each{ |h| h.each{ |k,v| result[k]<<v } }
  end
end

Les solutions qui ne fonctionnent qu'avec Ruby 1.9 sont acceptables, mais doivent être signalées comme telles.


Voici les résultats de l'évaluation des différentes réponses ci-dessous (et quelques autres de mon cru), en utilisant trois tableaux de hachages différents :

  • une où chaque hachage a des clés distinctes, de sorte qu'aucune fusion ne se produit jamais :
    [{:a=>1}, {:b=>2}, {:c=>3}, {:d=>4}, {:e=>5}, {:f=>6}, {:g=>7}, ...]

  • une où chaque hachage a la même clé, de sorte que la fusion est maximale :
    [{:a=>1}, {:a=>2}, {:a=>3}, {:a=>4}, {:a=>5}, {:a=>6}, {:a=>7}, ...]

  • et une autre qui est un mélange de clés uniques et partagées :
    [{:c=>1}, {:d=>1}, {:c=>2}, {:f=>1}, {:c=>1, :d=>1}, {:h=>1}, {:c=>3}, ...]

               user     system      total        real

    Phrogz 2a 0.577000 0.000000 0.577000 ( 0.576000) Phrogz 2b 0.624000 0.000000 0.624000 ( 0.620000) Glenn 1 0.640000 0.000000 0.640000 ( 0.641000) Phrogz 1 0.671000 0.000000 0.671000 ( 0.668000) Michael 1 0.702000 0.000000 0.702000 ( 0.700000) Michael 2 0.717000 0.000000 0.717000 ( 0.726000) Glenn 2 0.765000 0.000000 0.765000 ( 0.764000) fl00r 0.827000 0.000000 0.827000 ( 0.836000) sawa 0.874000 0.000000 0.874000 ( 0.868000) Tokland 1 0.873000 0.000000 0.873000 ( 0.876000) Tokland 2 1.077000 0.000000 1.077000 ( 1.073000) Phrogz 3 2.106000 0.093000 2.199000 ( 2.209000)

Le code le plus rapide est cette méthode que j'ai ajoutée :

def collect_values(hashes)
  {}.tap{ |r| hashes.each{ |h| h.each{ |k,v| (r[k]||=[]) << v } } }
end

J'ai accepté " réponse de glenn mcdonald "car il était compétitif en termes de vitesse, raisonnablement concis, mais (surtout) parce qu'il soulignait le danger d'utiliser un Hash avec une proc par défaut auto-modifiante pour une construction pratique, car cela peut introduire de mauvaises modifications lorsque l'utilisateur l'indexe plus tard.

Enfin, voici le code de référence, au cas où vous voudriez effectuer vos propres comparaisons :

require 'prime'   # To generate the third hash
require 'facets'  # For tokland1's map_by
AZSYMBOLS = (:a..:z).to_a
TESTS = {
  '26 Distinct Hashes'   => AZSYMBOLS.zip(1..26).map{|a| Hash[*a] },
  '26 Same-Key Hashes'   => ([:a]*26).zip(1..26).map{|a| Hash[*a] },
  '26 Mixed-Keys Hashes' => (2..27).map do |i|
    factors = i.prime_division.transpose
    Hash[AZSYMBOLS.values_at(*factors.first).zip(factors.last)]
  end
}

def phrogz1(hashes)
  Hash.new{ |h,k| h[k]=[] }.tap do |result|
    hashes.each{ |h| h.each{ |k,v| result[k]<<v } }
  end
end
def phrogz2a(hashes)
  {}.tap{ |r| hashes.each{ |h| h.each{ |k,v| (r[k]||=[]) << v } } }
end
def phrogz2b(hashes)
  hashes.each_with_object({}){ |h,r| h.each{ |k,v| (r[k]||=[]) << v } }
end
def phrogz3(hashes)
  result = hashes.inject({}){ |a,b| a.merge(b){ |_,x,y| [*x,*y] } }
  result.each{ |k,v| result[k] = [v] unless v.is_a? Array }
end
def glenn1(hs)
  hs.reduce({}) {|h,pairs| pairs.each {|k,v| (h[k] ||= []) << v}; h}
end
def glenn2(hs)
  hs.map(&:to_a).flatten(1).reduce({}) {|h,(k,v)| (h[k] ||= []) << v; h}
end
def fl00r(hs)
  h = Hash.new{|h,k| h[k]=[]}
  hs.map(&:to_a).flatten(1).each{|v| h[v[0]] << v[1]}
  h
end
def sawa(a)
  a.map(&:to_a).flatten(1).group_by{|k,v| k}.each_value{|v| v.map!{|k,v| v}}
end
def michael1(hashes)
  h = Hash.new{|h,k| h[k]=[]}
  hashes.each_with_object(h) do |h, result|
    h.each{ |k, v| result[k] << v }
  end
end
def michael2(hashes)
  h = Hash.new{|h,k| h[k]=[]}
  hashes.inject(h) do |result, h|
    h.each{ |k, v| result[k] << v }
    result
  end
end
def tokland1(hs)
  hs.map(&:to_a).flatten(1).map_by{ |k, v| [k, v] }
end
def tokland2(hs)
  Hash[hs.map(&:to_a).flatten(1).group_by(&:first).map{ |k, vs|
    [k, vs.map{|o|o[1]}]
  }]
end

require 'benchmark'
N = 10_000
Benchmark.bm do |x|
  x.report('Phrogz 2a'){ TESTS.each{ |n,h| N.times{ phrogz2a(h) } } }
  x.report('Phrogz 2b'){ TESTS.each{ |n,h| N.times{ phrogz2b(h) } } }
  x.report('Glenn 1  '){ TESTS.each{ |n,h| N.times{ glenn1(h)   } } }
  x.report('Phrogz 1 '){ TESTS.each{ |n,h| N.times{ phrogz1(h)  } } }
  x.report('Michael 1'){ TESTS.each{ |n,h| N.times{ michael1(h) } } }
  x.report('Michael 2'){ TESTS.each{ |n,h| N.times{ michael2(h) } } }
  x.report('Glenn 2  '){ TESTS.each{ |n,h| N.times{ glenn2(h)   } } }
  x.report('fl00r    '){ TESTS.each{ |n,h| N.times{ fl00r(h)    } } }
  x.report('sawa     '){ TESTS.each{ |n,h| N.times{ sawa(h)     } } }
  x.report('Tokland 1'){ TESTS.each{ |n,h| N.times{ tokland1(h) } } }
  x.report('Tokland 2'){ TESTS.each{ |n,h| N.times{ tokland2(h) } } }
  x.report('Phrogz 3 '){ TESTS.each{ |n,h| N.times{ phrogz3(h)  } } }

end

20voto

glenn mcdonald Points 8933

Faites votre choix :

hs.reduce({}) {|h,pairs| pairs.each {|k,v| (h[k] ||= []) << v}; h}

hs.map(&:to_a).flatten(1).reduce({}) {|h,(k,v)| (h[k] ||= []) << v; h}

Je suis fermement opposé à l'idée de modifier les valeurs par défaut pour les hachages, comme le font les autres suggestions, car alors vérification de pour une valeur modifie le hash, ce qui me semble très incorrect.

3voto

fl00r Points 41855
h = Hash.new{|h,k| h[k]=[]}
hs.map(&:to_a).flatten(1).each{|v| h[v[0]] << v[1]}

1voto

Michael Kohl Points 33345

Qu'est-ce que c'est ?

def collect_values(hashes)
  h = Hash.new{|h,k| h[k]=[]}
  hashes.each_with_object(h) do |h, result|
    h.each{ |k, v| result[k] << v }
  end
end

Edit - Egalement possible avec l'injection, mais IMHO pas aussi agréable :

def collect_values( hashes )
  h = Hash.new{|h,k| h[k]=[]}
  hashes.inject(h) do |result, h|
    h.each{ |k, v| result[k] << v }
    result
  end
end

1voto

sawa Points 62592

Idem pour d'autres réponses utilisant map(&:to_a).flatten(1) . Le problème est de savoir comment modifier les valeurs du hachage. J'ai utilisé le fait que les tableaux sont mutables.

def collect_values a
  a.map(&:to_a).flatten(1).group_by{|k, v| k}.
  each_value{|v| v.map!{|k, v| v}}
end

1voto

tokland Points 29813

Facet's Enumérable#map_by est très utile dans ces cas-là. Cette implémentation sera sans doute plus lente que les autres, mais un code modulaire et compact est toujours plus facile à maintenir :

require 'facets'
hs.flat_map(&:to_a).map_by { |k, v| [k, v] }
#=> {:b=>[2, 5], :d=>[6], :c=>[4], :a=>[1, 3]

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X