How does DateTime.ToUniversalTime() work?

How does the conversion to UTC from the standard DateTime format work?

More specifically, if I create a DateTime object in one time zone and then switch to another time zone and run ToUniversalTime() on it, how does it know the conversion was done correctly and that the time is still accurately represented?


Solution 1:

There is no implicit timezone attached to a DateTime object. If you run ToUniversalTime() on it, it uses the timezone of the context that the code is running in.

For example, if I create a DateTime from the epoch of 1/1/1970, it gives me the same DateTime object no matter where in the world I am.

If I run ToUniversalTime() on it when I'm running the code in Greenwich, then I get the same time. If I do it while I live in Vancouver, then I get an offset DateTime object of -8 hours.

This is why it's important to store time related information in your database as UTC times when you need to do any kind of date conversion or localization. Consider if your codebase got moved to a server facility in another timezone ;)

Edit: note from Joel's answer - DateTime objects by default are typed as DateTimeKind.Local. If you parse a date and set it as DateTimeKind.Utc, then ToUniversalTime() performs no conversion.

And here's an article on "Best Practices Coding with Date Times", and an article on Converting DateTimes with .Net.

Solution 2:

Firstly, it checks whether the Kind of the DateTime is known to be UTC already. If so, it returns the same value.

Otherwise, it's assumed to be a local time - that's local to the computer it's running on, and in particular in the time zone that the computer was using when some private property was first lazily initialized. That means if you change the time zone after your application was started, there's a good chance it will still be using the old one.

The time zone contains enough information to convert a local time to a UTC time or vice versa, although there are times that that's ambiguous or invalid. (There are local times which occur twice, and local times which never occur due to daylight saving time.) The rules for handling these cases are specified in the documentation:

If the date and time instance value is an ambiguous time, this method assumes that it is a standard time. (An ambiguous time is one that can map either to a standard time or to a daylight saving time in the local time zone) If the date and time instance value is an invalid time, this method simply subtracts the local time from the local time zone's UTC offset to return UTC. (An invalid time is one that does not exist because of the application of daylight saving time adjustment rules.)

The returned value will have a Kind of DateTimeKind.Utc, so if you call ToUniveralTime on that it won't apply the offset again. (This is a vast improvement over .NET 1.1!)

If you want a non-local time zone, you should use TimeZoneInfo which was introduced in .NET 3.5 (there are hacky solutions for earlier versions, but they're not nice). To represent an instant in time, you should consider using DateTimeOffset which was introduced in .NET 2.0SP1, .NET3.0SP1 and .NET 3.5. However, that still doesn't have an actual time zone associated with it - just an offset from UTC. That means you don't know what local time will be one hour later, for example - the DST rules can vary between time zones which happened to use the same offset for that particular instant. TimeZoneInfo is designed to take historical and future rules into account, as opposed to TimeZone which is somewhat simplistic.

Basically the support in .NET 3.5 is a lot better than it was, but still leaves something to be desired for proper calendar arithmetic. Anyone fancy porting Joda Time to .NET? ;)

Solution 3:

What @womp said, with the addition that it checks the DateTime's Kind property to see if it might already be a UTC date.

Solution 4:

DateTime.ToUniversalTime removes the timezone offset of the local timezone to normalize a DateTime to UTC. If you then use DateTime.ToLocalTime on the normalized value in another timezone, the timezone offset of that timezone will be added to the normalized value for correct representation in that timezone.