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):
- 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.
- 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:
- var adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1918, 1, 1), new DateTime(1919, 12, 31), delta, ruleStart, ruleEnd);
- 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.
- ///<summary>
- /// Creates the custom time zone info with DST rules.
- ///</summary>
- ///<returns>The custom time zone.</returns>
- public static TimeZoneInfo CreateCustomTimeZoneInfoWithDstRules()
- {
- // Clock is adjusted one hour forward or backward
- var delta = new TimeSpan(1, 0, 0);
- //This will hold all the DST adjustment rules
- var listOfAdjustments = new List<TimeZoneInfo.AdjustmentRule>();
- /*
- Rule US 1918 1919 – Mar lastSun 2:00 1:00 D
- Rule US 1918 1919 – Oct lastSun 2:00 0 S
- */
- var ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 03, 05, DayOfWeek.Sunday);
- var ruleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 10, 05, DayOfWeek.Sunday);
- var adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1918, 1, 1), new DateTime(1919, 12, 31), delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- /*
- Rule US 1942 only – Feb 9 2:00 1:00 W # War
- */
- ruleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 02, 09);
- adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1942, 1, 1), new DateTime(1942, 12, 31),
- delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- /*
- Rule US 1945 only – Aug 14 23:00u 1:00 P # Peace
- Rule US 1945 only – Sep 30 2:00 0 S
- */
- ruleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 23, 0, 0), 08, 14);
- ruleEnd = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 09, 30);
- adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1945, 1, 1), new DateTime(1945, 12, 31),
- delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- /*
- Rule US 1967 2006 – Oct lastSun 2:00 0 S
- */
- ruleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 10, 5, DayOfWeek.Sunday);
- /*
- Rule US 1967 1973 – Apr lastSun 2:00 1:00 D
- */
- ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 04, 05, DayOfWeek.Sunday);
- adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1967, 1, 1), new DateTime(1973, 12, 31),
- delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- /*
- Rule US 1974 only – Jan 6 2:00 1:00 D
- */
- ruleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 01, 06);
- adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1974, 1, 1), new DateTime(1974, 12, 31),
- delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- /*
- Rule US 1975 only – Feb 23 2:00 1:00 D
- */
- ruleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 02, 23);
- adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1975, 1, 1), new DateTime(1975, 12, 31),
- delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- /*
- Rule US 1976 1986 – Apr lastSun 2:00 1:00 D
- */
- ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 04, 05, DayOfWeek.Sunday);
- adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1976, 1, 1), new DateTime(1986, 12, 31),
- delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- /*
- Rule US 1987 2006 – Apr Sun>=1 2:00 1:00 D
- */
- ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 04, 01, DayOfWeek.Sunday);
- adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1987, 1, 1), new DateTime(2006, 12, 31),
- delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- /*
- Rule US 2007 max – Mar Sun>=8 2:00 1:00 D
- Rule US 2007 max – Nov Sun>=1 2:00 0 S
- */
- ruleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 03, 02, DayOfWeek.Sunday);
- ruleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 11, 01, DayOfWeek.Sunday);
- adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2007, 1, 1), DateTime.MaxValue.Date,
- delta, ruleStart, ruleEnd);
- listOfAdjustments.Add(adjustment);
- var adjustments = new TimeZoneInfo.AdjustmentRule[listOfAdjustments.Count];
- listOfAdjustments.CopyTo(adjustments);
- return TimeZoneInfo.CreateCustomTimeZone(“Custom Eastern Standard Time”, new TimeSpan(-5, 0, 0),
- “(GMT-05:00) Eastern Time (US Only)”, “Eastern Standard Time”, “Eastern Daylight Time”, adjustments);
- }
This test passes:
- ///<summary>
- /// Compares the custom time zone info with default.
- ///</summary>
- [TestMethod]
- public void CompareCustomTimeZoneInfoWithDefault()
- {
- var ts = new DateTime(1980, 4, 15, 12, 0, 0);
- var isDstDefault = TimeZoneInfo.FindSystemTimeZoneById(“Eastern Standard Time”).IsDaylightSavingTime(ts);
- var isDstCustom = CreateCustomTimeZoneInfoWithDstRules().IsDaylightSavingTime(ts);
- Assert.IsTrue(isDstDefault);
- Assert.IsFalse(isDstCustom);
- }
The complete Visual Studio project is available on GitHub for you to review and extend: DateTimeExperiments.