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 th
There are a couple of problems with your solution:
newEnd == end
may never be true, so the while
could loop foreverwhile(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);
}
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}");
}
}
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.
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);
}
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.