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

Используя Linq, чтобы подвести итог к числу (и пропустить остальные)

Если у нас есть класс, который содержит такое число:

class Person 
{
  public string Name {get; set;}
  public int Amount {get; set;}
}

а затем набор людей:

IList<Person> people;

Это содержит, допустим, 10 человек случайных имен и сумм есть ли выражение Linq, которое вернет мне подколлекцию объектов Person, чья сумма удовлетворяет условию?

Например, я хочу, чтобы первые х люди, сумма которых составляла менее 1000. Я могу сделать это традиционно

 var subgroup = new List<Person>();

 people.OrderByDescending(x => x.Amount);

 var count = 0;
 foreach (var person in people)
 {
    count += person.Amount;
    if (count < requestedAmount)
    {
        subgroup.Add(person);
    }
    else  
    {
        break;
    }
 }

Но мне было интересно, есть ли элегантный способ Linq делать что-то подобное, используя Sum, а затем некоторые другие функции, такие как Take?

UPDATE

Это фантастика:

var count = 0;
var subgroup = people
                  .OrderByDescending(x => x.Amount)
                  .TakeWhile(x => (count += x.Amount) < requestedAmount)
                  .ToList();

Но мне интересно, могу ли я как-то изменить его дальше, чтобы захватить следующего человека в списке людей и добавить остаток в сумму, чтобы общая сумма равнялась запрашиваемой сумме.

4b9b3361

Ответ 1

Вы можете использовать TakeWhile:

int s = 0;
var subgroup  = people.OrderBy(x => x.Amount)
                      .TakeWhile(x => (s += x.Amount) < 1000)
                      .ToList();

Примечание. Вы упоминаете в своем посте первые х людей. Можно было бы интерпретировать это как те, которые имеют наименьшую сумму, которая складывается до достижения 1000. Итак, я использовал OrderBy. Но вы можете заменить это на OrderByDescending если вы хотите начать получать от человека, имеющего наибольшую сумму.


Редактировать:

Чтобы сделать это, выберите еще один элемент из списка, который вы можете использовать:

.TakeWhile(x => {
                   bool bExceeds = s > 1000;
                   s += x.Amount;                                 
                   return !bExceeds;
                })

В TakeWhile здесь рассматривается значение s из предыдущей итерации, поэтому потребуется еще один, чтобы быть уверенным, что 1000 превышено.

Ответ 2

Мне не нравятся эти подходы к мутированию внутри запросов linq.

EDIT: Я не утверждал, что мой предыдущий код был непроверенным и был несколько псевдо-y. Я также упустил то, что Агрегат на самом деле съедает всю вещь сразу - как правильно указал, что это не сработало. Однако идея была правильной, но нам нужна альтернатива Aggreage.

Стыдно, что LINQ не имеет работающего агрегата. Я предлагаю код от user2088029 в этом сообщении: Как вычислить текущую сумму серии int в запросе Linq?.

И затем используйте это (которое проверено и что я намеревался):

var y = people.Scanl(new { item = (Person) null, Amount = 0 },
    (sofar, next) => new { 
        item = next, 
        Amount = sofar.Amount + next.Amount 
    } 
);       

Украденный код для долголетия:

public static IEnumerable<TResult> Scanl<T, TResult>(
    this IEnumerable<T> source,
    TResult first,
    Func<TResult, T, TResult> combine)
    {
        using (IEnumerator<T> data = source.GetEnumerator())
        {
            yield return first;

            while (data.MoveNext())
            {
                first = combine(first, data.Current);
                yield return first;
            }
        }
    }

Предыдущий, неправильный код:

У меня есть другое предложение; начните с списка

people

[{"a", 100}, 
 {"b", 200}, 
 ... ]

Рассчитать текущие итоги:

people.Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value})


[{item: {"a", 100}, total: 100}, 
 {item: {"b", 200}, total: 300},
 ... ]

Затем используйте TakeWhile и Select, чтобы вернуться к просто элементам;

people
 .Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value})
 .TakeWhile(x=>x.total<1000)
 .Select(x=>x.Item)

Ответ 3

Мне не нравятся все ответы на этот вопрос. Они либо мутируют переменную в запросе - плохую практику, которая приводит к неожиданным результатам, либо в случае решения Niklas (в противном случае хорошее), возвращает последовательность, которая имеет неправильный тип, или, в случае ответа Jeroen, код правильный, но может быть сделан для решения более общей проблемы.

Я бы улучшил усилия Никласа и Джеруна, сделав фактически общее решение, которое вернет правильный тип:

public static IEnumerable<T> AggregatingTakeWhile<T, U>(
  this IEnumerable<T> items, 
  U first,
  Func<T, U, U> aggregator,
  Func<T, U, bool> predicate)
{
  U aggregate = first;
  foreach (var item in items)
  {
    aggregate = aggregator(item, aggregate);
    if (!predicate(item, aggregate))
      yield break;
    yield return item; 
  }
}

Что мы теперь можем использовать для реализации решения конкретной проблемы:

var subgroup = people
  .OrderByDescending(x => x.Amount)
  .AggregatingTakeWhile(
    0, 
    (item, count) => count + item.Amount, 
    (item, count) => count < requestedAmount)
  .ToList();

Ответ 4

Try:

int sumCount = 0;

var subgroup = people
    .OrderByDescending(item => item.Amount)           // <-- you wanted to sort them?
    .Where(item => (sumCount += item.Amount) < requestedAmount)
    .ToList();

Но это не очаровательно... Это будет менее читаемо.

Ответ 5

Я принял комментарий Eric Lippert и пришел с этим лучшим решением. Я думаю, что лучший способ - создать функцию (в моем случае я написал метод расширения)

public static IEnumerable<T> TakeWhileAdding<T>(
    this IEnumerable<T> source, 
    Func<T, int> selector, 
    Func<int, bool> comparer)
{
    int total = 0;

    foreach (var item in source)
    {
        total += selector(item);

        if (!comparer(total))
            yield break;

        yield return item;
    }
}

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

var values = new Person[]
{
    new Person { Name = "Name1", Amount = 300 },
    new Person { Name = "Name2", Amount = 500 },
    new Person { Name = "Name3", Amount = 300 },
    new Person { Name = "Name4", Amount = 300 }
};

var subgroup = values.TakeWhileAdding(
    person => person.Amount, 
    total => total < requestedAmount);

foreach (var v in subgroup)
    Trace.WriteLine(v);

Это также можно создать для double, float или что-то вроде TimeSpan.

Таким образом, каждый раз, когда subgroup повторяется, используется новый счетчик.

Ответ 6

Гиоргос указал мне в правильном направлении, чтобы его ответ был принят.

Однако для полноты я пишу здесь решение, с которым я закончил.

var count = 0;
var exceeds = false;

var subgroup  = people.OrderBy(x => x.Amount).TakeWhile(x =>
{
    if (exceeds)
    {
        return false;
    }

    count += x.Amount;
    if (count >= requestedAmount)
    {
        x.Amount = requestedAmount - (count - x.Amount);
        exceeds = true;
        return true;
    }

    return !exceeds;
}).ToList();

Возвращает подгруппу, общая сумма которой равна запрашиваемой сумме. Большое спасибо!