web-dev-qa-db-ja.com

ハッシュの配列をマージして値の配列のハッシュを取得する方法

これは 配列のハッシュをRubyのハッシュの配列に変換する の反対です。

エレガントかつ/または効率的にハッシュの配列をハッシュに変換します。値はすべての値の配列です。

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

この簡潔なコードはほとんど機能しますが、重複がない場合は配列の作成に失敗します。

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 }

このコードは機能しますが、より良いバージョンを書くことができますか?

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

Ruby 1.9でのみ機能するソリューションは許容されますが、そのように注意する必要があります。


以下は、ハッシュの3つの異なる配列を使用して、以下のさまざまな回答(および私の回答のいくつか)をベンチマークした結果です。

  • ハッシュごとに異なるキーがあるため、マージは発生しません。
    [{:a=>1}, {:b=>2}, {:c=>3}, {:d=>4}, {:e=>5}, {:f=>6}, {:g=>7}, ...]

  • すべてのハッシュが同じキーを持つため、最大のマージが発生します。
    [{:a=>1}, {:a=>2}, {:a=>3}, {:a=>4}, {:a=>5}, {:a=>6}, {:a=>7}, ...]

  • もう1つは、一意のキーと共有キーの組み合わせです。
    [{:c=>1}, {:d=>1}, {:c=>2}, {:f=>1}, {:c=>1, :d=>1}, {:h=>1}, {:c=>3}, ...]
ユーザーシステムの実際の合計
 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)

最速のコードは私が追加したこのメソッドです:

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

glenn mcdonald's answer 」を受け入れました。速度の点で競争力があり、合理的に簡潔ですが、(最も重要なのは)自己変更デフォルトプロシージャでハッシュを使用することの危険性を指摘したためです。これは、ユーザーが後でインデックスを作成するときに、不適切な変更が発生する可能性があるためです。

最後に、独自の比較を実行する場合のベンチマークコードを次に示します。

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
44
Phrogz

好きなのを選びな:

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}

他の提案がそうであるように、私はハッシュのデフォルトをいじることに強く反対します。それは、値のcheckingがハッシュを変更するためです。

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

勝者を比較するのは興味深いかもしれないと思いました:

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

わずかな変形あり:

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

多くの場合、どちらかのアプローチを使用できるためです(通常、空の配列またはハッシュを作成するため)。

Phrogzのベンチマークコードを使用して、これらを比較する方法を次に示します。

            user     system      total        real
Phrogz 2a   0.440000   0.010000   0.450000 (  0.444435)
Phrogz 2ai  0.580000   0.010000   0.590000 (  0.580248)
1
Cary Swoveland

ファセットの Enumerable#map_by は、これらの場合に便利です。この実装は他の実装よりも遅くなることは間違いありませんが、モジュール式でコンパクトなコードの方が常に保守が容易です。

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

これどう?

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

編集-インジェクトでも可能ですが、私見ではそれほど良くありません:

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
1
Michael Kohl

map(&:to_a).flatten(1)を使用した他のいくつかの回答と同じです。問題は、ハッシュの値を変更する方法です。配列は変更可能であるという事実を使用しました。

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

これはどうですか?

hs.reduce({}, :merge)

最短!しかし、パフォーマンスはかなり悪いです:

       user     system      total        real
 Phrogz 2a  0.240000   0.010000   0.250000 (  0.247337)
 Phrogz 2b  0.280000   0.000000   0.280000 (  0.274985)
 Glenn 1    0.290000   0.000000   0.290000 (  0.290370)
 Phrogz 1   0.310000   0.000000   0.310000 (  0.315548)
 Michael 1  0.360000   0.000000   0.360000 (  0.356760)
 Michael 2  0.360000   0.000000   0.360000 (  0.360119)
 Glenn 2    0.370000   0.000000   0.370000 (  0.369354)
 fl00r      0.390000   0.000000   0.390000 (  0.385883)
 sawa       0.410000   0.000000   0.410000 (  0.408190)
 Tokland 1  0.410000   0.000000   0.410000 (  0.410097)
 Tokland 2  0.490000   0.000000   0.490000 (  0.497325)
 Ich        1.410000   0.000000   1.410000 (  1.413176) # <<-- new
 Phrogz 3   1.760000   0.010000   1.770000 (  1.762979)
0
Ich