Specify timezone of datetime without changing value

Justin picture Justin · Sep 17, 2013 · Viewed 9.8k times · Source

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?

Answer

Matt Johnson-Pint picture Matt Johnson-Pint · Sep 17, 2013

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.

  • Take the local date and time input from your user and parse it into a JavaScript Date class.
    • For easier parsing, you might consider using a moment instead, from the moment.js library.
  • Get the UTC date and time from the Date or moment, and pass it to your server.
    • This is relatively easy in JavaScript, so I'll spare you the details.
    • There are many formats you can pass it, but the preferred way is as an ISO8601 timestamp. Example: 2013-09-17T08:00:00.000Z
  • Don't forget the Z at the end. That designates that the time is in UTC.
  • Since it is passed to you as UTC, you can just store it without any conversion.
  • When retrieving it, you again pass UTC to the browser, load it into a Date or a moment with JavaScript, and then emit the local date and time.
  • I highly recommend you try moment.js if you take this approach. It can be done without it, but that can be much more complicated and error prone.

.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.

  • Use TimeZoneInfo.GetSystemTimeZones to build a drop-down list.
    • For each 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();


About Daylight Saving Time

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.