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

时间:2010-06-28 19:30:47

标签: c# .net vb.net linq

这个问题非常类似于我之前的问题,Use LINQ to count the number of combinations existing in two lists,除了一些进一步的曲折。

我有一个CartItem列表,可以根据DiscountItem列表中指定的项目获得折扣。我需要能够拉出购物车中可以获得折扣的商品并应用DiscountItem中指定的相应折扣。折扣仅适用于存在的每个组合。以下是在应用折扣之前两个列表的样子:

    BEFORE DISCOUNT:   

    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 

棘手的部分是,它不仅仅是将两个列表连接在一起或计算组合数量的问题。该折扣适用于DiscountItem列表中显示的CartItem的所有组合。在上面的示例中有3个这样的组合,如果您要在列表中迭代地应用3种组合的折扣,则每次应用折扣时数据都会如下所示:

    After 1st Discount is applied:

    CartItems                               DiscountItems
    ==============================          ===========================
    SKU     Qty    DiscountApplied          SKU     DiscountAmount
    ==============================          ===========================
    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    

    After 2nd Discount is applied:

    CartItems                               DiscountItems
    ==============================          ===========================
    SKU     Qty    DiscountApplied          SKU     DiscountAmount
    ==============================          ===========================
    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        

    After 3rd Discount is applied:

    CartItems                               DiscountItems
    ==============================          ===========================
    SKU     Qty    DiscountApplied          SKU     DiscountAmount
    ==============================          ===========================
    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来解决这个问题,因为它涉及枚举超过2个单独的列表,但我想不出我会用什么LINQ方法来实现这一点。 LINQ查询的最终结果应该是已应用折扣的CartItem s的集合。

4 个答案:

答案 0 :(得分: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 :(得分: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;
    });

答案 2 :(得分: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);
}
 };

答案 3 :(得分: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;
        }
    });
});