Safely comparing local and universal DateTimes
Asked Answered
M

2

45

I just noticed what seems like a ridiculous flaw with DateTime comparison.

DateTime d = DateTime.Now;
DateTime dUtc = d.ToUniversalTime();

d == dUtc; // false
d.Equals(dUtc); //false
DateTime.Compare(d, dUtc) == 0; // false

It appears that all comparison operations on DateTimes fail to do any type of smart conversion if one is DateTimeKind.Local and one is DateTimeKind.UTC. Is the a better way to reliably compare DateTimes aside from always converting both involved in the comparison to utc time?

Maseru answered 3/8, 2011 at 17:20 Comment(5)
Why not have all dates in UTC?Paly
For the business object that this is associated with, they will always be utc. However it sucks that in tons of other places through out the application I have to remember to convert to utc time when using the date field on one specific business object.Maseru
A source of nasty and hard-to-find bugs, esp. if you develop in a local timezone that's equal to UTC, finding your product failing when shipped or when daylight saving time starts.Thorton
I think the moral of the story is that you should use the standard form of any data (Unicode strings, UTC dates, whatever) everywhere in an application.Paly
I second your notion of the ridiculous flaw. The MSDN article (msdn.microsoft.com/en-us/library/…) states that a DateTime: "Represents an instant in time, typically expressed as a date and time of day." It seems that the author of the article agrees with you and I about how this is supposed to work. The implementation seems off.Ventricle
T
56

When you call .Equal or .Compare, internally the value .InternalTicks is compared, which is a ulong without its first two bits. This field is unequal, because it has been adjusted a couple of hours to represent the time in the universal time: when you call ToUniversalTime(), it adjusts the time with an offset of the current system's local timezone settings.

You should see it this way: the DateTime object represents a time in an unnamed timezone, but not a universal time plus timezone. The timezone is either Local (the timezone of your system) or UTC. You might consider this a lack of the DateTime class, but historically it has been implemented as "number of ticks since 1970" and doesn't contain timezone info.

When converting to another timezone, the time is — and should be — adjusted. This is probably why Microsoft chose to use a method as opposed to a property, to emphasize that an action is taken when converting to UTC.

Originally I wrote here that the structs are compared and the flag for System.DateTime.Kind is different. This is not true: it is the amount of ticks that differs:

t1.Ticks == t2.Ticks;       // false
t1.Ticks.Equals(t2.Ticks);  // false

To safely compare two dates, you could convert them to the same kind. If you convert any date to universal time before comparing you'll get the results you're after:

DateTime t1 = DateTime.Now;
DateTime t2 = someOtherTime;
DateTime.Compare(t1.ToUniversalTime(), t2.ToUniversalTime());  // 0
DateTime.Equals(t1.ToUniversalTime(), t2.ToUniversalTime());  // true

Converting to UTC time without changing the local time

Instead of converting to UTC (and in the process leaving the time the same, but the number of ticks different), you can also overwrite the DateTimeKind and set it to UTC (which changes the time, because it is now in UTC, but it compares as equal, as the number of ticks is equal).

var t1 = DateTime.Now
var t2 = DateTime.SpecifyKind(t1, DateTimeKind.Utc)
var areEqual = t1 == t2   // true
var stillEqual = t1.Equals(t2) // true

I guess that DateTime is one of those rare types that can be bitwise unequal, but compare as equal, or can be bitwise equal (the time part) and compare unequal.

Changes in .NET 6

In .NET 6.0, we now have TimeOnly and DateOnly. You can use these to store "just the time of day", of "just the date of the year". Combine these in a struct and you'll have a Date & Time struct without the historical nuisances of the original DateTime.

Alternatives

Working properly with DateTime, TimeZoneInfo, leap seconds, calendars, shifting timezones, durations etc is hard in .NET. I personally prefer NodaTime by Jon Skeet, which gives control back to the programmer in a meaningful an unambiguous way.

Often, when you’re not interested in the timezones per se, but just the offsets, you can get by with DateTimeOffset.

This insightful post by Jon Skeet explains in great depth the troubles a programmer can face when trying to circumvent all DateTime issues when just storing everything in UTC.

Background info from the source

If you check the DateTime struct in the .NET source, you'll find a note that explains how originally (in .NET 1.0) the DateTime was just the number of ticks, but that later they added the ability to store whether it was Universal or Local time. If you serialize, however, this info is lost.

This is the note in the source:

    // This value type represents a date and time.  Every DateTime
    // object has a private field (Ticks) of type Int64 that stores the
    // date and time as the number of 100 nanosecond intervals since
    // 12:00 AM January 1, year 1 A.D. in the proleptic Gregorian Calendar.
    //
    // Starting from V2.0, DateTime also stored some context about its time
    // zone in the form of a 3-state value representing Unspecified, Utc or
    // Local. This is stored in the two top bits of the 64-bit numeric value
    // with the remainder of the bits storing the tick count. This information
    // is only used during time zone conversions and is not part of the
    // identity of the DateTime. Thus, operations like Compare and Equals
    // ignore this state. This is to stay compatible with earlier behavior
    // and performance characteristics and to avoid forcing  people into dealing
    // with the effects of daylight savings. Note, that this has little effect
    // on how the DateTime works except in a context where its specific time
    // zone is needed, such as during conversions and some parsing and formatting
    // cases.
Thorton answered 3/8, 2011 at 17:24 Comment(12)
Thats what ive been doing, just kind of sucks to have to convert every date time used in any comparisonMaseru
@jdc0589: my original answer was incorrect, I updated it to reflected the actual internals going on.Thorton
Can someone justify this behavior for DateTime? I guess I just don't get it. Why doesn't the direct comparison account for the timezone? This seems defective to me.Ventricle
I think it's a bugToxinantitoxin
I think the technical term for this is the behaviour is completely f%$ked. If DateTime.Now != DateTime.UtcNow then this is mindblowingly bad behaviour. What is the point of DateTime.Kind at all if comparisons don't calculate including offsets?Yaya
@Yaya and others, the reasoning is that 'Local Time' means 'Without a timezone', i.e., it is unknown where in the world it is that time. Whereas 'UTC' means 'In the UTC (virtual) timezone', which is an exact time, the same wherever in the world you are. You can argue this is a flaw or that it's a feature, but it follows, at least to some extend, the international standard for date and time comparisons, ISO 8601 (section 4.1 currently). You cannot compare 'unknown' with 'known'.Thorton
@Thorton that's just incorrect. Local means local machine regional setting. The .NET install has full knowledge of the local offset and how to convert it to UTC. Unknown is DateTimeKind.Unspecified.Yaya
@Shiv, I just checked the code of DateTime and it doesn't store the timezone at all. It only has a flag for UTC/Local/Unspecified in two bits of the ulong it uses to store the time. UTC is the current time in the UTC zone, Local is time wherever your system is, but if you would serialize Local and send it to another timezone, it'll just be Local there (same time, different timezone). You're correct that this isn't ISO-8601, it's just historical behavior. Timezones are not a part of the DateTime struct, so they are not a part in the comparison either. Hence UTC <> Local.Thorton
@Thorton it shouldn't need to be part of the struct. The timezone is implicitly available from the system. The system automatically tracks DST changeover based off the system settings anyway. That's why the implementation makes no sense. It does half the job.Yaya
@shiv, after your comments, I've clarified my answer, thanks for your input. A time without timezone is relatively meaningless, unless you always store it as UTC and adjust accordingly (based on user or system prefs); how otherwise would you adjust for a change in user settings. .NET offers either UTC or Local, and only UTC is unambiguous (but not really, read this for background). .NET DateTime is flawed, a good alternative I use almost always is NodaTime.Thorton
I'm surprised nobody has mentioned DateTimeOffset which offers comparison (with implicit UTC conversion). See also: https://mcmap.net/q/25756/-datetime-vs-datetimeoffsetAsphodel
@Dejan, that’s a good point. Though timezones are not the same as offsets, and the original question was actually about comparing DateTime, which itself has different representations. But I agree, it should’ve been mentioned as a good alternative. I added it.Thorton
C
6

To deal with this, I created my own DateTime object (let's call it SmartDateTime) that contains the DateTime and the TimeZone. I override all operators like == and Compare and convert to UTC before doing the comparison using the original DateTime operators.

Camillecamilo answered 3/8, 2011 at 17:24 Comment(3)
This is what im planing on doing. The part that sucks is this is tied to linq-to-sql, so I have to make the existing datetime field private and expose an instance of my custom class publicy which persists to the private datetime....talk about uglyMaseru
Ugh. You might be better off just adding a TimeZoneInfo field in your database and storing the dates all in UTC.Camillecamilo
We're doing all our new work with DTO instead. Makes the data accurate and the backend has links to resource locale so we even have resource offset calculation and storage.Yaya

© 2022 - 2024 — McMap. All rights reserved.