Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

I have a List<DateTime> dates;

I have a class that has:

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

I am trying to figure out a clean way to group them.

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

}

Note: if there is an NWD on Friday and the next is Monday they should be grouped. Weekends are not considered.

For example if I have

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

The output would be:

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

Is there any way to do this (without have a bunch of counter variables) and using .Select or .Where or something.

Thanks

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
198 views
Welcome To Ask or Share your Answers For Others

1 Answer

So, we'll start out with this generic iterator function. It takes a sequence and a predicate that accepts two items and returns a boolean. It will read in items from the source and while an item, along with it's previous item, returns true based on the predicate, that next item will be in the "next group". If it returns false, the previous group is full and the next group is started.

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

We'll also need this simple helper method that gets the next working day based on a date. If you want to incorporate holidays as well it goes from trivial to quite hard, but that's where the logic would go.

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

Now to put it all together. First we order the days. (If you ensure they always come in ordered you can remove that part.) Then we group the consecutive items while each item is the next work day of the previous.

Then all we need to do is turn an IEnumerable<DateTime> of consecutive dates into a NonWorkingDay. For that the start date is the first date, and Days is the count of the sequence. While normally using both First and Count would iterate the source sequence twice, we happen to know that the sequence returned by GroupWhile is actually a List under the hood, so iterating it multiple times is not a problem, and getting the Count is even 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(),
                });
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...