Ruby on rails 优化选项的最大数量,以显示客户给定的现有选择集合
这是一个老问题(链接如下)的变体,我的措辞很糟糕,因此无法充分解决我的问题 背景 假设有一个装有苹果的苹果篮,每个苹果篮都是一个具有Ruby on rails 优化选项的最大数量,以显示客户给定的现有选择集合,ruby-on-rails,ruby,algorithm,optimization,Ruby On Rails,Ruby,Algorithm,Optimization,这是一个老问题(链接如下)的变体,我的措辞很糟糕,因此无法充分解决我的问题 背景 假设有一个装有苹果的苹果篮,每个苹果篮都是一个具有size属性的对象,在这个特定的篮中,我们有3个苹果:[{size:'S'},{size:'M'},{size:'L'}] 在购物过程中,每次客户将苹果添加到购物车时,他们都可以选择尺寸,但重要的是,他们不必选择尺寸,这是一个可选的选择器 我正在尝试编写一个方法,剩余的_选项,该方法可以最大化客户在向购物车添加苹果时显示的最大选项数,考虑到他们过去选择的历史记录。尺
size
属性的对象,在这个特定的篮中,我们有3个苹果:[{size:'S'},{size:'M'},{size:'L'}]
在购物过程中,每次客户将苹果添加到购物车时,他们都可以选择尺寸,但重要的是,他们不必选择尺寸,这是一个可选的选择器
我正在尝试编写一个方法,剩余的_选项
,该方法可以最大化客户在向购物车添加苹果时显示的最大选项数,考虑到他们过去选择的历史记录。尺寸选择是可选的这一事实非常重要。考虑这2个例子:
示例A:客户选择一个选项
客户将第一个苹果添加到购物车
客户看到提示请选择尺码(可选):[S,M,L]
客户决定选择S
客户将第二个苹果添加到购物车
客户看到提示请选择尺码(可选):[M,L]
示例B:客户未选择选项
客户将第一个苹果添加到购物车
客户看到提示请选择尺码(可选):[S,M,L]
客户跳过此步骤
客户将第二个苹果添加到购物车
客户看到提示请选择尺码(可选):[S,M,L]
在示例B中,由于客户没有选择选项,因此仍然显示可用的全套选项。该代码不会从选项集中任意“删除”苹果。换句话说,此方法不负责计算剩余数量,只负责计算剩余选项,并尝试将其最大化
注意:当然,即使此方法不计算数量,在实际结帐时,也会通过另一种方法为下一位客户调整库存。也就是说,假设在上面的例子中,客户没有为第二个苹果选择尺寸,那么在结帐时
- 例如,客户将收到1个
S
苹果和第二个M
或L
苹果,由其他代码随机选择。如果客户收到[S,M]
,那么下一个订购苹果的客户将只能添加1个苹果,剩余的\u选项将只返回[L]
- 例B,客户将收到2个随机苹果,可能是由其他代码随机选择的
[S,M]
,[M,L]
,[S,L]
。如果客户收到[S,L]
,那么下一个订购苹果的客户将只能添加1个苹果,剩余的\u选项将只返回[M]
复杂性
挑战在于,我希望剩余的_选项
适用于苹果对象上任何给定数量的可能属性(大小
,颜色
,价格
,等等),以及这些属性的任何数量的选项。由于选择是可选的,用户可以拒绝选择大小
,颜色
,价格
,等等,或者选择1(例如,仅大小
)或2(例如大小
和价格
)等等
考虑到这种复杂性,代码不应将向购物车添加苹果的过程视为一个独立的过程,其中剩余的_选项
是通过篮子中的选项减去最后选择的选项来计算的
相反,代码应该查看客户购物车中的所有苹果,无论是否为每个苹果选择了一个选项,然后计算剩余的苹果选项的最大数量
前一个解决方案不起作用,因为它们做了前一个,即将添加每个苹果视为一个独立的过程,从篮子中删除了一个选项
下面是一个需要澄清的例子。假设一篮3个苹果如下所示:
[
{size:'S', price:1},
{size:'M', price:2},
{size:'L', price:2},
]
basket = [
{origin:'IL', size:'S', color:'G', price:1},
{origin:'SP', size:'M', color:'G', price:2},
{origin:'SP', size:'M', color:'R', price:2},
{origin:'SP', size:'M', color:'Y', price:3},
{origin:'CA', size:'L', color:'G', price:1},
{origin:'SP', size:'L', color:'G', price:4},
{origin:'CA', size:'L', color:'R', price:4},
]
cart = Cart.new basket
cart << {price:2}
cart << {size:'M'}
cart << {origin:'SP'}
cart.remaining_options
# => {:origin=>#<Set: {"IL", "CA", "SP"}>, :size=>#<Set: {"S", "L", "M"}>, :color=>#<Set: {"G", "R", "Y"}>, :price=>#<Set: {1, 4, 3, 2}>}
cart << {price:2}
cart.remaining_options
# => {:origin=>#<Set: {"IL", "CA"}>, :size=>#<Set: {"S", "L"}>, :color=>#<Set: {"G", "R"}>, :price=>#<Set: {1, 4}>}
步骤1-添加第一个苹果
比如说,剩余的_选项
采用客户现有的参数。因此,当客户将第一个苹果添加到购物车时,购物车中没有任何内容,因此所有选项都会返回
basket.remaining_options([])
=> {size:['S','M','L'], price: [1,2]}
步骤2-添加第二个苹果
为了完成第一笔苹果交易,客户决定选择价格为2的苹果。然后他们把第二个苹果加入购物车。由于不止一个苹果的价格为:2
,因此客户的第一个选择不会产生容量限制,再次显示所有选项
basket.remaining_options([{price:2}])
=> {size:['S','M','L'], price: [1,2]}
注意:@obiruby敏锐地观察到,虽然{size:['S','M','L'],price:[1,2]}
在技术上是所有剩余选项,但它们并不都是可选的。对于第二个苹果,如果客户选择M
,则第一个苹果必须自动分配为L
,因此不再可选。这是由另一种我已经使用过的方法来处理的。这么多的方法。。。所有这些都是为了让这个最大的选项优化工作
步骤3-添加第三个苹果
为了完成第二笔苹果交易,客户决定再次选择价格为2的苹果。然后,他们将第三个苹果添加到购物车中。现在很棘手。如果此代码只是减去所选的选项,那么在面值上,剩余的_选项
可能会返回以下内容:{size:['S','M','L',price:[1]}
。但这并不准确,因为当所有的price:2
苹果都被拿走时,默认情况下所有的M
和L
苹果也是如此。这就是为什么
basket.remaining_options([{price:2}, {size:'M'}])
=> {size:['S'], price: [1]}
# user's 1st ordered apple has a price request
# user's 2nd ordered apple has a size request
# COMBO of the 2 only work if...
# user's 1st ordered apple is the {price:2, size:'L'} apple in basket
# user's 2nd ordered apple is the {size:'M', price:'2'} apple in basket
# therefore if user is looking at remaining options for a 3rd apple, there's only the {size:'S', price:'1'} apple left in basket
# test.rb
require 'minitest/autorun'
class Basket
attr_accessor :basket
def initialize(opts={})
@basket = opts[:basket]
end
def remaining_options(selected_options=[])
return {} if @basket.empty?
selected_options.each do |option_criteria|
select_option(option_criteria)
end
print_basket
end
def select_option(option_criteria)
selectable_options = @basket.select { |opt| matches?(opt, option_criteria) }
option_to_select = most_common_option(selectable_options)
remove_from_basket option_to_select
end
def most_common_option( selectable_options )
max_matches = 0
max_matches_idx = nil
selectable_options.each_with_index do |option, i|
option_matches = 0
option.keys.each do |k|
selectable_options.each do |opt, i|
option_matches += 1 if option[k] == opt[k]
end
if option_matches > max_matches
max_matches = option_matches
max_matches_idx = i
end
end
end
selectable_options[max_matches_idx]
end
def remove_from_basket( option_to_select )
idx_to_remove = @basket.index { |i| matches?(i, option_to_select)}
@basket.delete_at idx_to_remove
end
def matches?(opt, selected)
selected.keys.all? { |k| selected[k] == opt[k] }
end
def print_basket
hsh = Hash.new { |h,k| h[k] = [] }
@basket.each do |item_hsh|
item_hsh.each do |k, v|
hsh[k] << v unless hsh[k].include?(v)
end
end
hsh
end
end
class BasketTest < Minitest::Test
def setup_basket
Basket.new basket: [
{size:'S', price:1},
{size:'M', price:2},
{size:'L', price:2},
]
end
def test_initial_basket_case
basket = setup_basket
assert basket.remaining_options == {size:['S','M','L'], price: [1,2]}
end
def test_returns_empty_if_basket_empty
basket = Basket.new basket: []
assert basket.remaining_options([{price:2}]) == {}
end
def test_one_pick
basket = setup_basket
assert basket.remaining_options([{price:2}]) == {size:['S', 'L'], price: [1,2]}
end
def test_two_picks
basket = setup_basket
assert basket.remaining_options([{price:2}, {price:2}]) == {size:['S'], price: [1]}
end
def test_maximizes_options
larger_basket = Basket.new basket: [
{size:'S', price:1},
{size:'L', price:2},
{size:'M', price:2},
{size:'M', price:2}
]
assert larger_basket.remaining_options([{price:2}]) == {size:['S', 'L', 'M'], price: [1,2]}
end
def test_maximizes_options_complex
larger_basket = Basket.new basket: [
{size:'S', price:1, color: 'red'},
{size:'L', price:2, color: 'red'},
{size:'M', price:2, color: 'purple'},
{size:'M', price:2, color: 'green'},
{size:'M', price:2, color: 'green'},
{size:'M', price:2, color: 'green'}
]
assert larger_basket.remaining_options([{price:2}, {color: 'green'}]) == {size:['S', 'L', 'M'], price: [1,2], color: ['red', 'purple', 'green']}
end
end
require 'set'
def extract_options(basket)
basket.each_with_object( {} ) do |item, options|
item.each do |attr,value|
options[attr] = Set.new unless options.has_key?(attr)
options[attr] << value
end
end
end
def expand_item(item, options)
expanded = [[]]
item.each do |attr,value|
opts = value.nil? ? options[attr] : [value]
expanded = expanded.each_with_object( [] ) do |path,exp|
opts.each { |val| exp << ( path.dup << [attr, val] ) }
end
end
expanded.map(&:to_h)
end
def remaining_options(basket, cart)
options = extract_options(basket)
return [options] if cart.empty?
remaining = []
f,*r = *cart.map { |item| expand_item(item, options) }
f.product(*r) do |cart|
b = cart.each_with_object( basket.dup ) do |item,bask|
break unless index = bask.index(item)
bask.delete_at(index)
end
remaining << extract_options(b) if b
end
remaining.uniq
end
def remaining_options_union(basket,cart)
remaining_options(basket,cart).each_with_object( {} ) do |remain,union|
remain.each do |attr,set|
union[attr] = Set.new unless union[attr]
union[attr] += set
end
end
end
basket = [
{size:'S', price:1},
{size:'M', price:2},
{size:'L', price:2},
]
cart = []
remaining_options_union(basket,cart)
# => {:size=>#<Set: {"S", "M", "L"}>, :price=>#<Set: {1, 2}>}
cart << {size: nil, price: 2} # all attributes need to be populated
remaining_options(basket,cart)
# => [{:size=>#<Set: {"S", "L"}>, :price=>#<Set: {1, 2}>}, {:size=>#<Set: {"S", "M"}>, :price=>#<Set: {1, 2}>}]
remaining_options_union(basket,cart)
# => => {:size=>#<Set: {"S", "L", "M"}>, :price=>#<Set: {1, 2}>}
cart << {size: nil, price: 2}
remaining_options(basket,cart)
# => [{:size=>#<Set: {"S"}>, :price=>#<Set: {1}>}]
remaining_options_union(basket,cart)
# => {:size=>#<Set: {"S"}>, :price=>#<Set: {1}>}
require 'set'
class Cart
def self.extract_choices basket
basket.each_with_object( {} ) do |item, attrs|
item.each do |key,value|
raise "Null valued item attribute in basket" if value.nil?
attrs[key] = Set.new unless attrs.has_key?(key)
attrs[key] << value
end
end
end
attr_reader :remaining_options
def initialize basket
choices = self.class.extract_choices basket
@globs = choices # needed to unglob a null valued attribute in an item
@attrs = choices.keys # attributes found in the items of the basket
# The items in the basket shall have all the possible attributes populated and not null
basket.each do |item|
@attrs.each do |attr|
raise "Invalid item in basket" unless item[attr]
end
end
@virtual_baskets = [ basket ]
@remaining_options = choices
end
def <<(item)
# As we're using the 'Hash#==' method, each item must have all its
# attributes set (the same ones as in the basket), no more, no less.
item = @attrs.each_with_object( {} ) do |attr,obj|
obj[attr] = item[attr]
end
# NOTE: an item with globs will multiply the number of virtual baskets
update_virtual_baskets item
choices = @virtual_baskets.map{|b| self.class.extract_choices b}
@remaining_options = choices.each_with_object( {} ) do |choice, union|
choice.each do |attr,set|
union[attr] = Set.new unless union[attr]
union[attr] += set
end
end
end
private
def unglob item
expanded_items = [[]]
item.each do |attr, value|
choices = value.nil? ? @globs[attr] : [value]
expanded_items = expanded_items.each_with_object( [] ) do |pre, obj|
choices.each { |val| obj << ( pre.dup << [attr, val] ) }
end
end
expanded_items.map(&:to_h)
end
def update_virtual_baskets formated_item
expanded_items = unglob formated_item
valid_baskets = []
@virtual_baskets.each do |basket|
expanded_items.each do |item|
basket.each_index do |idx|
next unless basket[idx] == item
bsk = basket.dup
bsk.delete_at(idx)
valid_baskets << bsk
end
end
end
raise "Error while adding item to cart - no stock" if valid_baskets.empty?
@virtual_baskets = valid_baskets.uniq
end
end
basket = [
{origin:'IL', size:'S', color:'G', price:1},
{origin:'SP', size:'M', color:'G', price:2},
{origin:'SP', size:'M', color:'R', price:2},
{origin:'SP', size:'M', color:'Y', price:3},
{origin:'CA', size:'L', color:'G', price:1},
{origin:'SP', size:'L', color:'G', price:4},
{origin:'CA', size:'L', color:'R', price:4},
]
cart = Cart.new basket
cart << {price:2}
cart << {size:'M'}
cart << {origin:'SP'}
cart.remaining_options
# => {:origin=>#<Set: {"IL", "CA", "SP"}>, :size=>#<Set: {"S", "L", "M"}>, :color=>#<Set: {"G", "R", "Y"}>, :price=>#<Set: {1, 4, 3, 2}>}
cart << {price:2}
cart.remaining_options
# => {:origin=>#<Set: {"IL", "CA"}>, :size=>#<Set: {"S", "L"}>, :color=>#<Set: {"G", "R"}>, :price=>#<Set: {1, 4}>}