« Home | ASP.NET Defending Against Form Hackers » | Release Branches and Bugfixes with Subversion » | Amazon Elastic Compute Cloud - EC2 » | PropertyInfo.GetCustomAttributes() Doesn't Return ... » | Silverlight Animated Optical Illusion » | ObjectDataSource DateTime, and Locale » | How Does OpenID Work? » | Facebook dev, IIS 5.1, and error 405 » | Creating Your Own Dev SSL Cert for IIS » | Moving from ASP.NET 1.1 to 2.0 » 

Wednesday, October 01, 2008 

Converting Timezones in .net

Ever had the need to convert a date/time to another timezone? I had to do it a while back to display publishing deadlines for a website used in Australia and New Zealand. Back then I used a wrapper for the kernel32 function SystemTimeToTzSpecificLocalTime from the CodeProject article Convert between UTC (Universal Co-ordinated Time) and local time. As of version 3.5 of the framework there is now the TimeZoneInfo class making this much easier to do in managed code. But both come with some small caveats.

The API function works with daylight saving data retrieved from the registry. To use it you extract the data in the registry key "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\[Timezone name]\Tzi", and memcopy this into a TIME_ZONE_INFORMATION structure.

The TIME_ZONE_INFORMATION structure contains two SYSTEMTIME structures StandardDate, and DaylightDate date - which specify when the people in this timezone wind their clocks back / forward. This value in the registry is only ever the daylight rules for 'right now' - so we can run into problems with dates ~12 months outside of this range. Windows treats these rules as if they have been in effect forever - even when they are changed by a Windows Update.

As an example: lets say we are working on an app that stores ATM transactions in UTC time - we want to convert them to a local time to print onto statements for a bank in Los Angeles. A customer made withdrawals at UTC times: 31-Mar-2006 17:00, and 31-Mar-2007 17:00. The code to convert the UTC time using the wrapper from the CodeProject article looks like this:

DateTime utcTime = DateTime.Parse("2006-03-31T17:00:00");
DateTime converted = TimeZoneInformation.FromUniversalTime("Pacific Standard Time", utcTime);
Console.WriteLine(converted.ToString("s"));

utcTime = DateTime.Parse("2007-03-31T17:00:00");
converted = TimeZoneInformation.FromUniversalTime("Pacific Standard Time", utcTime);
Console.WriteLine(converted.ToString("s"));

Which gives:

UTC:2006-03-31T17:00:00 Los Angeles:2006-03-31T10:00:00
UTC:2007-03-31T17:00:00 Los Angeles:2007-03-31T10:00:00

So 5pm UTC converts to 10pm in LA - sounds about right? Well no.. I've obviously picked some dates where the daylight settings in the registry give us the wrong conversion. From 1987 to 2006 the USA East coast wound their clocks forward on the first Sunday of April, this changed in 2007 to the second Sunday of March. So on 31-Mar-2006 daylight savings hadn't kicked in, LA should've been 8 hours behind UTC. On 31-Mar-2007 daylight savings did apply - and we've correctly calculated LA time as 7 hours behind UTC. Windows is applying the second Sunday of March rule in 2006 - as this is the only rule it knows about.

That is - unless you are on Vista or Server 2008. The newer version of the Windows API is able to read the DYNAMIC_TIME_ZONE_INFORMATION - from a collection of multiple rules in the "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\[Timezone name]\Dynamic DST".

Enter the .net TimeZoneInfo class - lucky for us this class can interpret the dynamic DST data from the registry (even in XP). Here's the previous example using the TimeZoneInfo class:

TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
DateTime utcTime = DateTime.Parse("2006-03-31T17:00:00");
DateTime converted = TimeZoneInfo.ConvertTimeFromUtc(utcTime, tzi);
Console.WriteLine("UTC:{0:s} Los Angeles:{1:s}", utcTime, converted);

utcTime = DateTime.Parse("2007-03-31T17:00:00");
converted = TimeZoneInfo.ConvertTimeFromUtc(utcTime, tzi);
Console.WriteLine("UTC:{0:s} Los Angeles:{1:s}", utcTime, converted);

The output:

UTC:2006-03-31T17:00:00 Los Angeles:2006-03-31T09:00:00
UTC:2007-03-31T17:00:00 Los Angeles:2007-03-31T10:00:00

Seeing what I expect for the 2006 date. So everything is covered? Well no.. :)

The full list of daylight rules is VERY complicated - lots of governments worldwide seem to like messing with daylight rules. The new registry rules are not populated with historic rules. For example Sydney changed the rules for the Olympic games in 2000 - the daylight transition was on the last Sunday of August, rather than the last Sunday in October. Try converting a date in September 2000 - we should get the usual 10 hours difference plus 1 hour for daylight time. And we get:

UTC:2000-09-15T00:00:00 Sydney:2000-09-15T10:00:00

This conversion website gives the results we expect: here.

We can actually look in the TimeZoneInfo GetAdjustmentRules() and see the list of rules Windows knows about.

TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("AUS Eastern Standard Time");
int i = 0;
foreach (TimeZoneInfo.AdjustmentRule adj in tzi.GetAdjustmentRules())
{
    i++;
    Console.WriteLine("AdjustmentRule " + i);
    Console.WriteLine("adj.DateStart = " + adj.DateStart);
    Console.WriteLine("adj.DateEnd = " + adj.DateEnd);
    Console.WriteLine(String.Format("adj.DaylightTransitionStart = Day: {0}, DayOfWeek: {1}, Month: {2}, TimeOfDay: {3}, Week: {4}", adj.DaylightTransitionStart.Day,
        adj.DaylightTransitionStart.DayOfWeek,
        adj.DaylightTransitionStart.Month,
        adj.DaylightTransitionStart.TimeOfDay.ToShortTimeString(),
        adj.DaylightTransitionStart.Week));
    Console.WriteLine(String.Format("adj.DaylightTransitionEnd = Day: {0}, DayOfWeek: {1}, Month: {2}, TimeOfDay: {3}, Week: {4}", adj.DaylightTransitionEnd.Day,
        adj.DaylightTransitionEnd.DayOfWeek,
        adj.DaylightTransitionEnd.Month,
        adj.DaylightTransitionEnd.TimeOfDay.ToShortTimeString(),
        adj.DaylightTransitionEnd.Week));
    Console.WriteLine("adj.DaylightDelta = " + adj.DaylightDelta);
    Console.WriteLine();
}

The output:

AdjustmentRule 1
adj.DateStart = 1/01/0001 12:00:00 AM
adj.DateEnd = 31/12/2007 12:00:00 AM
adj.DaylightTransitionStart = Day: 1, DayOfWeek: Sunday, Month: 10, TimeOfDay: 2:00 AM, Week: 5
adj.DaylightTransitionEnd = Day: 1, DayOfWeek: Sunday, Month: 3, TimeOfDay: 3:00 AM, Week: 5
adj.DaylightDelta = 01:00:00

AdjustmentRule 2
adj.DateStart = 1/01/2008 12:00:00 AM
adj.DateEnd = 31/12/9999 12:00:00 AM
adj.DaylightTransitionStart = Day: 1, DayOfWeek: Sunday, Month: 10, TimeOfDay: 2:00 AM, Week: 1
adj.DaylightTransitionEnd = Day: 1, DayOfWeek: Sunday, Month: 4, TimeOfDay: 3:00 AM, Week: 1
adj.DaylightDelta = 01:00:00

How would you deal with this scenario? The MSDN article for TimeZoneInfo mentions the class can be used for "Creating a new time zone that is not already defined by the operating system." The popular tz database contains a very comprehensive collection of date rules. So I was 'sort of' expecting someone would write the code to import the tz database to custom TimeZoneInfo timezones. Unfortunately a quick google doesn't come up with someone who has already done this for me. An alternative exists to use the tz database in .net: PublicDomain - described in more detail in the CodeProject article: How to Use the Olson Time Zone Database in .NET

Futher reading:

Labels:

This comment has been removed by a blog administrator.

Post a Comment