LINQ: GroupBy с максимальным количеством в каждой группе

У меня есть список дубликатов:

Enumerable.Range(1,3).Select(o => Enumerable.Repeat(o, 3)).SelectMany(o => o)
// {1,1,1,2,2,2,3,3,3}

Я группирую их и получаю количество видимости:

Enumerable.Range(1,3).Select(o => Enumerable.Repeat(o, 3)).SelectMany(o => o)
    .GroupBy(o => o).Select(o => new { Qty = o.Count(), Num = o.Key })

Qty   Num
3     1
3     2
3     3

Мне действительно нужно ограничить количество на группу количеством. Если предел равен 2, результат для вышеуказанной группировки будет:

Qty   Num
2     1
1     1
2     2
1     2
2     3
1     3

Итак, если Qty = 10 и лимит равен 4, результат равен 3 строкам (4, 4, 2). Количество каждого числа не равно, как в примере. Указанный лимит количества одинаковый для всего списка (не зависит от числа).

Спасибо

Ответ 1

Был вопрос похожий вопрос, который возник недавно, спрашивая, как это сделать в SQL - нет действительно элегантного решения, и если это не Linq to SQL или Entity Framework ( т.е. будучи переведенным в SQL-запрос), я бы предположил, что вы не пытаетесь решить эту проблему с Linq и вместо этого пишите итерационное решение; это будет намного эффективнее и проще в обслуживании.

Тем не менее, если вы абсолютно должны использовать метод на основе набора ( "Linq" ), это можно сделать так:

var grouped =
    from n in nums
    group n by n into g
    select new { Num = g.Key, Qty = g.Count() };

int maxPerGroup = 2;
var portioned =
    from x in grouped
    from i in Enumerable.Range(1, grouped.Max(g => g.Qty))
    where (x.Qty % maxPerGroup) == (i % maxPerGroup)
    let tempQty = (x.Qty / maxPerGroup) == (i / maxPerGroup) ? 
        (x.Qty % maxPerGroup) : maxPerGroup
    select new
    {
        Num = x.Num,
        Qty = (tempQty > 0) ? tempQty : maxPerGroup
    };

Сравните с более простой и быстрой итеративной версией:

foreach (var g in grouped)
{
    int remaining = g.Qty;
    while (remaining > 0)
    {
        int allotted = Math.Min(remaining, maxPerGroup);
        yield return new MyGroup(g.Num, allotted);
        remaining -= allotted;
    }
}

Ответ 2

Некоторые из других ответов делают запрос LINQ более сложным, чем нужно. Использование цикла foreach, безусловно, быстрее и эффективнее, но альтернатива LINQ по-прежнему довольно проста.

var input = Enumerable.Range(1, 3).SelectMany(x => Enumerable.Repeat(x, 10));
int limit = 4;

var query =
    input.GroupBy(x => x)
         .SelectMany(g => g.Select((x, i) => new { Val = x, Grp = i / limit }))
         .GroupBy(x => x, x => x.Val)
         .Select(g => new { Qty = g.Count(), Num = g.Key.Val });

Ответ 3

Отличный ответ Aaronaught не распространяется на возможность получить лучшее из обоих миров... используя метод расширения для обеспечения итеративного решения.

Непроверенные:

public static IEnumerable<IEnumerable<U>> SplitByMax<T, U>(
  this IEnumerable<T> source,
  int max,
  Func<T, int> maxSelector,
  Func<T, int, U> resultSelector
)
{
  foreach(T x in source)
  {
    int number = maxSelector(x);
    List<U> result = new List<U>();
    do
    {
      int allotted = Math.Min(number, max); 
      result.Add(resultSelector(x, allotted));
      number -= allotted
    } while (number > 0 && max > 0);

    yield return result;
  }
}

Вызывается:

var query = grouped.SplitByMax(
  10,
  o => o.Qty,
  (o, i) => new {Num = o.Num, Qty = i}
)
.SelectMany(split => split);