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.
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);
}
}
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.
Related
I need to know if a Date is between a DateRange. I have three dates:
// The date range
DateTime startDate;
DateTime endDate;
DateTime dateToCheck;
The easy solution is doing a comparison, but is there a smarter way to do this?
Nope, doing a simple comparison looks good to me:
return dateToCheck >= startDate && dateToCheck < endDate;
Things to think about though:
DateTime is a somewhat odd type in terms of time zones. It could be UTC, it could be "local", it could be ambiguous. Make sure you're comparing apples with apples, as it were.
Consider whether your start and end points should be inclusive or exclusive. I've made the code above treat it as an inclusive lower bound and an exclusive upper bound.
Usually I create Fowler's Range implementation for such things.
public interface IRange<T>
{
T Start { get; }
T End { get; }
bool Includes(T value);
bool Includes(IRange<T> range);
}
public class DateRange : IRange<DateTime>
{
public DateRange(DateTime start, DateTime end)
{
Start = start;
End = end;
}
public DateTime Start { get; private set; }
public DateTime End { get; private set; }
public bool Includes(DateTime value)
{
return (Start <= value) && (value <= End);
}
public bool Includes(IRange<DateTime> range)
{
return (Start <= range.Start) && (range.End <= End);
}
}
Usage is pretty simple:
DateRange range = new DateRange(startDate, endDate);
range.Includes(date)
You could use extension methods to make it a little more readable:
public static class DateTimeExtensions
{
public static bool InRange(this DateTime dateToCheck, DateTime startDate, DateTime endDate)
{
return dateToCheck >= startDate && dateToCheck < endDate;
}
}
Now you can write:
dateToCheck.InRange(startDate, endDate)
You can use:
return (dateTocheck >= startDate && dateToCheck <= endDate);
I’ve found the following library to be the most helpful when doing any kind of date math. I’m still amazed nothing like this is part of the .Net framework.
http://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET
Following on from Sergey's answer, I think this more generic version is more in line with Fowler's Range idea, and resolves some of the issues with that answer such as being able to have the Includes methods within a generic class by constraining T as IComparable<T>. It's also immutable like what you would expect with types that extend the functionality of other value types like DateTime.
public struct Range<T> where T : IComparable<T>
{
public Range(T start, T end)
{
Start = start;
End = end;
}
public T Start { get; }
public T End { get; }
public bool Includes(T value) => Start.CompareTo(value) <= 0 && End.CompareTo(value) >= 0;
public bool Includes(Range<T> range) => Start.CompareTo(range.Start) <= 0 && End.CompareTo(range.End) >= 0;
}
In case anyone wants it as a Validator
using System;
using System.ComponentModel.DataAnnotations;
namespace GROOT.Data.Validation;
internal class DateRangeAttribute : ValidationAttribute
{
public string EndDate;
public string StartDate;
public override bool IsValid(object value)
{
return (DateTime)value >= DateTime.Parse(StartDate) && (DateTime)value <= DateTime.Parse(EndDate);
}
}
Usage
[DateRange(
StartDate = "01/01/2020",
EndDate = "01/01/9999",
ErrorMessage = "Property is outside of range")
]
I'm trying to build a query that will execute against the database as an IQueryable, and not in memory (IEnumerable).
The query will be used for several different purposes and each purpose has a slightly different way in which the Total property is calculated.
Because I'm using a Func for calculating the total, i get an error advising me that sql doesn't know how to deal with the Invoke method of my Func, which is understandable.
To get past the problem, i have had to list the groupings into memor by calling ToList() which is not good for performance.
Is there a way that i can execute this query as an IQueryable? Otherwise im going to have to write this query 20+ times with a calculation variance
Func<IGrouping<object, MyType>, double?> calculateTotal= (group) => #group.Sum(x => x.PassengerTotal);
Dictionary<object, double?> weekValues = queryable.GroupBy(o => new
{
Year = SqlFunctions.DatePart("yyyy", o.DateCreated),
Week = SqlFunctions.DatePart("ww", o.DateCreated),
Source = o.SourceId,
})
.ToList() //NEED TO REMOVE THIS CALL
.Select(ac => new WeeklyGraphGroup()
{
Year = ac.Key.Year,
Week = ac.Key.Week,
SourceId = ac.Key.Source,
Total = calculateTotal(ac)
})
.ToDictionary(dict =>
new
{
Year = dict.Year,
Week = dict.Week,
Source = dict.SourceId
}, grp => grp.Total);
Create a partial class as follows:
public partial class WeeklyGraphGroup
{
public int ? Year { get; set; }
public int ? Week { get; set; }
public int Source { get; set; }
}
public partial class WeeklyGraphGroup
{
private int ? _Total;
public int ? Total
{
get
{
this._Total = CalculateTotal(this.Year, this.Week, this.Source);
return this._Total;
}
}
public int ? CalculateTotal(int ? Year, int ? Week, int Source)
{
// do your calculation and return the value of total
// use whatever formula you want here. I guess you are calculating
// total based on any of the parameters(year, week or source);
return value;
}
}
Then do your query as below:
var list = db.Stores.GroupBy(o => new WeeklyGraphGroup
{
Year = SqlFunctions.DatePart("yyyy", o.DateCreated),
Week = SqlFunctions.DatePart("ww", o.DateCreated),
Source = o.SourceId,
})
.Select ( u => new WeeklyGraphGroup
{
Year = u.Key.Year,
Week = u.Key.Week,
Source = u.Key.Source
}
).ToList();
Total will be updated automatically
I have an object Member,
Member
PropertyA
PropertyZ
Membership[] Memberships
and Membership object is
Membership
PropertyA
PropertyZ
string Status
DateTime? StartDate
DateTime? EndDate
If I now have,
var members = GetAllMembers(DateTime.Now);
I want to find out if member has any active membership today,
// iterating through each member here
if(member.Memberships.Any())
{
// check if user has any membership duration that falls into
// today's date and membership status is also "active"
}
Problem is,
I am not sure how to check if any membership duration that falls into today's date and membership status is also "active"
to check that the targetdt(DateTime.Now) is between StartDate & EndDate:
if (member.Memberships.Any(x=>targetDt.Ticks > x.StartDate.Ticks && targetDt.Ticks < x.EndDate.Ticks && x.Status=="Active"))
{
// Has membership on targetDt.
}
I assume that null value for start or end date means 'open' interval
var today = DateTime.Now;
var activeMembers = allMembers.Where(m =>
m.Memberships.Any(ms =>
ms.Status == "Active" &&
(ms.StartDate == null || ms.StartDate <= today) && (ms.EndDate == null || today <= ms.EndDate)));
So I would structure your main classes slightly more different from what you have now. This it to get of having to compare on strings for the status, but since I don't know exactly how the active property is set but I would assume that for a membership to be active the start and (optional) end date have to be smaller than and greater than today's date.
class Member
{
private readonly IList<Membership> memberships;
public Member()
{
memberships = new List<Membership>();
}
public ReadOnlyCollection<Membership> Memberships
{
get
{
return new ReadOnlyCollection<Membership>(memberships);
}
}
}
class Membership
{
private readonly DateTime endDate;
private readonly DateTime startDate;
public Membership(DateTime startDate)
{
this.startDate = startDate;
}
DateTime StartDate
{
get
{
return this.startDate;
}
}
DateTime? EndDate
{
get
{
return endDate;
}
}
public Status Status
{
get
{
return CalculateMemebershipStatus();
}
}
private Status CalculateMemebershipStatus()
{
var todaysDate = DateTime.Today;
if (StartDate > todaysDate)
{
return Status.Deactivated;
}
if (!EndDate.HasValue)
{
return Status.Active;
}
if (todaysDate < EndDate.Value)
{
return Status.Active;
}
return Status.Deactivated;
}
}
enum Status
{
Active,
Deactivated
}
And when you need to find out all the members that have an active membership today you can do:
var memberCollection = new List<Member>();
...
var activeMembers = memberCollection.Where(x => x.Memberships.Any(y=> y.Status == Status.Active));
So this will go through your member collection and select the ones that have an active status today. Although as previously said this depends on how you build the active property but this is more contained in that testing for the activeness of the membership based on the date is done within the membership object. So the consumer of this data doesn't need to know how to say that a membership is active they simply only need to know that some will be active and others will be deactivated.
The structure
public struct Tick : IEquatable<Tick>
{
public DateTime date;
public decimal price;
public int volume;
public Tick(DateTime date, decimal price, int volume)
{
this.date = date;
this.price = price;
this.volume = volume;
}
public override bool Equals(object obj)
{
var other = (Tick)obj;
return this.date == other.date && this.price == other.price && this.volume == other.volume;
}
public bool Equals(Tick other)
{
return this.date == other.date && this.price == other.price && this.volume == other.volume;
}
}
is changed in this test:
[Test]
public void MarshalDoesntRoundsDateTime() {
for (int i = 0; i < 1000; i++)
{
var now = new Tick(DateTime.Now.AddSeconds(i), i, i);
var now2 = now;
var ticks = new Tick[1];
unsafe
{
fixed (Tick* ptr = &ticks[0])
{
Marshal.StructureToPtr(now2, (IntPtr)ptr, false);
now2 = (Tick)Marshal.PtrToStructure((IntPtr)ptr, typeof(Tick));
Assert.AreEqual(now.date.Ticks, now2.date.Ticks);
}
}
}
}
Expected: 635719676058860752
But was: 635719676058860000
What is going on? Why DateTime is rounded after marshalling? Is this documented somewhere?
Marshal.StructureToPtr() is intended to marshal data for unmanaged code. There is are multiple "standards" for dates in native code, none that are close in range and accuracy to DateTime. The CLR designers went for the COM interop standard, also exposed by DateTime.ToOADate().
As you can tell from the Reference Source, it can be no more accurate than 1 msec. DateTime is accurate to 0.1 usec. Inevitably the last 4 digits you are looking at must be 0.
It is not clear why you are doing this or why it matters. Guessing, do keep in mind that Marshal.StructureToPtr() only seems like an attractive way to serialize .NET data.
The true error is that DateTime shouldn't be marshalable... If you try to Marshal it directly you get an ArgumentException.
If you really really want to Marshal a DateTime (and I don't even want to know why, considering it is a semi-proprietary format of .NET), you could:
public long date;
public DateTime Date
{
get
{
return DateTime.FromBinary(date);
}
set
{
date = value.ToBinary();
}
}
I want to write an extension method that adds one day to a Nullable DateTime, but modifies the date itself.
I want to use it as follows:
someDate.AddOneDay();
This directly changes the value of someDate.
The code I initially wrote was:
public static DateTime? AddOneDay(this DateTime? date)
{
if (date.HasValue)
{
date.Value = date.Value.AddDays(1);
}
return null;
}
but this doesn't work since the reference is changed thus calling it this way
won't change the value of someDate.
Is there a way to achieve this and avoid code like:
someDate = someDate.AddOneDay();
Also I was thinking for some setter of the DateTime properties, but they don't have any..
public int Day { get; }
public int Month { get; }
public int Year { get; }
You can't DateTime is immutable, and should stay that way.
Just do:
someDate = someDate.AddOneDay();
And if you want to be more specific, you could rename your function to:
DateTime? someDate = someDate.AddOneDayOrDefault();
old school %)
public static void AddOneDay(ref DateTime date)
{
if (date != null) date = date.AddDays(1);
}
usage:
DateTime date = DateTime.Now;
AddOneDay(ref date);
UPD
one line version of method:
public static void AddOneDay(ref DateTime date) { date = date.AddDays(1); }
C# does support a similar feature, even for mutable values, which is the use of += on nullable values:
DateTime? date = GetDate();
var oneDay = TimeSpan.FromDays(1);
date += oneDay;