使用LINQ在两个列表中查找复杂组合的项目

4
这个问题与我之前的一个问题非常相似,链接在这里:使用LINQ计算两个列表中存在的组合数,但有些进一步的变化。
我有一个CartItem列表,可以根据在DiscountItem列表中指定的项目获得折扣。我需要能够提取可以获得折扣的购物车中的项目,并应用在DiscountItem中指定的适当折扣。只有存在每个组合时才应用折扣。以下是在折扣应用之前两个列表可能看起来像什么:
    折扣前:
CartItems DiscountItems ============================== =========================== SKU Qty DiscountApplied SKU DiscountAmount ============================== =========================== Ham 2 $0.00 Ham $0.33 Bacon 1 $0.00 Bacon $2.00 Ham 1 $0.00 Bacon 2 $0.00 Cheese 1 $0.00 Bacon 1 $0.00
棘手的部分在于,不仅仅是将两个列表连接在一起或计算组合数。折扣适用于出现在CartItem列表中的所有DiscountItem组合。在上面的示例中有3个这样的组合,如果你按照迭代的方式在列表中应用3个组合的折扣,每次应用折扣后数据将如下所示:
第一次折扣后:
购物车商品 折扣商品 ============================== =========================== SKU 数量 已折扣金额 SKU 折扣金额 ============================== =========================== Ham 2 $0.33 Ham $0.33 Bacon 1 $2.00 Bacon $2.00 Ham 1 $0.00 Bacon 2 $0.00 Cheese 1 $0.00 Bacon 1 $0.00
第二次折扣后:
购物车商品 折扣商品 ============================== =========================== SKU 数量 已折扣金额 SKU 折扣金额 ============================== =========================== Ham 2 $0.66 Ham $0.33 Bacon 1 $2.00 Bacon $2.00 Ham 1 $0.00 Bacon 2 $2.00 Cheese 1 $0.00 Bacon 1 $0.00
第三次折扣后:
购物车商品 折扣商品 ============================== =========================== SKU 数量 已折扣金额 SKU 折扣金额 ============================== =========================== Ham 2 $0.66 Ham $0.33 Bacon 1 $2.00 Bacon $2.00 Ham 1 $0.33 Bacon 2 $4.00 Cheese 1 $0.00 Bacon 1 $0.00
最终,除了奶酪和额外的培根之外,所有东西都有折扣。奶酪没有打折,因为它不在列表中的打折商品里。额外的培根没有享受到优惠,因为它没有对应的火腿商品,无法符合折扣组合的条件。总共有3个火腿和4个培根,所以其中一个培根不会得到折扣。
我想我应该可以使用 LINQ 来解决这个问题,因为它涉及枚举两个单独的列表,但是我不知道应该使用哪些 LINQ 方法来实现这一点。LINQ 查询的最终结果应该是已应用折扣的 CartItem 集合。


你可以聚合购物车商品吗? - Nick Larsen
@Nick,我希望我能够这样做,但事实并非如此。如果可以的话,这将是我的第一种方法,显然会使问题更容易解决。 - Ben McCormack
1
我对你们的业务规则很好奇。你们指出芝士和“额外的培根”不会获得折扣。为什么?这些规则看起来有些不合理,因为火腿和培根都能获得折扣。澄清一下你们的业务规则会有助于制定解决方案。 - jrista
@jrista 说得好。业务规则是购物车中的折扣商品每出现一次组合就会获得一次折扣。因此,3个火腿+4个培根将产生3个符合折扣条件的组合,并且会剩下1个没有折扣的培根。我已经更新了我的问题以澄清。 - Ben McCormack
1
@Ben:感谢您的澄清。下一个问题:这个处理过程必须使用LINQ吗?存在一些现有的设计模式可能更好地解决这个问题。 - jrista
显示剩余4条评论
4个回答

1

好的,只是为了好玩,这里有一个类似于LINQ的解决方案。

它可能远不如等效的迭代代码易读或高效,但它能工作!

var discountedCart = CartItems.Select(c => c);

var combinations = DiscountItems.Any()
    ? DiscountItems.GroupJoin(CartItems, d => d.SKU, c => c.SKU, (d, g) => g.Sum(c => c.Qty)).Min()
    : 0;

if (combinations > 0)
{
    var map = DiscountItems.ToDictionary(d => d.SKU, d => combinations);

    discountedCart = CartItems.Select(c =>
        {
            int mul;
            map.TryGetValue(c.SKU, out mul);

            if (mul < 1)
                return c;

            decimal amt = DiscountItems.Single(d => d.SKU == c.SKU).DiscountAmount;
            int qty = Math.Min(mul, c.Qty);

            map[c.SKU] = mul - qty;
            return new CartItem { SKU = c.SKU, Qty = c.Qty, DiscountApplied = amt * qty };
        });
}

foreach (CartItem item in discountedCart)
{
    Console.WriteLine("SKU={0} Qty={1} DiscountApplied={2}", item.SKU, item.Qty, item.DiscountApplied);
}

我怀疑如果你想要一个没有副作用的单一LINQ查询,那么你可以将所有内容都包装在Aggregate调用中,但这将需要更深层次的丑陋和低效。


1
获取您想要的确切结果有点困难。您可能需要存储中间结果 - 因此需要引入一个新类。这很具有挑战性,所以我按照以下方式完成了它 - 看起来它可以工作。
class Program {
    public class CartItem {
            public string sku { get; set; }
            public int qty {get;set;}
            public decimal DiscountApplied { get; set; }
            public CartItem(string sku,int qty,decimal DiscountApplied) {
                this.sku=sku;
                this.qty=qty;
                this.DiscountApplied=DiscountApplied;
            }
        }
 public class DiscountItem{
   public string sku {get;set;}
   public decimal DiscountAmount {get; set;}
}
static List<CartItem> carts=new List<CartItem>(){
new CartItem("Ham",2,0.0m ),
new CartItem("Bacon",1,0.00m  ),
new CartItem("Ham",1,0.00m ),
new CartItem("Bacon",2 ,0.00m),
new CartItem("Cheese",1,0.00m),
new CartItem("Bacon" , 1 ,  0.00m  )};

static  List<DiscountItem> discounts=new List<DiscountItem>() {
    new DiscountItem(){ sku="Ham", DiscountAmount=0.33m},
    new DiscountItem(){sku="Bacon",DiscountAmount=2.0m}};

class cartsPlus
{
    public CartItem Cart { get; set; }
    public int AppliedCount { get; set; }
}
public static void Main(string[] args){
    int num = (from ca in discounts
               join cart in carts on ca.sku equals cart.sku
               group cart by ca.sku into g
               select new { Sku = g.Key, Num = g.Sum(x => x.qty) }).Min(x => x.Num);

    var cartsplus = carts.Select(x => new cartsPlus { Cart = x, AppliedCount = 0 }).ToList();

    discounts.SelectMany(x => Enumerable.Range(1, num).Select(y => x)).ToList().ForEach(x=>{cartsPlus c=cartsplus.
            First(z=> z.Cart.sku==x.sku&&z.AppliedCount<z.Cart.qty);c.AppliedCount++;c.Cart.DiscountApplied+=x.DiscountAmount;});

     foreach (CartItem c in carts)
       Console.WriteLine("{0}  {1}   {2}", c.sku,c.qty, c.DiscountApplied);
}
 };

1
// Here we get all items have discounted, and gets minimal count of items
// This value is number of full combinations of items discounted
var minimalNumberOfItemsDiscounted =
CartItems.Where(ci => DiscountItems.Any(di => ci.SKU == di.SKU))
         .GroupBy(ci => ci.SKU)
         .Min(g => g.Count());

// Now we can apply discount to each item in cart, and we know how many 
// times (== minimalNumberOfItemsDiscounted) discount is applied

return CartItems
    .Select(ci => new 
    {
        CartItem = ci, 
        Discount = DiscountItems.FirstOrDefault(di => di.SKU == ci.SKU)
    })
    .Select(k =>
    { 
        if (k.Discount != null)
        {
            k.CartItem.Discount = minimalNumberOfItemsDiscounted * k.Discount.DiscountAmount;
        }
        return k.CartItem;
    });

0

编辑:我刚刚意识到这个问题有多久了。

我个人会使用以下代码,因为它对我来说是最易读的。它并没有使用太多 Linq,但我认为这是最简单的答案。

// For each discount that can be applied
foreach(var discount = DiscountedItems.Where(c => CartItems.Any(d => d.SKU == c.SKU)))
{
    var discountLimit = 3; // how many items are allowed to have a discount.
    foreach(var item in CartItems.Where(d => d.SKU == item.SKU))
    {
        if(discountLimit < item.Quantity)
        {
            // update the discount applied
            item.DiscountApplied = discountLimit * discount.DiscountAmount;
            discountLimit = 0; // causes the rest of the items to not get a discount
        }
        else
        {
            // update the discount applied
            item.DiscountApplied = item.Qty * discount.DiscountAmount;
            discountLimit -= item.Qty;
        }
    }
}

如果您有MoreLinq或LinqKit,您还可以执行以下操作:
// For each discount that can be applied
DiscountedItems.Where(c => CartItems.Any(d => d.SKU == c.SKU)).foreach(discount => 
{
    var discountLimit = 3; // how many items are allowed to have a discount.
    CartItems.Where(d => d.SKU == item.SKU).foreach(item =>
    {
        if(discountLimit < item.Quantity)
        {
            // update the discount applied
            item.DiscountApplied = discountLimit * discount.DiscountAmount;
            discountLimit = 0; // causes the rest of the items to not get a discount
        }
        else
        {
            // update the discount applied
            item.DiscountApplied = item.Qty * discount.DiscountAmount;
            discountLimit -= item.Qty;
        }
    });
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接