Reputation: 9153
Edit It looks like creating a table that holds the DateTimes by minutes to join against would make the most sense. 100 years worth of minutes is ~52M rows. Indexed by the Ticks should make the query run pretty fast. It now becomes
Thanks for the feedback everyone!
I have a class called Recurrence that looks like this:
public class Recurrence
{
public int Id { get; protected set; }
public DateTime StartDate { get; protected set; }
public DateTime? EndDate { get; protected set; }
public long? RecurrenceInterval { get; protected set; }
}
It is an entity framework POCO class. There are two things I want to do with this class all with standard query operators. (So that the query runs entirely server side).
First I want to create a query that returns all the dates from the start date to the end date inclusive with the given recurrence interval. The iterative function is simple
for(i=StartDate.Ticks; i<=EndDate.Ticks; i+=RecurrenceInterval)
{
yield return new DateTime(i);
}
Enumerable.Range() would be an option but there is no long version of Range. I'm thinking my only option here is Aggregate but I'm still not very strong with that function.
Finally once I have that query working, I want to return the values from there that are within a time window i.e. between a different start and end date. That is easy enough to do using SkipWhile/TakeWhile.
Here's how I could do it if DateTime.Ticks was an int
from recurrence in Recurrences
let range =
Enumerable
.Range(
(int)recurrence.StartDate.Ticks,
recurrence.EndDate.HasValue ? (int)recurrence.EndDate.Value.Ticks : (int)end.Ticks)
.Where(i=>i-(int)recurrence.StartDate.Ticks%(int)recurrence.RecurrenceLength.Value==0)
.SkipWhile(d => d < start.Ticks)
.TakeWhile(d => d <= end.Ticks)
from date in range
select new ScheduledEvent { Date = new DateTime(date) };
I guess what I need is an implementation of LongRange that could execute over an EF Query.
Upvotes: 1
Views: 2594
Reputation: 112324
You could create your own date range method
public static class EnumerableEx
{
public static IEnumerable<DateTime> DateRange(DateTime startDate, DateTime endDate, TimeSpan intervall)
{
for (DateTime d = startDate; d <= endDate; d += intervall) {
yield return d;
}
}
}
Then query with
var query =
from recurrence in Recurrences
from date in EnumerableEx.DateRange(recurrence.StartDate,
recurrence.EndDate ?? end,
recurrence.RecurrenceInterval)
select new ScheduledEvent { Date = date };
This assumes that RecurrenceInterval
is declared as TimeSpan
and end
as DateTime
.
EDIT: Would this version restrict the recurrences on the server side as you excpect?
var query =
from recurrence in Recurrences
where
recurrence.StartDate <= end &&
(recurrence.EndDate != null && recurrence.EndDate.Value >= start ||
recurrence.EndDate == null)
from date in EnumerableEx.DateRange(
recurrence.StartDate,
recurrence.EndDate.HasValue && recurrence.EndDate.Value < end ? recurrence.EndDate.Value : end,
recurrence.RecurrenceInterval)
where (date >= start)
select new ScheduledEvent { Date = date };
Here the returned recurrences already take in account the start
and the end
date, thus not returning obsolete recurrences. EnumerableEx.DateRange
has no effect on the first part of the query.
Upvotes: 3
Reputation: 261
Here's the function that yields the intersection of the Recurrence points and a specified subinterval:
public class Recurrence
{
public int Id { get; protected set; }
public DateTime StartDate { get; protected set; }
public DateTime? EndDate { get; protected set; }
public long? RecurrenceInterval { get; protected set; }
// returns the set of DateTimes within [subStart, subEnd] that are
// of the form StartDate + k*RecurrenceInterval, where k is an Integer
public IEnumerable<DateTime> GetBetween(DateTime subStart, DateTime subEnd)
{
long stride = RecurrenceInterval ?? 1;
if (stride < 1)
throw new ArgumentException("Need a positive recurrence stride");
long realStart, realEnd;
// figure out where we really need to start
if (StartDate >= subStart)
realStart = StartDate.Ticks;
else
{
long rem = subStart.Ticks % stride;
if (rem == 0)
realStart = subStart.Ticks;
else
// break off the incomplete stride and add a full one
realStart = subStart.Ticks - rem + stride;
}
// figure out where we really need to stop
if (EndDate <= subEnd)
// we know EndDate has a value. Null can't be "less than" something
realEnd = EndDate.Value.Ticks;
else
{
long rem = subEnd.Ticks % stride;
// break off any incomplete stride
realEnd = subEnd.Ticks - rem;
}
if (realEnd < realStart)
yield break; // the intersection is empty
// now yield all the results in the intersection of the sets
for (long t = realStart; t <= realEnd; t += stride)
yield return new DateTime(t);
}
}
Upvotes: 2