Split date range into date range chunks

Your code looks fine for me. I don't really like the idea of while(true)
But other solution would be to use enumerable.Range:

public static IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
    return Enumerable
          .Range(0, (Convert.ToInt32((end - start).TotalDays) / dayChunkSize +1))
          .Select(x => Tuple.Create(start.AddDays(dayChunkSize * (x)), start.AddDays(dayChunkSize * (x + 1)) > end
                                                                       ? end : start.AddDays(dayChunkSize * (x + 1))));
}  

or also, this will also work:

public static IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
    var dateCount = (end - start).TotalDays / 5;
    for (int i = 0; i < dateCount; i++)
    {
        yield return Tuple.Create(start.AddDays(dayChunkSize * i)
                                , start.AddDays(dayChunkSize * (i + 1)) > end 
                                 ? end : start.AddDays(dayChunkSize * (i + 1)));
    }
}

I do not have any objects for any of the implementations. They are practically identical.


There are a couple of problems with your solution:

  • the test newEnd == end may never be true, so the while could loop forever (I now see that this condition should always be triggered, but it wasn't obvious on first reading of the code; the while(true) feels a bit dangerous still)
  • AddDays is called three times for each iteration (minor performance issue)

Here is an alternative:

public IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
    DateTime startOfThisPeriod = start;
    while (startOfThisPeriod < end)
    {
        DateTime endOfThisPeriod = startOfThisPeriod.AddDays(dayChunkSize);
        endOfThisPeriod = endOfThisPeriod < end ? endOfThisPeriod : end;
        yield return Tuple.Create(startOfThisPeriod, endOfThisPeriod);
        startOfThisPeriod = endOfThisPeriod;
    }
}

Note that this truncates the last period to end on end as given in the code in the question. If that's not needed, the second line of the while could be omitted, simplifying the method. Also, startOfThisPeriod isn't strictly necessary, but I felt that was clearer than reusing start.


With respect to accepted answer you could use the short form of tuples:

private static IEnumerable<(DateTime, DateTime)> GetDateRange1(DateTime startDate, DateTime endDate, int daysChunkSize)
{
    DateTime markerDate;

    while ((markerDate = startDate.AddDays(daysChunkSize)) < endDate)
    {
        yield return (startDate, markerDate);
        startDate = markerDate;
    }

    yield return (startDate, endDate);
}

But I prefer to use named tuples:

private static IEnumerable<(DateTime StartDate, DateTime EndDate)> GetDateRange(DateTime startDate, DateTime endDate, int daysChunkSize)
{
    DateTime markerDate;

    while ((markerDate = startDate.AddDays(daysChunkSize)) < endDate)
    {
        yield return (StartDate: startDate, EndDate: markerDate);
        startDate = markerDate;
    }

    yield return (StartDate: startDate, EndDate: endDate);
}

I think your code fails when the difference between start and end is smaller than dayChunkSize. See this:

var singleRange = SplitDateRange(DateTime.Now, DateTime.Now.AddDays(7), dayChunkSize: 15).ToList();
Debug.Assert(singleRange.Count == 1);

Proposed solution:

public static IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
    DateTime chunkEnd;
    while ((chunkEnd = start.AddDays(dayChunkSize)) < end)
    {
        yield return Tuple.Create(start, chunkEnd);
        start = chunkEnd;
    }
    yield return Tuple.Create(start, end);
}

Tags:

C#

.Net