Подтвердить что ты не робот

LINQ, упрощающее выражение - взять, а сумма взятого не превышает заданного значения

Учитывая настройку, подобную этой.

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

var products = new List<Product> {
    new Product { Cost = 5 },
    new Product { Cost = 10 },
    new Product { Cost = 15 },
    new Product { Cost = 20 }
};

var credit = 15;

Предположим, что список будет отсортирован в данном порядке. Я хочу в основном перебрать каждый элемент в списке, сохранить суммарную стоимость и продолжать получать продукты, если общая стоимость не превышает credit.

Я могу сделать это с помощью некоторых циклов и т.д., но мне было интересно, есть ли способ свести его к более простому запросу LINQ.

4b9b3361

Ответ 1

Не "полностью" linq, потому что ему нужна одна дополнительная переменная, но это самый легкий, о котором я мог подумать:

int current=0;
var selection = products.TakeWhile(p => (current = current + p.Cost) <= credit);

Ответ 2

Другие указали на захваченный переменный подход, и есть, возможно, правильные точки зрения, что этот подход плох, потому что он мутирует состояние. Кроме того, захваченные подходы с переменными могут быть повторены только один раз и опасны, потому что a. вы можете забыть этот факт и попытаться повторить итерацию дважды; б. захваченная переменная не отражает сумму взятых элементов.

Чтобы избежать этих проблем, просто создайте метод расширения:

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

Использование:

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

Обратите внимание, что теперь вы можете повторять итерацию дважды (хотя будьте осторожны, если ваш TAccumulate изменяет тип ссылки).

Ответ 3

Вы можете сделать это, если хотите решение без внешней переменной

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;

Ответ 4

ok, повторите мой комментарий выше в ответ @Aducci, здесь версия, использующая 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);

И это моя реализация Scan (которая, как я предполагаю, похожа на то, что в Rx Framework, но я не проверял)

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

Ответ 5

Используйте захваченную переменную для отслеживания суммы, принятой до сих пор.

int sum = 0;

IEnumerable<Product> query = products.TakeWhile(p =>
{
  bool canAfford = (sum + p.Cost) <= credit;
  sum = canAfford ? sum + p.Cost : sum;
  return canAfford;
});