2013-03-14 7 views
13

Ich habe einen List<DateTime> dates;Gruppierung Contiguous Termine

Ich habe eine Klasse, die hat:

class NonWorkingDay 
{ 
    public DateTime Start; 
    public int Days; 
} 

Ich versuche, ihnen eine saubere Art und Weise zu gruppieren, um herauszufinden.

public List<NonWorkingDay> GetContiguousDates(List<DateTime> dates) 
{ 

} 

Hinweis: Wenn es am Freitag einen NWD gibt und der nächste Montag ist, sollten sie gruppiert werden. Wochenenden werden nicht berücksichtigt.

Zum Beispiel, wenn ich

September 3 2013 
September 20 2013 
September 23 2013 
September 24 2013 
September 30 2013 
October 1 2013 

Der Ausgang würde sein:

Start = September 3 2013, Days = 1 
Start = September 20 2013, Days = 3 //weekend got skipped 
Start = September 30 2013, Days = 2 

Gibt es eine Möglichkeit, dies zu tun (ohne eine Reihe von Zählervariablen haben) und mit .Select oder. Wo oder so. So

Dank

+4

Schönes Puzzle! ... – spender

+0

Könnten Sie GroupBy Woche und dann die Elemente in der Gruppe zählen? Gruppierung nach Wochen finden Sie hier http://stackoverflow.com/questions/8561782/how-to-group-dates-by-week – bUKaneer

+0

Nein, denn es könnte sagen, 2,5 Wochen Montag-Freitag drin – jmasterx

Antwort

17

, werden wir mit dieser generischen Iterator Funktion beginnen. Es benötigt eine Sequenz und ein Prädikat, das zwei Elemente akzeptiert und einen booleschen Wert zurückgibt. Es liest Elemente aus der Quelle ein und während ein Element zusammen mit seinem vorherigen Element basierend auf dem Prädikat "true" zurückgibt, wird das nächste Element in der "nächsten Gruppe" sein. Wenn sie false zurückgibt, ist die vorherige Gruppe voll und die nächste Gruppe wird gestartet.

public static IEnumerable<IEnumerable<T>> GroupWhile<T>(this IEnumerable<T> source 
    , Func<T, T, bool> predicate) 
{ 
    using (var iterator = source.GetEnumerator()) 
    { 
     if (!iterator.MoveNext()) 
      yield break; 

     List<T> currentGroup = new List<T>() { iterator.Current }; 
     while (iterator.MoveNext()) 
     { 
      if (predicate(currentGroup.Last(), iterator.Current)) 
       currentGroup.Add(iterator.Current); 
      else 
      { 
       yield return currentGroup; 
       currentGroup = new List<T>() { iterator.Current }; 
      } 
     } 
     yield return currentGroup; 
    } 
} 

Wir brauchen auch diese einfache Hilfsmethode, die den nächsten Arbeitstag basierend auf einem Datum erhält. Wenn Sie auch Ferien integrieren wollen, geht es von trivial zu ziemlich hart, aber das ist, wo die Logik gehen würde.

public static DateTime GetNextWorkDay(DateTime date) 
{ 
    DateTime next = date.AddDays(1); 
    if (next.DayOfWeek == DayOfWeek.Saturday) 
     return next.AddDays(2); 
    else if (next.DayOfWeek == DayOfWeek.Sunday) 
     return next.AddDays(1); 
    else 
     return next; 
} 

Nun, um alles zusammen zu setzen. Zuerst bestellen wir die Tage. (Wenn Sie sicherstellen, dass sie immer bestellt werden, können Sie diesen Teil entfernen.) Dann gruppieren wir die aufeinanderfolgenden Artikel, während jeder Artikel der nächste Arbeitstag des vorherigen ist.

Dann müssen wir nur einen IEnumerable<DateTime> von aufeinanderfolgenden Daten in eine NonWorkingDay drehen. Das Startdatum ist das erste Datum und Days ist die Anzahl der Sequenz. Während normalerweise beide First und Count iterieren die Quellsequenz zweimal, wir wissen, dass die von GroupWhile zurückgegebene Sequenz ist eigentlich eine List unter der Haube, so iterieren es mehrmals ist kein Problem, und die Count ist sogar O (1).

public IEnumerable<NonWorkingDay> GetContiguousDates(IEnumerable<DateTime> dates) 
{ 
    return dates.OrderBy(d => d) 
      .GroupWhile((previous, next) => GetNextWorkDay(previous).Date == next.Date) 
      .Select(group => new NonWorkingDay 
       { 
        Start = group.First(), 
        Days = group.Count(), 
       }); 
} 
+5

Absolute Genie, gut gemacht Herr! ; o) – bUKaneer

+0

@plutonix einige sind vielleicht nicht sofort offensichtlich, aber in diesem Fall ist es praktisch nur Kopieren/Einfügen ... – Servy