I have been working on cleaning my office of 8 years of stuff from several engineers working on many projects.  It turns out that we have a few extra single board computers with displays, so at the end of the day last Friday I though why not create a little application to display the time, you know, a clock. 

How difficult could that be?  It turns out that it is quite simple – until I decided to gold plate the project by adding time displays for our offices around the world.

I decided to use C#, which actually made creating the main clock quite easy.   The application was simply a text box and a timer.  I set the timer to fire a couple of times a second, and when it does use a DateTime object to get the current time and retrieve a string to display.

And I could have been done, but of course that gold plating came up.   Seems simple enough, simply offset the time from the local time to the location that I want the time for and display it.    Sure enough, I had the time displayed for UK, Italy, Kansas City, Japan and China in no time at all.

But it is October, and for those of us still stuck with Daylight Savings Time, we know that the clocks are about to change.   My first attempt was to simply check to see if the local time was DST or Standard time, then change the offset for China.  China doesn’t have Daylight Savings Time.

If you know anything about the time changes around the world, you already know that my plan is flawed – in a big way.   It turns out that the transitions in and out of DST take place at different times around the world.   If you didn’t know that, do a quick search for “Daylight Savings” and you will find many WEB sites dedicated to tracking the time changes dates, and times.

Now the real challenge of this application; how do I programmatically find out when the time changes occur and handle them correctly?  After a considerable amount of research it turns out that the solution is to read the data from the registry and parse it to figure out when the time changes occur.

Reading Time Change Information from the Registry

Reading the data from the registry is simple, using the data is a little more complicated.  First, reading from the registry can be done like:

            byte[] binarydata = (byte[])Registry.GetValue("HKEY_LOCAL_MACHINE\\Time Zones\\Eastern Standard Time", "TZI", null);

 

Where I have hardcoded the registry key for example purposes, but in the end I will use some variables.  

We now have a binary blob with the data, but it needs to be converted to use the real data.   To start we will need a couple of structs to hold the data and make it usable.   We will need a SYSTEMTIME and REG_TZI_FORMAT.   You may have expected that we would need a TIME_ZONE_INFORMATION struct, but we don’t.   The data is stored in the registry as a REG_TZI_FORMAT, which excludes some of the values found in TIME_ZONE_INFORMATION.

    struct SYSTEMTIME

    {

        internal short wYear;

        internal short wMonth;

        internal short wDayOfWeek;

        internal short wDay;

        internal short wHour;

        internal short wMinute;

        internal short wSecond;

        internal short wMilliseconds;

    }

 

    struct REG_TZI_FORMAT

    {

        internal long Bias;

        internal long StdBias;

        internal long DSTBias;

        internal SYSTEMTIME StandardStart;

        internal SYSTEMTIME DSTStart;

    }

 

Now we need to convert the binary blob to a REG_TZI_FORMAT.   To do that I created the following helper functions:

        private void BinaryToSystemTime(ref SYSTEMTIME ST, byte[] binary, int offset)

        {

            ST.wYear = (short)(binary[offset + 0] + (binary[offset + 1] << 8));

            ST.wMonth = (short)(binary[offset + 2] + (binary[offset + 3] << 8));

            ST.wDayOfWeek = (short)(binary[offset + 4] + (binary[offset + 5] << 8));

            ST.wDay = (short)(binary[offset + 6] + (binary[offset + 7] << 8));

            ST.wHour = (short)(binary[offset + 8] + (binary[offset + 9] << 8));

            ST.wMinute = (short)(binary[offset + 10] + (binary[offset + 11] << 8));

            ST.wSecond = (short)(binary[offset + 12] + (binary[offset + 13] << 8));

            ST.wMilliseconds = (short)(binary[offset + 14] + (binary[offset + 15] << 8));

        }

 

 

        private REG_TZI_FORMAT ConvertFromBinary(byte[] binarydata)

        {

            REG_TZI_FORMAT RTZ = new REG_TZI_FORMAT();

 

            RTZ.Bias = binarydata[0] + (binarydata[1] << 8) + (binarydata[2] << 16) + (binarydata[3] << 24);

            RTZ.StdBias = binarydata[4] + (binarydata[5] << 8) + (binarydata[6] << 16) + (binarydata[7] << 24);

            RTZ.DSTBias = binarydata[8] + (binarydata[9] << 8) + (binarydata[10] << 16) + (binarydata[11] << 24);

            BinaryToSystemTime(ref RTZ.StandardStart, binarydata, 4 + 4 + 4);

            BinaryToSystemTime(ref RTZ.DSTStart, binarydata, 4 + 16 + 4 + 4);

 

            return RTZ;

        }

 

I am the first to admit that there may be a better way to get the settings from the registry and into the REG_TXI_FORMAT, but I am not a great C# programmer which I have said before on this blog.   So sometimes I chose brute force over elegant.

Now that we have the Bias information and the start date information, we can start to make sense of it.   The bias is an offset, in minutes, from local time (if already in local time for the time zone in question) to get to UTC – or as Microsoft defines it: UTC = local time + bias.  Standard bias is an offset to adjust for standard time, which I think is usually zero.   And DST bias is and offset to adjust for daylight savings time.

Since we don’t have the local time for a time zone other than the one that the computer is set to, what we first need to do is convert local time to UTC, which is simple enough using:

                DateTime.Now.ToUniversalTime();

Then, since we have UTC we need to do a little math to alter the formula to: local time = UTC – bias.  In other words, we need to subtract the bias minutes.

I am ahead of myself though, the standard and DST start dates really aren’t dates.   Instead they indicate the month, day of week and week number of the time change.   The dDay member of SYSTEM time will be set to the week number of the date change indicating that the change happens on the first, second… day of week of the month.  So we need to convert them to dates so that we can determine which bias to use, and when to change to a different bias.   To do that, I wrote the following function:

        private DateTime SystemTimeToDateTimeStart(SYSTEMTIME Time, int Year)

        {

            DayOfWeek[] Days = { DayOfWeek.Sunday,

DayOfWeek.Monday,

DayOfWeek.Tuesday,

DayOfWeek.Wednesday,

DayOfWeek.Thursday,

DayOfWeek.Friday,

DayOfWeek.Saturday };

            DateTime InfoTime = new DateTime(Year,

Time.wMonth,

Time.wDay == 1 ? 1 : ((Time.wDay - 1) * 7) + 1,

Time.wHour,

Time.wMinute,

Time.wSecond,

DateTimeKind.Utc);

            DateTime BestGuess = InfoTime;

            while (BestGuess.DayOfWeek != Days[Time.wDayOfWeek])

            {

                BestGuess = BestGuess.AddDays(1);

            }

            return BestGuess;

        }

 

SystemTimeToDateTimeStart gets two parameters; a SYSTEMTIME and a year.   The reason is that we will try this year and next year because we are interested in start dates that are in the future, not the past.  The function starts by getting a new Datetime with the first possible date and then looking for the correct date.

Using the start dates, we can then determine the correct bias to use, and the next date that time will change:

            NextTimeChange = StandardChange;

            CurrentBias = TimezoneSettings.Bias + TimezoneSettings.DSTBias;

            if (DSTChange.Year != 1 && StandardChange.Year != 1)

            {

                if (DSTChange.CompareTo(StandardChange) < 0)

                {

                    NextTimeChange = DSTChange;

                    CurrentBias = TimezoneSettings.StdBias + TimezoneSettings.Bias;

                }

            }

            else

            {

                // I don't like this, but it turns out that China Standard Time

                // has a DSTBias of -60 on every Windows system that I tested.

                // So, if no DST transitions, then just use the Bias without

                // any offset

                CurrentBias = TimezoneSettings.Bias;

            }

 

Note that some time zones do not change time, in which case the years will remain set to 1.   Further, I found that the registry settings are actually wrong in that the DST Bias is set to -60 for China even though there is not DST in China, so I ignore the standard and DST bias for those time zones.

There is one thing that I have not solved, and don’t plan to solve.  If the time zone for this computer changes, this application will not update the clock using the new time zone.  I tell  you this because you may need to deal with it – I do not because I won’t let the user get to the control panel applet to change the timezone.

Copyright © 2012 – Bruce Eitman

All Rights Reserved