Introduction
To test some boundary cases in a program that reads the time stamps attached to the files stored on a Windows computer, I needed ready access to the Daylight Saving Time (DST) transition dates for any year.
Background
Two years ago, to meet a similar requirement, I wrote a program that I called my Time Zone Lab, about which I wrote in "Time Zone Lab: An Expedition into Windows Time Zones," at http://www.codeproject.com/Articles/816165/Time-Zone-Lab-An-Expedition-into-Windows-Time-Zone. Today, to meet a closely related, need, I concluded that the simplest way to extract the information that I needed from the time zone tables in the Windows Registry was to extend the Time Zone Lab, then construct a Microsoft Excel worksheet around the output of the new routine.
Since most parts of the world observe Daylight Saving Time (hencforeth referred to as "DST"), and they will never be able to agree on when to start and stop using DST, thanks to the way Earth's axis it tilted in relation to the Sun, Windows keeps a set of adjustment rules for each of the world's time zones. To further complicate matters, from time to time, countries change the rules that govern when DST starts and ends, as the United States did in 2007. Hence, there is not one rule, but a set of them, for each time zone, each having a starting and ending date that determines the years to which it applies. As an example, the Central Standard Time zone, in which I live, has two rules. The first rule applies to all dates through the end of 2006, and the second applies to everyth8ing since then.
Long before I dug into it, I wondered whether Windows kept a table of years containing a row for each year that held the DST start and end dates for that year for each time zone. However, such a table would be gigantic, and it would impose a perpetual maintenance burden on Microsoft that can be easily avoided, since the transition dates are based on mathematical formulas, making it relatively straightforward to compute the transition dates for any year as needed.
Using the code
Extracting the adjustment rules falls to a new member of static class TimeZoneTasks
. The new method, EmumerateTimeZoneAdjustments
, has two methods of determining the time zone for which to extract the adjustment rules.
- When the program is called with an initial command line argument of
EmumerateTimeZoneAdjustments
, followed by the ID of a time zone, the adjustment rules that apply to the specified time zone are reported. - When the program is called without arguments, the adjustment rules that apply to the local time zone, which is determined by the Regional Settings area of the Windows Control Panel, is reported.
If you don't happen to know them by heart, the EnumTimeZones
command line option delivers a complete list of the time zone IDs, along with a great deal of information about each. Since this task creates a very wide table, you may want to pipe the output into a text file, so that you don't have to unwrap the very long lines that cover the basics of each time zone. Somewhat to my surprise, the Time Zone ID is not some fabricated, otherwise meaningless code; in most cases, it is the name by which the time zone is known in the country for which the computer is configured.
If you would rather avoid dealing with a console program that creates wide reports, you can find the output that was generated by that report from the computer on which the original Time Zone Lab was created in worksheet Time Zones on ZAPHOD42
of Microsoft Excel workbook DST_Transition_Date_Computations.XLSX
, about which more shall be said shortly. For those who are just itching to know, ZAPHOD42
is the NETBIOS name I gave to that computer, which is now mostly relegated to supporting a DOS application that remains under my occasional care. This year, my work has moved to a spiffy new Windows 7 machine that runs a 64 bit operating system, and is assigned a NETBIOS name of ENIGMA
.
The table of adjustment rules is exposed to .NET programs through the TimeZoneInfo.AdjustmentRule
object, in particular, its TransitionTime
property, which defines the parameters from which the adjustment dates for any year covered by the rule are derived.
The TimeZoneInfo.AdjustmentRule Object
A TimeZoneInfo.AdjustmentRule
object exposes the following five properties that fully describe one DST adjustment rule.
DateStart
is a System.DateTime
structure that identifies the earliest date to which the rule applies. Although the property is a complete DateTime
structure, only the date portion is used; the members that represent times are set to zero. DateEnd
is another System.DateTime
structure that identifies the last date to which the rule applies. The observation made above about the DateStart
member applies to this member, too. -
DaylightDelta
is a System.TimeSpan
structure that gives the amount of time that is added to the standard time to derive the DST time. The Days
member is always zero, and I suspect it is safe to say that so are the Seconds
amd Milliseconds
members. Some regions may use the Minutes
member to adjust by a fraction of an hour, although I did not scan for such cases. Regardless, the Ticks
member is the number of Ticks (100 nanosecond increments) that is equal to the sum of everything else, converted to ticks.
-
DaylightTransitionStart
is a TransitionTime
structure that defines the parameters required to determine when DST starts.
-
DaylightTransitionEnd
is a TransitionTime
structure that defines the parameters required to determine when DST ends.
The TransitionTime Structure
Just as a TimeZoneInfo.AdjustmentRule
object exposes five properties, so does a TransitionTime
structure. I suspect the reason for this being a structure, rather than a full-fledged class, is that the members are read directly from the binary TZI
value in the Windows Registry. Unlike the other adjustment rule members, nothing needs transformation.
-
IsFixedDateRule
is a Boolean value that, if true, means that the transition occurs on a fixed date, such as the first day of November. Otherwise, the transition occurs on a floating date that is computed from the values of the other properties.
-
Month
is the ordinal number of the month in which the transition occurs. This propertiy applies to all transition dates. Valid values are 1 through 12, the month numbers to which you are almost certainly accustomed,, which coincidentally correspond to the values found in the wMonth
member of the SYSTEMTIME
structure used by the underlying Windows API routines. This member is a System.Int32
.
-
Day
is the ordinal day of the month on which the transition occurs. Its value applies only to transitions that occur on a fixed date, that is, when IsFixedDateRule
is true. When IsFixedDateRule
is false, its value is 1, which is ignored. This member is a System.Int32
.
-
DayOfWeek
expresses the weekday on which the transition occurs as a member of the System.DayOfWeek
enumeration. These names are symbolic, and are not localized, meaning that you may see Sunday, even though you know it as Sontag, the German name for Sunday, but this is no different than other enumerated types, and they thoughfully provided instructions for getting a localized name, right in the documentatin page, https://msdn.microsoft.com/en-us/library/system.timezoneinfo.transitiontime.dayofweek(v=vs.110).aspx. You should be aware that the numerical values are from zero for Sunday through 6 for Saturday, which happens to correspond to the values found in the wDayOfWeek
member of the SYSTEMTIME
structure used by the underlying Windows API routines. This member plays a role when the IsFixedDateRule
member is false, which I suspect is most of the time.
-
Week
is the week of the month in which a time change occurs, ranging in value from 1 to 5, where 5 means the last weekday of the specified month, and applicable only to floating dates, that is, those for which the IsFixedDateRule
member value is false. This member is a System.Int32
.
Though I could have had this method generate a table of transition dates for a span of years or calculate them for a specified year, I chose to put that work into the Microsoft Excel workbook that already held the aforementioned time zone chart. The next section discusses the worksheet, and explains its formulas.
Microsoft Excel Workbook DST_Transition_Date_Computations.XLSX
The most relevant, not to mention interesting, worksheet is CST Adjustment Rules
, which is divided into three major sections, with three minor sections that play supporting roles.
- The entire worksheet revolves around the section near the top, composed of 25 columns divided into four color coded groups. This table stores everything in the two adjustment rules stored in my computer for the
Central Standard Time
zone in which I live.
- The first part, with the blue headings, gives the index (array subscript), ordinal, and effective dates of a DST adjustment rule. The effective dates correspond to the
DateStart
and DateEnd
properties of the TimeZoneInfo.AdjustmentRule
from which it was populated. - The columns in the pink region display the relevant members of the
TimeSpan
structure that holds the time adjustment delta, corresponding to the DaylightDelta
member from which the values originated. - The columns that have green headings and borders correspond to the
DaylightTransitionStart
structure, a TimeZoneInfo.TransitionTime
structure, in case you forgot; everyting in it is displayed, augmented by lookups of the weekday and month names from two small tables located past the right end of the entire table. These tables comprise two of the three supporting actors mentioned above. - The columns with tan headings and brown borders correspond to another
TransitionTime
structure, DaylightTransitionEnd
.
- Immediately beneath the blue columns described above is another distinct region in which the cells in all but the last row are blue. Since it depends on the first area, it was created next, and was used to work out the formulas that went into the last major section.
- Since this area is devoted to computing the transition dates for a single year, the only input variable is cell
B20
, which corresponds to a one-cell named range, DST_Year
. - The cells to the right use the values in the blue cells above them to compute and display the transition dates.
- The blue cells are populated from the the applicable row in the multi-colored table above based on the transition rule that applies to the specified year.
- The formula in the cell to the left of the year, cell
A20
, determines the correct adjustment rule to apply by range testing the year in cell A20 against both rows in the table of adjustment rules. Since I was iterested in just one time zone, which has just two rules, I didn't bother to figure out how to range test against an infinite number of rows. Should it be necessary, that is left as an exercise for the first reader who needs it. - The ten blue cells above the two transition dates are one-cell named ranges, from which I constructed their formulas. I use this technique when I want or need to make the parameters that went into a formula clear to an audience, which might be me a year ot two from now.
- The final major section is a table of transition dates from 1999 through 2400, which happens to be the next leap century.
- The columns that have purple headings and borders contain the years and transition dates.
- The remaining columns display the values looked up from the adjustment rule tables, from which the adjustment dates are derived.
- Except for the column that stores the
IsFixedDateRule
value, the column names in ghe light gray section corresond exactly to those of the columns in the table of adjustment rules from which their values are drawn. To avoid further widening of the columns, I shortened those labels to Fixed
, which should convey what they are. - Between the row that contains the column group labels and the row that contains the individual column labels is an unlabeled row that contains numbers; these are the column numbers from which the data in that column are drawn from the table of adjustment rules. The lookup formulas use the values in these cells, so that the formula in the upper left corner can be copied into the rest of the table. Otherwise, the cell formulas are garden variety VLOOKUP formulas that use addrresses anchored to the column that stores the
Index
of the adjustment rule (column G
) and the row (row 23
) that stores the column of the Adjustment_Rules_Lookup
range from which to draw the value for the cell. - The formulas in in the transition date cells in the purpose area were developed by copying the two formulas that were constructed with named ranges into a text editor, then using its Find and Replace function to replace each named range with the appropriate cell from the top row in the region of light gray data cells to their right. Once tested, these two formulas were copied down 402 times to complete the table.
There are three key formulas that warrant explanation, the first of which is the one in column G
, which determines the rule upon which everything to its right is based by performing two inclusive range tests. At first glance, it looks like a monster, and it is on the long side.
=IF(AND(D25>=YEAR(CST_Adjustment_Rule_1_Start_Date),D25<=YEAR(CST_Adjustment_Rule_1_End_Date)),0,IF(AND(D25>=YEAR(CST_Adjustment_Rule_2_Start_Date),D25<=YEAR(CST_Adjustment_Rule_2_End_Date)),1,"Undefined"))
This formula is easier to understand when it is formatted more like a nested method call, as follows.
<code>=IF ( AND ( D25 >= YEAR ( CST_Adjustment_Rule_1_Start_Date ) ,</code>
<code> D25 <= YEAR ( CST_Adjustment_Rule_1_End_Date ) ) ,</code>
<code> 0 ,</code>
<code> IF ( AND ( D25 >= YEAR ( CST_Adjustment_Rule_2_Start_Date ) ,</code>
<code> D25 <=YEAR ( CST_Adjustment_Rule_2_End_Date ) ) ,</code>
<code> 1 ,</code>
<code> "Undefined"</code>
<code> )</code>
<code> )</code>
The formulas that calculate the transition year are easier to understand if you start by wrapping your head around the fromulas in cells C20 and D20, which are written with named ranges.
The formula in cell C20 calculates the DST transition start date (the date on which observation of DST begins) is as follows.
=IF(DST_Start_IsFixedDate,DATE(DST_Year,DST_Start_Month,DST_Start_DayOfMonth),DATE(DST_Year,DST_Start_Month,(DST_Start_Week*7+1)-WEEKDAY(DATE(DST_Year,DST_Start_Month,8-DAY(DST_Start_Weekday)))))
I'll parse this one out like I did the adjustment rule lookup.
<code>=IF ( DST_Start_IsFixedDate ,</code>
<code> DATE ( DST_Year ,</code>
<code> DST_Start_Month ,</code>
<code> DST_Start_DayOfMonth ) ,</code>
<code> DATE ( DST_Year ,</code>
<code> DST_Start_Month ,</code>
<code> ( DST_Start_Week * 7 + 1 )</code>
<code> - WEEKDAY ( DATE ( DST_Year ,</code>
<code> DST_Start_Month ,</code>
<code> 8 - DST_Start_Weekday </code>
<code> )</code>
<code> )</code>
<code> )</code>
<code> )</code>
The outermost IF
function evaluates a Boolean, DST_Start_IsFixedDate
.
- Since a value of
True
means that the transition occurs on a fixed day each year, computation of which is straightforward. - Computation of a floating date is considerably more complex, and is easier to understand if it is broken down a bit further.
- Computing the year and month components of the date is identical for both fixed and floating dates. The formula could be simplified by moving the
IF
function into the third argument of a single Date
function. However, since the formula as written is tested, meets my immediate need, and is not noticeably inefficient, I shall leave simplification as an exercise for another colleague who cherishes computational efficiency as much as do I. - The
Day
portion of the floating date formula divides neatly into two halves, which I shall diagram separately. - The first half is
DST_Start_Week * 7 + 1
, which gives the day of the month on which the Sunday following the transition falls, keeping in mind that both Windows and Excel start their weeks on Sunday, and that DST_Start_Week
values range from 1, for the first week of a month, to 5, which represents the last week of a month, which is usually a partial week. - From this value, the
Weekday
on which the transition occurs, which is an integer value between 1 (Sunday) and 7 (Saturday) is subtracted. - Since the
Weekday
function expects a Date
as input, a nested Date
function supplies it. - The
Year
and month of the final nested Date
function are as above, while the Day
is 8
less DST_Start_Weekday
. Think this through; since Sunday is 1, Day resolves to 7 (8 - 1). Hence, for the year 2016 in the Central Standard Time zone,, the formula is Date (2016,3,15)-Weekday(Date(2016,3,7))
, which simplifies to Date (2016,3,15)-2
, or Sunday, 13 March 2016.
Computation of the ending transition date is analogous, substituting the _End_
rnage names for the _Start_
ranges.
The formulas in the table follow the same design, replacing the range names with relative column addresses, with the caveat that the final subexpression, e. g., N25+1
in cell E25
, must be enclosed in parentheses to force the correct evaluation order.
For the sake of completeness, the third of the three bit players in this worksheet is the legend in the upper left corner.
I don't expect anybody to use the 402 year table, which I created as a mental exercise. I expect to actually use the year field in the blue region, which requires less work; enter a year, get transition dates.
To prevent accidents that would render it unusable, the workseet is protected. The password, TheCodeProject
, is no big secret; I put a copy in the comments section of the workbook's advanced properties. The year cell in the transition date calculation area is open, as are the adjustmenr rule cells in the top table, so that you can input the values for the time zone of interest to you.
I cannot leave the workbook without calling attention to a copule of its other interesting features.
_Index of Named Ranges
is a standard worksheet that I keep in my library of Excel templates, from which I can quickly insert it into any maderately complex project that uses many ranges, such as this one. You fill it by placing the insertion point in cell A2
and selecting Paste Names, the last choice on the Use in Formula pulldown on the Formulas ribbon. This command fills the cells in columns A
and B
, while formulas fill the remaining columns. If additional ranges are pasted below the bordered cells, do the following.
- Copy the formats from the last bordered row of columns
A
and B
down as far as necessary, or maybe a bit further, to allow room for further growth. Use Paste Special for this task. - Copy everything from the last bordered row of columsn
C
through H
down as far as you copied the formats in columns A
and B
. Since you want everything, this is a regular copy operation.
Index
is a directory of the entire workbook, which makes all of its worksheets readily accessible without usintg the clumsy VCR buttons that Microsoft provided at the bottom of the window. This worksheet is generated by a VBA macro in an add-in, which can be run as often as necessary to update the index. When you run the macro, the index is completely rebuilt, then sorted by worksheet name. If you insert another sheet in front of it, not to worry; the next time you run the macro, a brand new sheet is inserted at the very front of the tab order. The add-in, WorkbookIndexGenerator.XLAM
, is included, along with DST_Transition_Date_Computations.XLSX
, in the NOTES
directory of the archive that accompanies this article. The VBA project in the add-in is signed with a code signing certificate that was valid when I signed it.
I usually manually color code the first two tabs as shown; the macros don't do this, and I've never bothered to fully automate maintenance of the range name index. Once upon a time, I had a macro that did everything, but I found that it wa too combersome, and abandoned it long ago.
Points of Interest
Shifting the focus back to the C# code, while most of the code is unremarkable (especially the new work), there are a couple of noteworthy features of the new code, and several in the esisting code that are worth your attention.
Rearranging Output Generated by string.Format
The ShowTransitionTimeDetails
method began as a garden variety string formatter, extracted into its own one-statement method, since the same statement is needed twice to report the complete contents of an adjustment rule, each of which contains two TransitionTime
structures.
private static string ShowTransitionTimeDetails ( TimeZoneInfo.TransitionTime ptzTransitionTime )
{
return string.Format (
"IsFixedDateRule = {0}, Month = {3}, Week = {5}, Day = {1} ({2}), TimeOfDay = {4}" ,
new object [ ]
{
ptzTransitionTime.IsFixedDateRule ,
ptzTransitionTime.Day ,
ptzTransitionTime.DayOfWeek ,
ptzTransitionTime.Month ,
ptzTransitionTime.TimeOfDay ,
ptzTransitionTime.Week
} );
}
Its noteworthy feature is that the current format control string is a revision that displays the fields in a different order than did the original. However, since format items are matched with elements in the parameter array by their positions (subscripts), the array was unaffected. This is a welcome improvement over the limitations imposed by the printf
functions in the C runtime library, Indeed, it makes me wonder whether anybody has considered porting string.Format
to C.
Robust Command Line Argument Parsing
Almost every Windows program I have ever created, even many that have graphical user interfaces, and run in the Windows subsystem, accept command line arguments. While this is hardly unusual, what might be is the work that I have put into the robust command line parsing engine that I use. The next figure shows the method call that instantiates the command line engine for the time zone lab.
CmdLneArgsBasic cmdArgs = new CmdLneArgsBasic (
new char [ ] { SW_OUTPUT } ,
CmdLneArgsBasic.ArgMatching.CaseInsensitive );
cmdArgs.AllowEmptyStringAsDefault = CmdLneArgsBasic.BLANK_AS_DEFAULT_ALLOWED;
Since this assembly has a narrow focus and few options, it makes few demands on the command parsing engine. The constructor defines one switch, SW_OUTPUT
, and specifies that argument matching shall be case insensitive. The remaining arguments are positional. Since a CmdLneArgsBasic
object supports up to nine of these by default, nothing further is required to process the maximum of two that it needs. The CmdLneArgsBasic
class is one of a number exported by WizardWrx.DLLServices2.dll
, which I recently published as an open source repository on GetHub. The repository URL is https://github.com/txwizard/DLLServices2, The library is available under a three-clause BSD license, and I would be delighted to have some extra hands to extend it in several ways. Since its dependence upon three 32 bit native functions limits the library to 32 bit address spaces, I think the most valuable feature that the library needs is set forth in the first issue opened against the project (by me), which you can review at https://github.com/txwizard/DLLServices2/issues/1.
Suppressing the Banner
Although this assembly doesn't really need it, since its development started from a template into which it was already incorporated, it gets a feature that enables it to suppress part or all of its console output. Since the routine that makes this decision might need to create a warning message that need not be displayed until after the banner, it returns the message through an out
parameter of type string
. The block of code shown below processes this command line option, which happens to be the only supported switch. Depending on the outcome, it displays the banner if the output option allows it. Next, if SetOutputFormat
left a message in its out
paramter, the message is displayed. Finally, the block that began with the definintion of the output string closes, allowing the string to go out of scope, and be garbage collected. Since the OutputFormat
must outilive this block, it is defined and initialized just before execution enters the block.
OutputFormat enmOutputFormat = OutputFormat.None;
{
string strDeferredMessage = null;
enmOutputFormat = SetOutputFormat (
cmdArgs ,
ref strDeferredMessage );
if ( enmOutputFormat != OutputFormat.None )
{
s_theApp.DisplayBOJMessage ( );
}
if ( !string.IsNullOrEmpty ( strDeferredMessage ) )
{
Console.WriteLine ( strDeferredMessage );
}
}
The SetOutputFormat
method parses its string input into a nice, compact enumerated type that is much more efficient to process and pass around.
private static OutputFormat SetOutputFormat (
CmdLneArgsBasic pcmdArgs ,
ref string pstrDeferredMessage )
{
const bool IGNORE_CASE = true;
const int NONE = 0;
OutputFormat renmOutputFormat = OutputFormat.Verbose;
try
{
if ( pcmdArgs.ValidSwitchesInCmdLine > NONE )
{
renmOutputFormat = ( OutputFormat ) Enum.Parse (
typeof ( OutputFormat ) ,
pcmdArgs.GetSwitchByName (
SW_OUTPUT ,
OutputFormat.Verbose.ToString ( ) ) ,
IGNORE_CASE );
}
}
catch ( ArgumentException exArg )
{
s_theApp.BaseStateManager.AppExceptionLogger.ReportException ( exArg );
pstrDeferredMessage = string.Format (
Properties.Resources.ERRMSG_INVALID_OUTPUT_FORMAT ,
WizardWrx.StringTricks.ExtractBoundedSubstrings (
exArg.Message ,
SpecialCharacters.SINGLE_QUOTE ) ,
renmOutputFormat ,
Environment.NewLine );
}
return renmOutputFormat;
}
I prefer enumerations over strings for use within switch blocks for two reasons.
- Enumerations are extremely efficient to process. I have seen debug code that implemented a switch statement as a jump table. It doesn't get any better than that!
- Strings for use in a switch block musst be either literals hard coded into the statement or constants. Resource strings are not considered constants, and cannot be used.
There is probably at least another nugget or two that I've overlooked. I have been told that reading my code is a lot like peeling an onino, but without the tears. Enjoy!
History
Thursday, 16 June 2016 is the first version of this article.