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