Как объединить массив хэшей, чтобы получить хэш массивов значений
Это противоположно превращение массива массивов в массив хашей в 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, приемлемы, но следует отметить как таковые.
Ниже приведены результаты сопоставления различных ответов ниже (и нескольких моих собственных) с использованием трех разных массивов хешей:
-
где каждый хэш имеет разные ключи, поэтому никакого слияния не происходит:
[{: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}, ...]
- и тот, который представляет собой сочетание уникальных и общих ключей:
[{: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)
Самый быстрый код - этот метод, который я добавил:
def collect_values(hashes)
{}.tap{ |r| hashes.each{ |h| h.each{ |k,v| (r[k]||=[]) << v } } }
end
Я принял "glenn mcdonald answer, поскольку он был конкурентоспособным с точки зрения скорости, достаточно кратким, но (что наиболее важно), поскольку он указывал на опасность используя хеш с самонастраивающимся профилем по умолчанию для удобной конструкции, так как это может привести к плохим изменениям, когда пользователь индексирует его позже.
Наконец, здесь приведен примерный код, если вы хотите запустить свои собственные сравнения:
require 'prime' # To generate the third hash
require 'facets' # For tokland1 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
Ответы
Ответ 1
Сделайте выбор:
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}
Я категорически против того, чтобы возиться со значениями по умолчанию для хэшей, как это делают другие предложения, потому что тогда проверка значения изменяет хэш, что кажется мне очень неправильным.
Ответ 2
h = Hash.new{|h,k| h[k]=[]}
hs.map(&:to_a).flatten(1).each{|v| h[v[0]] << v[1]}
Ответ 3
Как это?
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
Изменить - также возможно с помощью инъекции, но IMHO не так приятно:
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
Ответ 4
То же самое с некоторыми другими ответами, используя 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
Ответ 5
Facet Перечислимый # 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]
Ответ 6
Я подумал, что было бы интересно сравнить победителя:
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)
Ответ 7
Как насчет этого?
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)
Ответ 8
[{'a' => 1}, {'b' => 2}, {'c' => 3}].reduce Hash.new, :merge