27

I am looking for a method of splitting a date range into a series of date ranges by chunk size of days. I am planning on using this to buffer calls to a service which if the date range is too large, the service faults.

This is what I have come up with so far. It seems to work, but I am not sure if it will exit properly. This seems like something that has probably been done several times before, but I can't find it.

public IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
    var newStart = start;
    var newEnd = start.AddDays(dayChunkSize);

    while (true)
    {
        yield return new Tuple<DateTime, DateTime>(newStart, newEnd);

        if (newEnd == end)
            yield break;

        newStart = newStart.AddDays(dayChunkSize);
        newEnd = (newEnd.AddDays(dayChunkSize) > end ? end : newEnd.AddDays(dayChunkSize));
    }
}

I'm looking for improvement suggestions, or "Dude, use this existing function for this!"

Matthew Strawbridge
  • 19,940
  • 10
  • 72
  • 93
Khan
  • 17,904
  • 5
  • 47
  • 59

8 Answers8

50

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);
}
Ben
  • 356
  • 4
  • 12
Tomas Grosup
  • 6,396
  • 3
  • 30
  • 44
  • Thanks for pointing out the bug, Love the clean look of your solution. – Khan Dec 03 '12 at 21:06
  • 1
    Very nice answer. Thank you! I would add as an additional - if you don't want "overlapping" modify the line "yield return Tuple.Create(start,chunkEnd);" to be "chunkEnd.AddDays(-1)". This will ensure that the start/end dates for each row do not overlap. – Matthew M. Oct 22 '13 at 22:37
  • Does not work for me. I am actually using it inside a C# script embedded in an SSIS (ETL tool) package. I get two errors - Error The body of 'CodeHere.csproj.ScriptMain.SplitDateRange(System.DateTime, System.DateTime, int)' cannot be an iterator block because 'IEnumerable>' is not an iterator interface type C:\Users\............blah blah... $$$ AND $$$ The name 'Tuple' does not exist in the current context – Steam Oct 29 '13 at 21:09
  • BTW, I added your code after my main method. Also, I was using System.Tuple and i still get the tuple error. I am new to c#, so please bear with me. – Steam Oct 29 '13 at 21:10
  • ^^^^^^^^^^^^^^^^^^ This code works only for .NET 4.0 and above. How to make it work for 3.5 and below ? – Steam Oct 29 '13 at 22:30
  • public static IEnumerable> SplitDateRange(DateTime start, DateTime end, int dayChunkSize) { DateTime chunkEnd; while ((chunkEnd = start.AddDays(dayChunkSize)) < end) { yield return Tuple.Create(start, chunkEnd); start = chunkEnd.AddDays(1); } yield return Tuple.Create(start, end); } – Oduwole Oluwasegun Feb 28 '19 at 13:50
  • You can make this more generic by refactoring dayChunkSize to be a TimeSpan interval – atomaras Jul 09 '19 at 19:14
2

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.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
RAS
  • 3,375
  • 15
  • 24
2

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.

Matthew Strawbridge
  • 19,940
  • 10
  • 72
  • 93
2

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);
}
RredCat
  • 5,259
  • 5
  • 60
  • 100
0

If you know how many chunks/intervals/periods/parts you want to split your time range into, I've found the following to be helpful

You can use the DateTime.Ticks property to define your intervals, and then create a series of DateTime objects based on your defined interval:

IEnumerable<DateTime> DivideTimeRangeIntoIntervals(DateTime startTS, DateTime endTS, int numberOfIntervals)
{
    long startTSInTicks = startTS.Ticks;
    long endTsInTicks = endTS.Ticks;
    long tickSpan = endTS.Ticks - startTS.Ticks;
    long tickInterval = tickSpan / numberOfIntervals;

    List<DateTime> listOfDates = new List<DateTime>();
    for (long i = startTSInTicks; i <= endTsInTicks; i += tickInterval)
    {
        listOfDates.Add(new DateTime(i));
    }
    return listOfDates;
}

You can convert that listOfDates into however you want to represent a timerange (a tuple, a dedicated date range object, etc). You can also modify this function to directly return it in the form you need it.

BobbyA
  • 2,090
  • 23
  • 41
0

There are a lot of corner cases that are unhandled in the answers so far. And it's not entirely clear how you would want to handle them. Do you want overlapping start/end of ranges? Is there a minimum range size? Below is some code that'll handle some of the corner cases, you'll have to think about overlapping especially and possibly push the start/end of ranges by a few seconds or maybe more depending on the data you're returning.

    public static IEnumerable<(DateTime start, DateTime end)> PartitionDateRange(DateTime start,
                                                                                DateTime end,
                                                                                int chunkSizeInDays)
    {
        if (start > end)
            yield break;

        if (end - start < TimeSpan.FromDays(chunkSizeInDays))
        {
            yield return (start, end);
            yield break;
        }

        DateTime e = start.AddDays(chunkSizeInDays);

        for (;e < end; e = e.AddDays(chunkSizeInDays))
        {
            yield return (e.AddDays(-chunkSizeInDays), e);
        }

        if (e < end && end - e > TimeSpan.FromMinutes(1))
            yield return (e, end);
    }

Example call:

    static void Main(string[] _)
    {
        Console.WriteLine("expected");

        DateTime start = DateTime.Now - TimeSpan.FromDays(10);
        DateTime end = DateTime.Now;

        foreach (var range in PartitionDateRange(start, end, 2))
        {
            Console.WriteLine($"{range.start} to {range.end}");
        }

        Console.WriteLine("start > end");

        start = end + TimeSpan.FromDays(1);

        foreach (var range in PartitionDateRange(start, end, 2))
        {
            Console.WriteLine($"{range.start} to {range.end}");
        }

        Console.WriteLine("less than partition size");

        start = end - TimeSpan.FromDays(1);

        foreach (var range in PartitionDateRange(start, end, 2))
        {
            Console.WriteLine($"{range.start} to {range.end}");
        }
    }
MikeJ
  • 1,299
  • 7
  • 10
0

The accepted solution looks good in most cases. If you need to take away overlap on the beginning and the end of each chunk, then this might work better.

public static IEnumerable<(DateTime FromDate, DateTime ToDate)> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
        {
            DateTime chunkEnd;
            while ((chunkEnd = start.AddDays(dayChunkSize-1)) < end)
            {
                yield return (start, chunkEnd);
                start = chunkEnd.AddDays(1);
            }
            yield return (start, end);
        }
Adel Tabareh
  • 1,478
  • 14
  • 10
0

hare is an example spliced by month

IEnumerable<(DateTime, DateTime)> SplitDateRange(DateTime start, DateTime end, int monthChunkSize)
{
    DateTime dateEnd=DateTime.Parse(end.ToString());
    
    for (int i = 0;start.AddMonths(i) < dateEnd; i+=monthChunkSize)
    {
            end = start.AddMonths(i+monthChunkSize);
             start.AddMonths(i);
        
        yield return (start.AddMonths(i), end<dateEnd?end:dateEnd);     
    }
}