Ruby hash combinations
Asked Answered
A

4

6

For an e-commerce app, I'm trying to turn a hash of options, each with an array of choices, into an array of hashes representing the combinations of those choices. For example:

# Input:
{ :color => [ "blue", "grey" ],
  :size  => [ "s", "m", "l" ] }

# Output:
[ { :color => "blue", :size => "s" },
  { :color => "blue", :size => "m" },
  { :color => "blue", :size => "m" },
  { :color => "grey", :size => "s" },
  { :color => "grey", :size => "m" },
  { :color => "grey", :size => "m" } ]

Input may have additional options inside of it with an undetermined number of choices for each one, but it will only ever be nested 1 level deep. Any

Allain answered 3/9, 2014 at 20:54 Comment(0)
M
6

You can try:

ary = input.map {|k,v| [k].product v}
output = ary.shift.product(*ary).map {|a| Hash[a]}

Result:

[
  {:color=>"blue", :size=>"s"},
  {:color=>"blue", :size=>"m"},
  {:color=>"blue", :size=>"l"},
  {:color=>"grey", :size=>"s"},
  {:color=>"grey", :size=>"m"},
  {:color=>"grey", :size=>"l"}
]
Mockup answered 3/9, 2014 at 21:3 Comment(3)
I think you mean shift instead of unshift (which doesn't do anything if no arguments are given). And FWIW in Ruby 2+ you can replace the last map with map(&:to_h), ergo: ary.shift.product(*ary).map(&:to_h).Quinate
@Jordan - of course I meant shift, it's late. :P Thanks for pointing it out.Mockup
@Mockup It's a great solution, although it might be a little opaque to beginners. And personally I think it's a little nicer with destructuring assignment instead of shift: first, *rest = input.map {|k,v| [k].product v }; output = first.product(*rest).map(&:to_h). But that's just me. ;)Quinate
I
7

A variant on the above:

input = { color: [ "blue", "grey" ],
          size:  [ "s", "m", "l" ],
          wt:    [:light, :heavy] }

keys = input.keys
  #=> [:color, :size, :wt]
values = input.values
  #=> [["blue", "grey"], ["s", "m", "l"], [:light, :heavy]]
values.shift.product(*values).map { |v| Hash[keys.zip(v)] }
  #=> [{:color=>"blue", :size=>"s", :wt=>:light},
  #    {:color=>"blue", :size=>"s", :wt=>:heavy},
  #    {:color=>"blue", :size=>"m", :wt=>:light},
  #    {:color=>"blue", :size=>"m", :wt=>:heavy},
  #    {:color=>"blue", :size=>"l", :wt=>:light},
  #    {:color=>"blue", :size=>"l", :wt=>:heavy},
  #    {:color=>"grey", :size=>"s", :wt=>:light},
  #    {:color=>"grey", :size=>"s", :wt=>:heavy},
  #    {:color=>"grey", :size=>"m", :wt=>:light},
  #    {:color=>"grey", :size=>"m", :wt=>:heavy},
  #    {:color=>"grey", :size=>"l", :wt=>:light},
  #    {:color=>"grey", :size=>"l", :wt=>:heavy}]
Imena answered 4/9, 2014 at 7:6 Comment(0)
M
6

You can try:

ary = input.map {|k,v| [k].product v}
output = ary.shift.product(*ary).map {|a| Hash[a]}

Result:

[
  {:color=>"blue", :size=>"s"},
  {:color=>"blue", :size=>"m"},
  {:color=>"blue", :size=>"l"},
  {:color=>"grey", :size=>"s"},
  {:color=>"grey", :size=>"m"},
  {:color=>"grey", :size=>"l"}
]
Mockup answered 3/9, 2014 at 21:3 Comment(3)
I think you mean shift instead of unshift (which doesn't do anything if no arguments are given). And FWIW in Ruby 2+ you can replace the last map with map(&:to_h), ergo: ary.shift.product(*ary).map(&:to_h).Quinate
@Jordan - of course I meant shift, it's late. :P Thanks for pointing it out.Mockup
@Mockup It's a great solution, although it might be a little opaque to beginners. And personally I think it's a little nicer with destructuring assignment instead of shift: first, *rest = input.map {|k,v| [k].product v }; output = first.product(*rest).map(&:to_h). But that's just me. ;)Quinate
W
3

You're basically trying to compute combinations here, and that means two levels of iteration with a way of aggregating the results of those operations:

input = {:color=>["blue", "grey"], :size=>["s", "m", "l"]}

combinations = input[:color].flat_map do |color|
  input[:size].collect do |size|
    { color: color, size: size }
  end
end

puts combinations.inspect
# => [{:color=>"blue", :size=>"s"}, {:color=>"blue", :size=>"m"}, {:color=>"blue", :size=>"l"}, {:color=>"grey", :size=>"s"}, {:color=>"grey", :size=>"m"}, {:color=>"grey", :size=>"l"}]

Here flat_map comes in handy as it collapses the results of the inner expansion.

Weep answered 3/9, 2014 at 21:1 Comment(2)
Thanks. I'm trying to find a more general way to do this that does not rely on the keys of the input hash because those will be user inputsAllain
@mu-is-too-short You're right. At the same time, I thought it was implied when I said "Input may have additional options inside of it with an undetermined number of choices for each one". I do not think this solution would quite handle that scenario.Allain
S
1

Please try OCG options combination generator.

require "ocg"

generator = OCG.new(
  :color => %w[blue grey],
  :size  => %w[s m l]
)

puts generator.next until generator.finished?

Generator includes much more functionality that will help you to deal with other options.

Shabbygenteel answered 1/11, 2019 at 10:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.