2010-06-03 30 views
10

tengo una lista de números duplicados:LINQ: GroupBy con el máximo recuento en cada grupo

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

grupo I ellos y obtener la cantidad de ocurrencia:

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 

Lo que realmente necesito es limitar la cantidad por grupo a un cierto número. Si el límite es 2 el resultado para la agrupación anterior sería:

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

lo tanto, si Cantidad = 10 y el límite es 4, el resultado es 3 filas (4, 4, 2). La cantidad de cada número no es igual que en el ejemplo. El límite de cantidad especificado es el mismo para toda la lista (no difiere según el número).

Gracias

+0

Soy curioso. ¿Para qué se usa este algoritmo? – Luke101

+0

Necesito escupir datos en este formato para una máquina CNC. – JKJKJK

Respuesta

4

Hubo un similar question que se acercó preguntando recientemente cómo hacer esto en SQL - no hay una solución muy elegante y menos que esto es LINQ to SQL o Entity Framework (es decir, que se traduce en una consulta SQL), Realmente sugeriría que no trate de resolver este problema con Linq y en su lugar escriba una solución iterativa; va a ser mucho más eficiente y fácil de mantener.

Dicho esto, si es absolutamente necesario utilizar un método basado en conjuntos ("LINQ"), esta es una manera que podría hacerlo:

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

Comparar con la versión más simple y más rápido iterativo:

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

Tiene razón acerca de que el método LINQ es demasiado complejo. Gracias. – JKJKJK

0

La excelente respuesta de Aaronaught no cubre la posibilidad de obtener lo mejor de ambos mundos ... usando un método de extensión para proporcionar una solución iterativa.

No comprobado:

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

Llamado por:

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

Algunas de las otras respuestas están haciendo la consulta LINQ mucho más compleja de lo que debe ser. El uso de un bucle foreach es ciertamente más rápido y más eficiente, pero la alternativa de LINQ sigue siendo bastante sencilla.

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