Search code examples
c#datetimetimezoneicalendarnodatime

Replace TimeZone


We are trying to build basic event calendaring functionality that allows a user to create an event and specify a start time at a given month, day, year, hour, and minute as well as a time zone (System.TimeZoneInfo.Id). The CMS system generates the resulting System.DateTime based on the location of our server, let's say TimeZoneInfo.Id Mountain Standard Time. The CMS does not provide an option with their date picker component to specify the time zone. We do however have control over the SQL datetime precision, by default set to 7.

The DateTime gets formatted as yyyyMMddTHHmmssZ as for purposes of populating start/end times in an .ics/ical. With this format it makes May 25, 2018 7:00PM (20180508T192840Z) always look like the server's Mountain Standard Time (MST) rather than May 25, 2018 7:00PM in selected Eastern Standard Time (EST).

How can I "replace" the time zone for the DateTime that is generated without changing the year/month/day/hour/minute with either DateTime, DateTimeOffset, TimeZoneInfo, NodaTime, or even string functions to format into yyyyMMddTHHmmssZ?

The following:

TimeZoneInfo destinationTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var converted = TimeZoneInfo.ConvertTime(dateTime1, destinationTimeZone);

or:

LocalDateTime fromLocal = LocalDateTime.FromDateTime(dateTime1);
DateTimeZone fromZone = DateTimeZoneProviders.Tzdb["America/Denver"];
ZonedDateTime fromZoned = fromLocal.InZoneLeniently(fromZone);

DateTimeZone toZone = DateTimeZoneProviders.Tzdb["America/Chicago"];
ZonedDateTime toZoned = fromZoned.WithZone(toZone);
LocalDateTime toLocal = toZoned.LocalDateTime;
var result = toLocal.ToDateTimeUnspecified();

Creates a new DateTime with hour adjusted from CST to EST which wouldn't work as the goal is to have a DateTime with the original hour value but with TimeZoneInfo.Id Eastern Standard Time.

DateTime constructor doesn't seem to have a contructor that specifies TimeZoneInfo, only DateTimeKind.

How can this be done with some even such as a DateTime created from DateTime.Now?


Solution

  • A few things:

    • You're format specifier includes a Z at the end. This is treated by .Net's string formatting as a character literal, because it is not a valid datetime formatting specifier. Note that formatting tokens are case sensitive. As a literal, it is just copied to the output - just like the T is. Thus, this string you generate is always going to be interpreted as UTC by anything that parses it, since that's what the Z means in the ISO 8601 standard. That is ultimately the root cause of the problem you're facing.

      If you intended it to reflect an ambiguous local time (because time zone is elsewhere in your .ics perhaps?), then omit the Z entirely. If you intended however to include the time zone offset, then you might use the K specifier for DateTime values, or perhaps the zzz specifier in conjunction with DateTimeOffset values - depending on your specific needs.

    • As others pointed out, DateTime is not time zone aware, but also note that neither is DateTimeOffset in that it only tracks an offset from UTC and not a specific time zone. For example, it can track -07:00, but it cannot tell you that it is in Mountain Time. That is why Noda Time has its ZonedDateTime type. .Net doesn't have any such built-in type on its own.

    • In your code, not that that in the call to TimeZoneInfo.ConvertTime, the .Kind of the dateTime1 variable will be taken into account. If it's DateTimeKind.Utc, then the result will be deterministic. But if it's DateTimeKind.Unspecified, or DateTimeKind.Local, then it will be treated as if it is in terms of the local computer's time zone - which is the server time zone in your case.

    • Note that it is far better to write your code in a way that behaves the same regardless of what the server's time zone is set to. This generally means avoiding DateTimeKind.Local, such as DateTime.Now, TimeZoneInfo.Local, and others. Instead, use DateTime.UtcNow for getting the current DateTime. Alternatively you can use either DateTimeOffset.Now or DateTimeOffset.UtcNow, or one of the methods on Noda Time's IClock implementations.

    At the end of the day, though there are several possible solutions to your problem, the simplest one to generate the current time as a string in a specific time zone would be:

    TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
    DateTime utcNow = DateTime.UtcNow;
    DateTime converted = TimeZoneInfo.ConvertTime(utcNow, destinationTimeZone);
    string s = converted.ToString("yyyyMMddTHHmmss");
    

    OR you might want:

    TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
    DateTimeOffset utcNow = DateTimeOffset.UtcNow;
    DateTimeOffset converted = TimeZoneInfo.ConvertTime(utcNow, destinationTimeZone);
    string s = converted.ToString("yyyyMMddTHHmmsszzz").Replace(":","");
    

    Note the removal of the : via Replace at the end - that's because in the ISO 8601 basic format, the offset should be like -0500 rather than -05:00. Unfortunately there is no format specifier to get that directly. (Only the ISO 8601 extended format uses the colon).