Creating Custom TimeZoneInfo With Daylight Saving Time Transitions

This article will show you how to create a custom TimeZoneInfo that incorporates AdjustmentRules for Daylight Saving Time (DST) Transitions all the way back to 1918. The backdrop for this effort is covered in my previous blog post: Beware of Daylight Saving Time Transitions in .NET. You might want to read that one first for the context.

As noted in the previous post, the System.TimeZoneInfo uses AdjustmentRules to account for DST Transitions, and the default AdjustmentRules do not incorporate all the available DST data. But it is possible to create a custom TimeZoneInfo and populate the AdjustmentRules with DST Transition data available to cover all DST transitions.

First, we need to get the complete set of DST transition data. The authoritative source of TimeZone and DST data is: Sources for Time Zone and Daylight Saving Time Data. TZ database releases can be downloaded from IANA’s ftp site. Look for the file name starting with tzdata (not tzcode). Download the latest version. At this time, it is tzdata2013a.tar.gz.

Unzip the tar file and open the file named northamerica (because I need the data for U.S. Eastern Time Zone). Take some time to review this file. It has more than just dry numerical data! This fantastic article by Jon Udel gives a rereshing perspective on the richness of this document: A literary appreciation of the Olson/Zoneinfo/tz database.

The DST Transition rules we are interested in are listed starting at line number 116. The listing looks like this:

What does all this mean? There is an excellent explanation by Bill Seymour  in this article: How to Read the tz Database Source Files. Linux man-pages also describe the format in its timezone compiler section – zic. Armed with this knowledge, we can start creating the AdjustmentRules needed to create the custom TimeZoneInfo.  Let’s start with the first rule.

This rule is to be read as: The transition to Daylight (D) Saving Time started on the last Sunday (lastSun) of March (Mar) 1918 at 2:00 a.m. by saving 1:00 hour (advancing the clocks from 2:00 a.m. to 3:00 a.m.). This rule is coded as follows, keeping in mind that the last Sunday of March 1918 was in the fifth (05) week of March (03):

  1. var ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 03, 05, DayOfWeek.Sunday);

A FloatingDateRule means that the rule is dynamic based on whatever date occurs on the last Sunday of March for each year when this rule is in effect. The other kind of rule is FixedDateRule, which means that the exact date is specified for all the years when this rule is in effect. As an example, the FixedDateRule is used to represent a one-time change to the start date for the transition to the war time (“W”) on February 9, 1942.

Continuing with the 1918-1919 rule, the transition back to Standard (S) Time was on the last Sunday of October 1919 at 2:00 a.m., by returning to previous Standard time.

  1. var ruleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 10, 05, DayOfWeek.Sunday);

The adjustment rule is created from the above information and then added to the list of AdjustmentRules as follows:

  1. var adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1918, 1, 1), new DateTime(1919, 12, 31), delta, ruleStart, ruleEnd);
  2. listOfAdjustments.Add(adjustment);

Using this method, the entire rule set can be encoded in the AdjustmentRules and a custom TimeZoneInfo can be created using TimeZoneInfo.CreateCustomTimeZone method. The final result is shown below.

  1. ///<summary>
  2. /// Creates the custom time zone info with DST rules.
  3. ///</summary>
  4. ///<returns>The custom time zone.</returns>
  5. public static TimeZoneInfo CreateCustomTimeZoneInfoWithDstRules()
  6. {
  7.     // Clock is adjusted one hour forward or backward
  8.     var delta = new TimeSpan(1, 0, 0);
  9.     //This will hold all the DST adjustment rules
  10.     var listOfAdjustments = new List<TimeZoneInfo.AdjustmentRule>();
  11.     /*
  12.         Rule    US    1918    1919    –    Mar    lastSun    2:00    1:00    D
  13.         Rule    US    1918    1919    –    Oct    lastSun    2:00    0    S             
  14.      */
  15.     var ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 03, 05, DayOfWeek.Sunday);
  16.     var ruleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 10, 05, DayOfWeek.Sunday);
  17.     var adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1918, 1, 1), new DateTime(1919, 12, 31), delta, ruleStart, ruleEnd);
  18.     listOfAdjustments.Add(adjustment);
  19.     /*
  20.         Rule    US    1942    only    –    Feb    9    2:00    1:00    W # War
  21.      */
  22.     ruleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 02, 09);
  23.     adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1942, 1, 1), new DateTime(1942, 12, 31),
  24.                                                                delta, ruleStart, ruleEnd);
  25.     listOfAdjustments.Add(adjustment);
  26.     /*
  27.         Rule    US    1945    only    –    Aug    14    23:00u    1:00    P # Peace
  28.         Rule    US    1945    only    –    Sep    30    2:00    0    S
  29.      */
  30.     ruleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 23, 0, 0), 08, 14);
  31.     ruleEnd = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 09, 30);
  32.     adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1945, 1, 1), new DateTime(1945, 12, 31),
  33.                                                                delta, ruleStart, ruleEnd);
  34.     listOfAdjustments.Add(adjustment);
  35.     /*
  36.         Rule    US    1967    2006    –    Oct    lastSun    2:00    0    S             
  37.      */
  38.     ruleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 10, 5, DayOfWeek.Sunday);
  39.     /*
  40.         Rule    US    1967    1973    –    Apr    lastSun    2:00    1:00    D
  41.      */
  42.     ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 04, 05, DayOfWeek.Sunday);
  43.     adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1967, 1, 1), new DateTime(1973, 12, 31),
  44.                                                                delta, ruleStart, ruleEnd);
  45.     listOfAdjustments.Add(adjustment);
  46.     /*
  47.         Rule    US    1974    only    –    Jan    6    2:00    1:00    D
  48.      */
  49.     ruleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 01, 06);
  50.     adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1974, 1, 1), new DateTime(1974, 12, 31),
  51.                                                                delta, ruleStart, ruleEnd);
  52.     listOfAdjustments.Add(adjustment);
  53.     /*
  54.         Rule    US    1975    only    –    Feb    23    2:00    1:00    D             
  55.      */
  56.     ruleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 02, 23);
  57.     adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1975, 1, 1), new DateTime(1975, 12, 31),
  58.                                                                delta, ruleStart, ruleEnd);
  59.     listOfAdjustments.Add(adjustment);
  60.     /*
  61.         Rule    US    1976    1986    –    Apr    lastSun    2:00    1:00    D             
  62.      */
  63.     ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 04, 05, DayOfWeek.Sunday);
  64.     adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1976, 1, 1), new DateTime(1986, 12, 31),
  65.                                                                delta, ruleStart, ruleEnd);
  66.     listOfAdjustments.Add(adjustment);
  67.     /*
  68.         Rule    US    1987    2006    –    Apr    Sun>=1    2:00    1:00    D             
  69.      */
  70.     ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 04, 01, DayOfWeek.Sunday);
  71.     adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1987, 1, 1), new DateTime(2006, 12, 31),
  72.                                                                delta, ruleStart, ruleEnd);
  73.     listOfAdjustments.Add(adjustment);
  74.     /*
  75.         Rule    US    2007    max    –    Mar    Sun>=8    2:00    1:00    D
  76.         Rule    US    2007    max    –    Nov    Sun>=1    2:00    0    S             
  77.      */
  78.     ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 03, 02, DayOfWeek.Sunday);
  79.     ruleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 11, 01, DayOfWeek.Sunday);
  80.     adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2007, 1, 1), DateTime.MaxValue.Date,
  81.                                                                delta, ruleStart, ruleEnd);
  82.     listOfAdjustments.Add(adjustment);
  83.     var adjustments = new TimeZoneInfo.AdjustmentRule[listOfAdjustments.Count];
  84.     listOfAdjustments.CopyTo(adjustments);
  85.     return TimeZoneInfo.CreateCustomTimeZone(“Custom Eastern Standard Time”, new TimeSpan(-5, 0, 0),
  86.           “(GMT-05:00) Eastern Time (US Only)”, “Eastern Standard Time”, “Eastern Daylight Time”, adjustments);
  87. }

This test passes:

  1. ///<summary>
  2. /// Compares the custom time zone info with default.
  3. ///</summary>
  4. [TestMethod]
  5. public void CompareCustomTimeZoneInfoWithDefault()
  6. {
  7.     var ts = new DateTime(1980, 4, 15, 12, 0, 0);
  8.     var isDstDefault = TimeZoneInfo.FindSystemTimeZoneById(“Eastern Standard Time”).IsDaylightSavingTime(ts);
  9.     var isDstCustom = CreateCustomTimeZoneInfoWithDstRules().IsDaylightSavingTime(ts);
  10.     Assert.IsTrue(isDstDefault);
  11.     Assert.IsFalse(isDstCustom);
  12. }

The complete Visual Studio project is available on GitHub for you to review and extend: DateTimeExperiments.

About Ash Tewari

Ash joined AIS in May 2012 as a Senior Software Engineer. He has a Master’s of Science in Engineering from Auburn University and a Bachelor’s in Technology from Regional Engineering College in Jalandhar, India. He is passionate about creating software and finding ways to improve his craft. For more than a decade, Ash has built software components, applications and frameworks encompassing desktop and web clients, middleware components and databases. He enjoys sharing his knowledge and perspectives with the community by participating in user groups, presenting in code camps and via his blog (http://www.ashtewari.com/). Ash likes to read, watch movies and get out in the mountains for backpacking and hiking.