2010-06-09 20 views
5

Sto provando a selezionare un sottogruppo di un elenco in cui gli articoli contengano date contigue, ad es.Utilizzare LINQ per raggruppare una sequenza per data senza spazi vuoti

 
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 

sto usando un punto di rotazione di volta in volta, in modo da prendere l'elemento esempio pivot come 3, mi piacerebbe ottenere la seguente risultante eventi contigui in tutto il pivot:

 
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 

Il mio attuale implementazione è una laboriosa "passeggiata" in passato, poi verso il futuro, per creare l'elenco:

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

l'elenco relatedActivities dovrebbe quindi contenere gli eventi contigui, in ordine.

C'è un modo migliore (forse utilizzando LINQ) per questo?

Ho avuto l'idea di usare .Aggregate() ma non potrebbe pensare come raggiungere l'aggregato di uscire quando trova un gap nella sequenza.

+0

Qual è lo scopo di 'preceedingEvents.TakeWhile (a => a.ID! = PreviousEvent.ID)'? L'ID sembra essere sempre unico nel tuo esempio. –

+0

Questa riga prende semplicemente l'oggetto dall'enumerabile che precede l'elemento 'previousEvent'. Pensalo come un metodo '.Precedente()'. – Codesleuth

+0

Completato l'esempio per includere solo le riunioni. A pensarci bene, potrebbe essere meglio mostrare "Congedo annuale", ma mi viene l'idea che spero: s – Codesleuth

risposta

2

In questo caso penso che un foreach ciclo standard è probabilmente più leggibile di una query 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(); 

Per quello che vale, ecco una più o meno equivalente - e di gran lunga meno leggibile - query 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

Come ccomet menziona anche, i risultati di esempio mostrati nella domanda non corrispondono alle specifiche o al codice. Il mio codice dovrebbe generare gli stessi risultati del tuo (cioè, dovrebbero corrispondere alle tue specifiche). Probabilmente avrai bisogno di una logica aggiuntiva se vuoi che i risultati corrispondano ai tuoi risultati di esempio. – LukeH

+0

Questo è molto, molto più pulito del mio. Probabilmente molto più veloce, e sicuramente sembra più elaborato. –

+0

"i risultati di esempio mostrati nella tua domanda non corrispondono alle tue specifiche o al codice" beh, l'ho usato e funziona perfettamente. Non sono sicuro di cosa intendi con questo. – Codesleuth

2

In qualche modo, non penso che LINQ fosse veramente destinato a essere utilizzato per ricerche bidirezionali-unidimensionale-profondità-prima, ma ho costruito un LINQ funzionante utilizzando Aggregazione. Per questo esempio ho intenzione di utilizzare una lista invece di una matrice. Inoltre, ho intenzione di usare Activity per riferirsi a qualsiasi classe si archiviano i dati in. Sostituirlo con tutto ciò che è appropriato per il codice.

Prima ancora di iniziare, abbiamo bisogno di una piccola funzione per gestire qualcosa. List.Add(T) restituisce null, ma vogliamo essere in grado di accumularsi in un elenco e restituire il nuovo elenco per questa funzione di aggregazione. Quindi tutto ciò di cui hai bisogno è una semplice funzione come la seguente.

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

Innanzitutto, viene visualizzato l'elenco ordinato di tutte le attività, quindi viene inizializzato l'elenco delle attività correlate. Questo elenco iniziale conterrà solo l'attività di destinazione, per iniziare.

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

Dobbiamo rompere questo in due liste, il passato e il futuro, proprio come si fa attualmente.

Inizieremo con il passato, la costruzione dovrebbe sembrare per lo più familiare. Quindi lo aggregeremo in attività correlate. Questo usa la funzione ListWithAdd che abbiamo scritto in precedenza. Si potrebbe condensare in una riga e saltare dichiarando precedentiEventi come una propria variabile, ma l'ho tenuta separata per questo esempio.

Avanti, costruiremo i seguenti eventi in modo simile, e allo stesso modo aggregare esso.

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

È possibile ordinare correttamente il risultato dopo, come ora relatedActivities devono contenere tutte le attività senza spazi. Non si romperà immediatamente quando colpisce il primo spazio, no, ma non penso che tu possa letteralmente uscire da un LINQ. Quindi, invece, ignora semplicemente tutto ciò che trova oltre un varco.

Si noti che questo codice di esempio funziona solo sulla differenza effettiva nel tempo. Il tuo output di esempio sembra implicare che hai bisogno di altri fattori di confronto, ma questo dovrebbe essere sufficiente per iniziare. Basta aggiungere la logica necessaria al confronto della sottrazione della data in entrambe le voci.

+0

"Il tuo output di esempio sembra implicare che hai bisogno di altri fattori di confronto" come dichiaro nella domanda, c'è un punto di partenza: ID 3. Il risultato dovrebbe avere solo eventi contigui dall'articolo 3. – Codesleuth

+0

LukeH ha appena chiarito per me, mi dispiace. Ho cambiato la domanda per includere solo le riunioni ora. – Codesleuth

+0

@Codesleuth Bene, finché l'unico requisito è ottenere tutte le attività con un punto di tempo sequenziale senza pause proveniente da un indice specificato (all'inizio, alla fine o in qualsiasi punto nel mezzo), LukeH e le mie soluzioni faranno entrambe il trucco perfettamente. Ma sostengo la mia affermazione secondo cui LukeH's è più elegante, e richiede anche solo un attraversamento (il mio richiede 2). –

5

Ecco un'implementazione:

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 puoi convertire le date in int per mezzo di sottrazione, o immaginare una versione DateTime (facilmente).

Problemi correlati