2010-06-09 70 views
5

Estoy intentando seleccionar un subgrupo de una lista donde los elementos tienen fechas contiguas, p.Use LINQ para agrupar una secuencia por fecha sin espacios

 
ID StaffID Title    ActivityDate 
-- ------- ----------------- ------------ 
1  41 Meeting with John 03/06/2010 
2  41 Meeting with John 08/06/2010 
3  41 Meeting Continues 09/06/2010 
4  41 Meeting Continues 10/06/2010 
5  41 Meeting with Kay  14/06/2010 
6  41 Meeting Continues 15/06/2010 

estoy usando un punto de giro cada vez, así que tome el elemento de pivote como ejemplo 3, me gustaría obtener el siguiente resultado eventos contiguos alrededor del pivote:

 
ID StaffID Title    ActivityDate 
-- ------- ----------------- ------------ 
2  41 Meeting with John 08/06/2010 
3  41 Meeting Continues 09/06/2010 
4  41 Meeting Continues 10/06/2010 

Mi actual implementación es un laborioso "paseo" en el pasado, a continuación, en el futuro, para construir la lista:

var activity = // item number 3: Meeting Continues (09/06/2010) 

var orderedEvents = activities.OrderBy(a => a.ActivityDate).ToArray(); 

// Walk into the past until a gap is found 
var preceedingEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID); 
DateTime dayBefore; 
var previousEvent = activity; 
while (previousEvent != null) 
{ 
    dayBefore = previousEvent.ActivityDate.AddDays(-1).Date; 
    previousEvent = preceedingEvents.TakeWhile(a => a.ID != previousEvent.ID).LastOrDefault(); 
    if (previousEvent != null) 
    { 
     if (previousEvent.ActivityDate.Date == dayBefore) 
      relatedActivities.Insert(0, previousEvent); 
     else 
      previousEvent = null; 
    } 
} 


// Walk into the future until a gap is found 
var followingEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID); 
DateTime dayAfter; 
var nextEvent = activity; 
while (nextEvent != null) 
{ 
    dayAfter = nextEvent.ActivityDate.AddDays(1).Date; 
    nextEvent = followingEvents.SkipWhile(a => a.ID != nextEvent.ID).Skip(1).FirstOrDefault(); 
    if (nextEvent != null) 
    { 
     if (nextEvent.ActivityDate.Date == dayAfter) 
      relatedActivities.Add(nextEvent); 
     else 
      nextEvent = null; 
    } 
} 

la lista relatedActivities entonces debe contener los eventos contiguos, en orden.

¿Existe alguna forma mejor (quizás usando LINQ) para esto?

Tuve una idea de usar .Aggregate() pero no pude pensar en cómo hacer que el agregado salga cuando encuentra un espacio en la secuencia.

+0

¿Cuál es el propósito de 'preceedingEvents.TakeWhile (a => a.ID! = PreviousEvent.ID)'? La identificación parece ser siempre única en tu ejemplo. –

+0

Esa línea simplemente toma el elemento del enumerable que está antes del elemento 'evento anterior '. Piénselo como un método '.Previous()'. – Codesleuth

+0

Se borró un poco la muestra para incluir solo las reuniones. Pensándolo bien, podría ser mejor mostrar 'Licencia anual', pero entiendes lo esencial: – Codesleuth

Respuesta

2

En este caso creo que un foreach bucle estándar es probablemente más fácil de leer que una consulta LINQ:

var relatedActivities = new List<TActivity>(); 
bool found = false; 

foreach (var item in activities.OrderBy(a => a.ActivityDate)) 
{ 
    int count = relatedActivities.Count; 
    if ((count > 0) && (relatedActivities[count - 1].ActivityDate.Date.AddDays(1) != item.ActivityDate.Date)) 
    { 
     if (found) 
      break; 

     relatedActivities.Clear(); 
    } 

    relatedActivities.Add(item); 
    if (item.ID == activity.ID) 
     found = true; 
} 

if (!found) 
    relatedActivities.Clear(); 

Por lo que vale la pena, he aquí una más o menos equivalente - y mucho menos legible - consulta LINQ:

var relatedActivities = activities 
    .OrderBy(x => x.ActivityDate) 
    .Aggregate 
    (
     new { List = new List<TActivity>(), Found = false, ShortCircuit = false }, 
     (a, x) => 
     { 
      if (a.ShortCircuit) 
       return a; 

      int count = a.List.Count; 
      if ((count > 0) && (a.List[count - 1].ActivityDate.Date.AddDays(1) != x.ActivityDate.Date)) 
      { 
       if (a.Found) 
        return new { a.List, a.Found, ShortCircuit = true }; 

       a.List.Clear(); 
      } 

      a.List.Add(x); 
      return new { a.List, Found = a.Found || (x.ID == activity.ID), a.ShortCircuit }; 
     }, 
     a => a.Found ? a.List : new List<TActivity>() 
    ); 
+0

Como ccomet también menciona, los resultados del ejemplo que se muestran en su pregunta no coinciden con su especificación o código. Mi código debe generar los mismos resultados que el suyo (es decir, deben coincidir con su especificación). Probablemente necesite alguna lógica adicional si desea que los resultados coincidan con los resultados de su ejemplo. – LukeH

+0

Esto es mucho, mucho más limpio que el mío. Probablemente mucho más rápido, y definitivamente se ve más elegante. –

+0

"los resultados del ejemplo que se muestran en su pregunta no coinciden con su especificación o código". Lo he estado usando y funciona perfectamente. No estoy seguro de lo que quieres decir con esto. – Codesleuth

2

De alguna manera, no creo que LINQ estuviese destinado a ser usado para búsquedas bidireccionales de profundidad unidimensional, pero construí un LINQ que funcionaba usando Aggregate. Para este ejemplo, voy a usar una lista en lugar de una matriz. Además, voy a utilizar Activity para referirme a cualquier clase en la que esté almacenando los datos. Reemplácela con la que sea apropiada para su código.

Antes de siquiera comenzar, necesitamos una pequeña función para manejar algo. List.Add(T) devuelve nulo, pero queremos ser capaces de acumular en una lista y devolver la nueva lista para esta función agregada. Entonces todo lo que necesitas es una función simple como la siguiente.

private List<T> ListWithAdd<T>(List<T> src, T obj) 
{ 
    src.Add(obj); 
    return src; 
} 

Primero, obtenemos la lista ordenada de todas las actividades, y luego inicializamos la lista de actividades relacionadas. Esta lista inicial contendrá la actividad objetivo solamente, para comenzar.

List<Activity> orderedEvents = activities.OrderBy(a => a.ActivityDate).ToList(); 
List<Activity> relatedActivities = new List<Activity>(); 
relatedActivities.Add(activity); 

Tenemos que dividir esto en dos listas, el pasado y el futuro tal como lo hace actualmente.

Comenzaremos por el pasado, la construcción debería parecerle muy familiar. Luego, agregaremos todo en actividades relacionadas. Esto usa la función ListWithAdd que escribimos anteriormente. Podrías condensarlo en una línea y omitir declarando eventos previos como su propia variable, pero lo mantuve separado para este ejemplo.

A continuación, crearemos los siguientes eventos de forma similar y también los agregaremos.

var nextEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID); 
relatedActivities = nextEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, nextItem) => nextItem.ActivityDate.Subtract(items.OrderBy(a => a.ActivityDate).Last().ActivityDate).Days.Equals(1) ? ListWithAdd(items, nextItem) : items).ToList(); 

Puede ordenar adecuadamente el resultado después, como ahora relatedActivities deben contener todas las actividades sin huecos. No se romperá inmediatamente cuando llegue al primer espacio, no, pero no creo que puedas salir literalmente de un LINQ. Por lo tanto, simplemente ignora todo lo que encuentra pasado un espacio.

Tenga en cuenta que este código de ejemplo solo funciona con la diferencia de tiempo real. El resultado de su ejemplo parece implicar que necesita algunos otros factores de comparación, pero esto debería ser suficiente para comenzar. Simplemente agregue la lógica necesaria a la comparación de resta de fecha en ambas entradas.

+0

"Tu resultado de ejemplo parece implicar que necesitas algunos otros factores de comparación", como afirmo en la pregunta, hay una punto de partida: ID 3. El resultado solo debe tener eventos contiguos al ítem 3. – Codesleuth

+0

LukeH acaba de aclarar esto para mí, lo siento. Cambié la pregunta para incluir solo reuniones ahora. – Codesleuth

+0

@Codesleuth Bueno, siempre que el único requisito sea obtener todas las actividades con un punto de tiempo secuencial sin intervalo que se origine a partir de un índice especificado (al principio, al final o en cualquier punto intermedio), tanto LukeH como mis soluciones harán el truco a la perfección. Pero estoy de acuerdo con mi afirmación de que LukeH es más elegante y que solo requiere un recorrido transversal (el mío requiere 2). –

5

Aquí es una implementación:

public static IEnumerable<IGrouping<int, T>> GroupByContiguous(
    this IEnumerable<T> source, 
    Func<T, int> keySelector 
) 
{ 
    int keyGroup = Int32.MinValue; 
    int currentGroupValue = Int32.MinValue; 
    return source 
    .Select(t => new {obj = t, key = keySelector(t)) 
    .OrderBy(x => x.key) 
    .GroupBy(x => { 
     if (currentGroupValue + 1 < x.key) 
     { 
     keyGroup = x.key; 
     } 
     currentGroupValue = x.key; 
     return keyGroup; 
    }, x => x.obj); 
} 

Y Puede convertir las fechas en enter mediante resta o imaginar una versión DateTime (fácilmente).

Cuestiones relacionadas