2012-07-03 11 views
5

Tengo una IQueryable(Of Job) donde tiene empleo, entre otras cosas:Agrupación preservando ordenación en LINQ

Property CreatedOn as DateTime 
Property JobType as JobTypes 

Enum JobTypes 
    JobType1 
    JobType2 
    JobType3 
End Enum 

Lo que quiero salir de ella es una lista, ordenada por CreatedOn, a continuación, agrupados por JobType con un recuento de

Ej Say Tengo (fechas abreviadas)

11:00 JobType1 
11:01 JobType2 
11:02 JobType2 
11:03 JobType2 
11:04 JobType2 
11:05 JobType3 
11:06 JobType1 
11:07 JobType1 

Quiero

JobType1 1 
JobType2 4 
JobType3 1 
JobType1 2 

No sé cómo tener en cuenta los pedidos al agruparlos. ¿alguien me puede indicar el camino correcto para hacer esto? Preferiría preferiría una sintaxis fluida. VB.Net o C# está bien.

+2

No debería el último elemento de los resultados sea JobType1? – Hogan

+0

@Hogan Copiar y pegar el error tipográfico, gracias lo arreglaré – Basic

+0

Posible duplicado: http://stackoverflow.com/questions/2194761/can-i-use-linq-to-retrieve-only-change-values ​​ – Hogan

Respuesta

3

Este truco es bastante fácil de entrenar LinqToObjects hacer:

public static IEnumerable<IGrouping<TKey, TSource>> GroupContiguous<TKey, TSource>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector) 
{ 
    bool firstIteration = true; 
    MyCustomGroupImplementation<TKey, TSource> currentGroup = null; 

    foreach (TSource item in source) 
    { 
    TKey key = keySelector(item); 
    if (firstIteration) 
    { 
     currentGroup = new MyCustomGroupImplementation<TKey, TSource>(); 
     currentGroup.Key = key; 
     firstIteration = false; 
    } 
    else if (!key.Equals(currentGroup.Key)) 
    { 
     yield return currentGroup; 
     currentGroup = new MyCustomGroupImplementation<TKey, TSource>(); 
     currentGroup.Key = key; 
    } 
    currentGroup.Add(item); 
    } 
    if (currentGroup != null) 
    { 
    yield return currentGroup; 
    } 
} 

public class MyCustomGroupImplementation<TKey, TSource> : IGrouping<TKey, TSource> 
{ 
    //TODO implement IGrouping and Add 
} 

utilizado por

IEnumerable<IGrouping<JobType, Job> query = Jobs 
    .OrderBy(j => j.CreatedOn) 
    .GroupContiguous(j => j.JobType); 

No es tan fácil de hacer un "mirar a la fila anterior" con sólo cualquier proveedor de linq antiguo Espero que no tengas que enseñar a LinqToSql o LinqToEntities cómo hacer esto.

+0

En realidad es Entidades pero no tengo ningún problema con '.ToList()' ing y hacerlo en memoria - no es una lista enorme (con suerte no más de unos cientos de objetos) y ya estoy recuperando la lista completa para otras razones. Voy a darle un giro – Basic

+0

Así es como hacer esto. +1. – Avish

+1

¿Qué es MyCustomGroupImplementation? – Hogan

2

Aquí hay un enfoque razonable que utiliza el método Aggregrate.

Si usted comienza con una lista de JobTypes así:

var jobTypes = new [] 
{ 
    JobTypes.JobType1, 
    JobTypes.JobType2, 
    JobTypes.JobType2, 
    JobTypes.JobType2, 
    JobTypes.JobType2, 
    JobTypes.JobType3, 
    JobTypes.JobType1, 
    JobTypes.JobType1, 
}; 

Usted puede utilizar Aggregate definiendo en primer lugar el acumulador de este modo:

var accumulator = new List<KeyValuePair<JobTypes, int>>() 
{ 
    new KeyValuePair<JobTypes, int>(jobTypes.First(), 0), 
}; 

Entonces la llamada Aggregate método es el siguiente:

var results = jobTypes.Aggregate(accumulator, (a, x) => 
{ 
    if (a.Last().Key == x) 
    { 
     a[a.Count - 1] = 
      new KeyValuePair<JobTypes, int>(x, a.Last().Value + 1); 
    } 
    else 
    { 
     a.Add(new KeyValuePair<JobTypes, int>(x, 1)); 
    } 
    return a; 
}); 

Y aleta aliado llamar a esto le da este resultado:

Job Types Results

simple, una especie de ...

+0

¿Está solucionando el problema ordenando por fecha, ignorando la fecha y simplemente agregando en el Tipo de trabajo? Lo siento, no estoy del todo claro cómo funciona tu enfoque – Basic

+0

@Basic: supongo que ya has consultado tus datos y los tienes ordenados según la fecha. La agrupación que está haciendo solo requiere un enumerable de tipos de trabajo. – Enigmativity

+0

Gracias Enigmatividad, al final fui con la respuesta de David, pero tu respuesta ciertamente amplió mi comprensión de los agregadores. +1 – Basic

1

La respuesta no LINQ

Dada

public enum JobTypes 
{ 
    JobType1, 
    JobType2, 
    JobType3 
} 

public class Job 
{ 
    public JobTypes JobType { get; set; } 
    public DateTime CreatedOn { get; set; } 
} 

public class JobSummary 
{ 
    public JobSummary(JobTypes jobType, long count) 
    { 
     this.JobType = jobType; 
     this.Count = count; 
    } 

    public JobTypes JobType { get; set; } 
    public long Count { get; set; } 
} 

entonces se podría

private List<JobSummary> GetOrderedSummary(List<Job> collection) 
{ 
    var result = new List<JobSummary>(); 
    if (!collection.Any()) 
    { 
     return result; 
    } 
    var orderedCollection = collection.OrderBy(j => j.CreatedOn); 
    var temp = orderedCollection.First(); 
    var count = 1; 

    foreach (var job in orderedCollection.Skip(1)) 
    { 
     if (temp.JobType == job.JobType) 
     { 
      count++; 
      continue; 
     } 

     result.Add(new JobSummary(temp.JobType, count)); 
     temp = job; 
     count = 1; 
    } 

    result.Add(new JobSummary(temp.JobType, count)); 

    return result; 
} 

usando

private void DoSomething() 
{ 
    var collection = new List<Job> 
    { 
     new Job{JobType = JobTypes.JobType1, CreatedOn = DateTime.Now}, 
     new Job{JobType = JobTypes.JobType2, CreatedOn = DateTime.Now.AddSeconds(1)}, 
     new Job{JobType = JobTypes.JobType2, CreatedOn = DateTime.Now.AddSeconds(2)}, 
     new Job{JobType = JobTypes.JobType2, CreatedOn = DateTime.Now.AddSeconds(3)}, 
     new Job{JobType = JobTypes.JobType2, CreatedOn = DateTime.Now.AddSeconds(4)}, 
     new Job{JobType = JobTypes.JobType3, CreatedOn = DateTime.Now.AddSeconds(5)}, 
     new Job{JobType = JobTypes.JobType3, CreatedOn = DateTime.Now.AddSeconds(6)}, 
     new Job{JobType = JobTypes.JobType1, CreatedOn = DateTime.Now.AddSeconds(7)}, 
     new Job{JobType = JobTypes.JobType1, CreatedOn = DateTime.Now.AddSeconds(8)}, 
    }; 

    var summary = GetOrderedSummary(collection); 

} 
+0

Gracias por la sugerencia, pero esto es más detallado de lo que esperaba y, como se mencionó, no es una solución LINQ. Dicho esto, bienvenido a SO y espero verte en el futuro. + – Basic

+0

de nada oh, también encontré http://tomasp.net/blog/custom-linq-grouping.aspx podría ser interesante – G2Mula

2

Esta versión actualizada utiliza una subrutina para hacer lo mismo que antes, pero no necesita el campo interno adicional. (He guardado mi versión anterior, que, para evitar el uso de una rutina Zip, necesitaba el campo adicional OrDer.)

Option Explicit On 
Option Strict On 
Option Infer On 
Imports so11310237.JobTypes 
Module so11310237 
Enum JobTypes 
    JobType1 
    JobType2 
    JobType3 
End Enum 
Sub Main() 
Dim data = {New With{.CO=#11:00#, .JT=JobType1, .OD=0}, 
    New With{.CO=#11:03#, .JT=JobType2, .OD=0}, 
    New With{.CO=#11:05#, .JT=JobType3, .OD=0}, 
    New With{.CO=#11:02#, .JT=JobType2, .OD=0}, 
    New With{.CO=#11:06#, .JT=JobType1, .OD=0}, 
    New With{.CO=#11:01#, .JT=JobType2, .OD=0}, 
    New With{.CO=#11:04#, .JT=JobType2, .OD=0}, 
    New With{.CO=#11:07#, .JT=JobType1, .OD=0}} 

' Check that there's any data to process 
If Not data.Any Then Exit Sub 

' Both versions include a normal ordering first. 
Dim odata = From q In data Order By q.CO 

' First version here (and variables reused below): 

Dim ljt = odata.First.JT 

Dim c = 0 
For Each o In odata 
    If ljt <> o.JT Then 
    ljt = o.JT 
    c += 1 
    End If 
    o.OD = c 
Next 

For Each p In From q In data Group By r=q.JT, d=q.OD Into Count() 
    Console.WriteLine(p) 
Next 

Console.WriteLine() 

' New version from here: 

' Reset variables (still needed :-() 
ljt = odata.First.JT 
c = 0 
For Each p In From q In odata Group By r=q.JT, d=IncIfNotEqual(c,q.JT,ljt) Into Count() 
    Console.WriteLine(p) 
Next 

End Sub 

Function IncIfNotEqual(Of T)(ByRef c As Integer, ByVal Value As T, ByRef Cmp As T) As Integer 
If Not Object.Equals(Value, Cmp) Then 
    Cmp = Value 
    c += 1 
End If 
Return c 
End Function 

End Module 
+0

Gracias Mark pero si bien aprecio la brevedad, no es muy fácil de mantener. También me obliga a usar un tipo intermedio o modificar mi clase existente para tener una propiedad '.OD' que no tendría sentido en ningún otro lugar de mi código. – Basic

+0

Actualizado para que no necesite tampoco, solo las dos variables locales que tendrían que restablecerse (bueno, la 'c' no tendría que reiniciarse) si la enumeración debía enumerarse varias veces. –

1

La respuesta LINQ

public enum JobTypes 
{ 
    JobType1, 
    JobType2, 
    JobType3 
} 

static void Main(string[] args) 
{ 
    var collection = new[] 
    { 
     new {JobType = JobTypes.JobType1, CreatedOn = DateTime.Now}, 
     new {JobType = JobTypes.JobType2, CreatedOn = DateTime.Now.AddSeconds(1)}, 
     new {JobType = JobTypes.JobType2, CreatedOn = DateTime.Now.AddSeconds(2)}, 
     new {JobType = JobTypes.JobType2, CreatedOn = DateTime.Now.AddSeconds(3)}, 
     new {JobType = JobTypes.JobType2, CreatedOn = DateTime.Now.AddSeconds(4)}, 
     new {JobType = JobTypes.JobType3, CreatedOn = DateTime.Now.AddSeconds(5)}, 
     new {JobType = JobTypes.JobType1, CreatedOn = DateTime.Now.AddSeconds(7)}, 
     new {JobType = JobTypes.JobType1, CreatedOn = DateTime.Now.AddSeconds(8)} 
    }; 

    var orderedCollection = collection.OrderBy(job => job.CreatedOn); 
    var temp = orderedCollection.First().JobType; 
    var identifier = 0; 
    var summary = orderedCollection.Select(job => 
    { 
     if (job.JobType == temp) 
     { 
      return new { JobType = job.JobType, Id = identifier }; 
     } 

     temp = job.JobType; 
     return new { JobType = job.JobType, Id = ++identifier }; 
    }).GroupBy(job => new { job.JobType, job.Id }).Select(job => new { JobType = job.Key.JobType, Count = job.Count() }); 

    foreach (var sum in summary) 
    { 
     Console.WriteLine("JobType: {0}, Count: {1}", sum.JobType, sum.Count); 
    } 

    Console.ReadLine(); 
} 
+0

+1 Es un buen enfoque, gracias. Ya implementé el método de David B, así que lo dejaré como aceptado, pero es una buena alternativa, gracias – Basic