LINQ, simplifying expression - take while sum of taken does not exceed given value

前端 未结 5 1177
别那么骄傲
别那么骄傲 2021-01-04 03:50

Given a setup like this ..

class Product {
   int Cost;
   // other properties unimportant
}

var products = new List {
    new Product { Cost         


        
相关标签:
5条回答
  • 2021-01-04 04:08

    Others have pointed out the captured variable approach, and there are arguably correct viewpoints that this approach is bad because it mutates state. Additionally, the captured variable approaches can only be iterated once, and are dangerous because a. you might forget that fact and try to iterate twice; b. the captured variable does not reflect the sum of the items taken.

    To avoid these problems, just create an extension method:

    public static IEnumerable<TSource> TakeWhileAggregate<TSource, TAccumulate>(
        this IEnumerable<TSource> source,
        TAccumulate seed,
        Func<TAccumulate, TSource, TAccumulate> func,
        Func<TAccumulate, bool> predicate
    ) {
        TAccumulate accumulator = seed;
        foreach (TSource item in source) {
            accumulator = func(accumulator, item);
            if (predicate(accumulator)) {
                yield return item;
            }
            else {
                yield break;
            }
        }
    }
    

    Usage:

    var taken = products.TakeWhileAggregate(
        0, 
        (cost, product) => cost + product.Cost,
        cost => cost <= credit
    );
    

    Note that NOW you can iterate twice (although be careful if your TAccumulate is mutable a reference type).

    0 讨论(0)
  • 2021-01-04 04:10

    You can do this if you want a solution without an external variable

    var indexQuery = products.Select((x,index) => new { Obj = x, Index = index });
    
    var query = from p in indexQuery 
                let RunningTotal = indexQuery.Where(x => x.Index <= p.Index)
                                             .Sum(x => x.Obj.Cost)
                where credit >= RunningTotal
                select p.Obj;
    
    0 讨论(0)
  • 2021-01-04 04:10

    ok, re my comment above in @Aducci's answer, here's a version using Scan

          var result=products.Scan(new {Product=(Product)null, RunningSum=0},
            (self, next) => new {Product=next, RunningSum=self.RunningSum+next.Cost})
            .Where(x=>x.RunningSum<=credit)
            .Select(x => x.Product);
    

    And this is my implementation of Scan (which I assume is similar to what's in the Rx Framework, but I haven't checked)

        public static IEnumerable<TAccumulate> Scan<TSource, TAccumulate>(this IEnumerable<TSource> source,
          TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> accumulator) {
    
          foreach(var item in source) {
            seed=accumulator(seed, item);
            yield return seed;
          }
        }
    
    0 讨论(0)
  • 2021-01-04 04:21

    Not "fully" linq, because it needs one extra variable, but it is the easiest I could think of:

    int current=0;
    var selection = products.TakeWhile(p => (current = current + p.Cost) <= credit);
    
    0 讨论(0)
  • 2021-01-04 04:22

    Use a captured variable to track the amount taken so far.

    int sum = 0;
    
    IEnumerable<Product> query = products.TakeWhile(p =>
    {
      bool canAfford = (sum + p.Cost) <= credit;
      sum = canAfford ? sum + p.Cost : sum;
      return canAfford;
    });
    
    0 讨论(0)
提交回复
热议问题