I'm wondering how to go about changing the timezone of a DateTime object without actually changing the value. Here is the background...
I have an ASP.NET MVC site hosted on AppHarbor and the server has its time set to UTC. When I submit a form from my site containing a datetime value, say 9/17/2013 4:00am, it comes up to the server with that value. However then when I do:
public ActionResult Save(Entity entity)
{
entity.Date = entity.Date.ToUniversalTime();
EntityService.Save(entity);
}
...it incorrectly keeps it as the same time (4am) because the server is already on UTC time. So after converting to UTC and saving to the database, the database value is 2013-09-17 04:00:00.000, when really it should be 2013-09-17 08:00:00.000, as the browser is on EST. I was hoping after form submission and passing the values to the controller action, it would set the DateTime's timezone to the browser's (EST) rather than the server host's (UTC), however that didn't happen.
In any case, now I'm stuck with a DateTime object that contains the correct date/time value but the wrong time zone, so I can't correctly convert it to UTC. I store the time zone of each user, so if there was a way to set the time zone for the DateTime object without actually changing the value (I can't do TimeZoneInfo.ConvertTime because it'll change the value) then I could have the correct date/time value AND the correct time zone, then I could safely do date.ToUniversalTime. That being said, I've only seen how to change the KIND of a DateTime, not the TimeZone, without changing its actual value.
Any ideas?
Actually, it is doing exactly what you asked it to do. From the point of view of the server, you passed it an "unspecified" DateTime
. In other words, just year, month day, etc., withoutout any context. If you look at the .Kind
property, you will see that it is indeed DateTimeKind.Unspecified
.
When you call .ToUniversalTime()
on an unspecified kind of DateTime
, .NET will assume the context of the local time zone of the computer that the code is running on. You can read more about this in the documentation here. Since your server is set to UTC then regardless of the input kind, all three kinds will yield the same result. Basically, it's a no-op.
Now, you said that you wanted the time to reflect the user's time zone. Unfortunately, that is information that the server doesn't have. There's no magic HTTP header that carries time zone details.
There are ways to achieve this effect though, and you have some options.
JavaScript Method
This is probably the easiest option, but it does require JavaScript.
Date
class.
moment
instead, from the moment.js library.Date
or moment
, and pass it to your server.
2013-09-17T08:00:00.000Z
Z
at the end. That designates that the time is in UTC.Date
or a moment
with JavaScript, and then emit the local date and time..NET Method using Windows Time Zones
If you aren't going to invoke JavaScript, then you will need to ask the user for their time zone. This can work well in a larger application, such as on the user's profile page.
TimeZoneInfo.GetSystemTimeZones
to build a drop-down list.
TimeZoneInfo
item in the list, use the .Id
for the value, and the .DisplayName
for the text.Then you can use this value when you want to convert the time.
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById(yourUsersTimeZone);
DateTime utc = TimeZoneInfo.ConvertTimeToUtc(theInputDatetime, tz);
This will work with Windows time zones, which are a Microsoft creation, and have some drawbacks. You can read about a few of their deficiencies in the timezone tag wiki.
.NET Method using IANA Time Zones
If you want to use the more standard IANA time zones, such as America/New_York
or Europe/London
, then you can use a library like Noda Time. It offers a much better API for working with date and time than the built-in framework. There's a bit of a learning curve, but if you're doing anything complicated it is well worth the effort. As an example:
DateTimeZone tz = DateTimeZoneProviders.Tzdb["America/New_York"];
var pattern = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss");
LocalDateTime dt = pattern.Parse("2013-09-17 04:00:00").Value;
ZonedDateTime zdt = tz.AtLeniently(dt);
Instant utc = zdt.ToInstant();
Regardless of which of these three approaches you take, you will have to deal with the problems created by Daylight Saving Time. Each of these samples shows a "lenient" approach, where if the local time you specify is ambiguous or invalid, that some rule is followed so you still get some valid moment in time. You can see this directly with the Noda Time approach when I called AtLeniently
. But it occurs with the other ones also - it's just implicit. In JavaScript, the rules can vary per browser, so don't expect consistent results.
Depending on what kind of data you're collecting, you may decide it's perfectly acceptable to make this kind of assumption. But in many cases it's not appropriate to assume. In that case, you may need to either alert your user that the input time is invalid, or ask them which of two ambiguous times they meant.
In .Net, you can check for this with TimeZoneInfo.IsInvalidTime
and TimeZoneInfo.IsAmbiguousTime
.
For an example of how daylight saving time works, see here. In the "spring-forward" transition, a time during the transition is invalid. In the "fall-back" transition, a time during the transition is ambiguous - that is, it could have happened either before or after the transition.