This question already has answers here:
Closed 6 years ago.
Summary
Given a Hash where some of the values are arrays, how can I get an array of hashes for all possible combinations?
Test Case
options = { a:[1,2], b:[3,4], c:5 }
p options.self_product
#=> [{:a=>1, :b=>3, :c=>5},
#=> {:a=>1, :b=>4, :c=>5},
#=> {:a=>2, :b=>3, :c=>5},
#=> {:a=>2, :b=>4, :c=>5}]
When the value for a particular key is not an array, it should simply be included as-is in each resulting hash, the same as if it were wrapped in an array.
Motivation
I need to generate test data given a variety of values for different options. While I can use [1,2].product([3,4],[5])
to get the Cartesian Product of all possible values, I'd rather use hashes to be able to label both my input and output so that the code is more self-explanatory than just using array indices.
I suggest a little pre-processing to keep the result general:
options = { a:[1,2], b:[3,4], c:5 }
options.each_key {|k| options[k] = [options[k]] unless options[k].is_a? Array}
=> {:a=>[1, 2], :b=>[3, 4], :c=>[5]}
I edited to make a few refinements, principally the use of inject({})
:
class Hash
def self_product
f, *r = map {|k,v| [k].product(v).map {|e| Hash[*e]}}
f.product(*r).map {|a| a.inject({}) {|h,e| e.each {|k,v| h[k]=v}; h}}
end
end
...though I prefer @Phrogz's '2nd attempt', which, with pre-processing 5=>[5]
, would be:
class Hash
def self_product
f, *r = map {|k,v| [k].product(v)}
f.product(*r).map {|a| Hash[*a.flatten]}
end
end
First attempt:
class Hash
#=> Given a hash of arrays get an array of hashes
#=> For example, `{ a:[1,2], b:[3,4], c:5 }.self_product` yields
#=> [ {a:1,b:3,c:5}, {a:1,b:4,c:5}, {a:2,b:3,c:5}, {a:2,b:4,c:5} ]
def self_product
# Convert array values into single key/value hashes
all = map{|k,v| [k].product(v.is_a?(Array) ? v : [v]).map{|k,v| {k=>v} }}
#=> [[{:a=>1}, {:a=>2}], [{:b=>3}, {:b=>4}], [{:c=>5}]]
# Create the product of all mini hashes, and merge them into a single hash
all.first.product(*all[1..-1]).map{ |a| a.inject(&:merge) }
end
end
p({ a:[1,2], b:[3,4], c:5 }.self_product)
#=> [{:a=>1, :b=>3, :c=>5},
#=> {:a=>1, :b=>4, :c=>5},
#=> {:a=>2, :b=>3, :c=>5},
#=> {:a=>2, :b=>4, :c=>5}]
Second attempt, inspired by @Cary's answer:
class Hash
def self_product
first, *rest = map{ |k,v| [k].product(v.is_a?(Array) ? v : [v]) }
first.product(*rest).map{ |x| Hash[x] }
end
end
In addition to being more elegant, the second answer is also about 4.5x faster than the first when creating a large result (262k hashes with 6 keys each):
require 'benchmark'
Benchmark.bm do |x|
n = *1..8
h = { a:n, b:n, c:n, d:n, e:n, f:n }
%w[phrogz1 phrogz2].each{ |n| x.report(n){ h.send(n) } }
end
#=> user system total real
#=> phrogz1 4.450000 0.050000 4.500000 ( 4.502511)
#=> phrogz2 0.940000 0.050000 0.990000 ( 0.980424)