- You Can't Know Everything
- Raise Your Confidence
- Let's Discover Something
- Encode the Knowledge in Tests
- Go Where No Man Has Gone Before
- Conclusion
Let's Discover Something
Let's rejoin our eXtreme .NET team to see how they are learning about spiking.
Exercise 6-1: Spiking How Time Zone Data Works in Windows
In this exercise, we are going to investigate how we can use time zone information in our application. In Chapter 3 we encountered this as a task we couldn't accurately estimate. We didn't know enough about how time zones are stored to give an accurate estimate. We need to understand how to access the time zone data from the operating system. Then we need to work out how to use this information to get the time in different places in the world. With this knowledge, we can go back to our team with some firmer estimates about how long this task will take to develop.
The first place to look is the classes that ship as part of the .NET Framework. If there is support for time zone data there, our lives will be made much simpler.
-
Open up the help file and look up the .NET Framework DateTime structure.
Unfortunately the only time zone–related functionality provided is to convert local times to time in UTC (formally GMT; don't ask me why this was changed!) and back again. This just works using the local time zone that the machine is currently running in. As the documentation states:
This method assumes that the current DateTime holds the local time value, and not a UTC time. Therefore, each time it is run, the current method performs the necessary modifications on the DateTime to derive the UTC time, whether the current DateTime holds the local time or not.
This method always uses the local time zone when making calculations. (Source: MSDN library)
-
Within the System namespace, there is a TimeZone class; let's see whether that is useful for us.
All the documentation has to say about it is this:
A time zone is a geographical region in which the same standard time is used. (Source: MSDN library)
Not very verbose, but it sounds as though it might be promising.
If you examine the methods, you will discover that the constructor is protected, and the only way to create an instance of this class is to use the static property CurrentTimeZone. This is not very useful if we want to get the time zone information for different time zones.
(As another exercise, you can try to build a test application using the Time-Zone type and see whether you can get time zone for some other part of the world.)
-
Our next stop is to see what the Win32 API provides in terms of time zone functionality. A couple of functions look interesting: GetTimeZoneInformation and SetTimeZoneInformation. They both work with a TIME_ZONE_INFORMATION structure, which contains all the data that we need to build our application; for a given time zone, the fields in the structure are as shown in the following table.
Time Zone Fields
Field
Description
Bias
The offset from UTC in minutes
Standard Name
The name of standard time on this machine in this time zone
Standard Date
The date and time that daylight savings moves over to standard time
Standard Bias
The additional difference from UTC during standard time (usually zero)
DaylightName
The name of daylight savings time on this machine in this time zone
DaylightDate
The date and time of the transition from standard time to daylight savings time
DaylightBias
The additional difference from UTC during daylight savings time (usually–60)
This looks promising, but the methods only enable you to get or set the time zone on the local machine and retrieve time zone information about the time zone set on the local machine.
We are still stumped. We want to get time zone information for all the available time zones in the world. We figure it must be possible because the Control Panel extension that you use to set the Date and Time does it in the Time Zone tab.
-
That is the next place for us to look. If we could work out how the timedate.cpl (basically a DLL) worked, we could emulate that and get all the time zone information for around the world. So, first, we can look to see whether it exposes any form of interface (COM or .NET). You can try to open it using OLE View or ildasm, but no luck there; it supports neither a COM interface nor does it contain CLR header information.
-
Back to basics, open the CPL in Visual Studio as a resource to examine whether there are any clues in there. Maybe the time zone information might have been stored in the DLL (maybe in a string table).
Nope.
The only thing left we can think of is to open up the timedate.cpl file in a hex editor. (Yuck, I haven't poked around inside Windows system files for a while; surely there's a good reason for that!)
We begin this stage of the investigation by examining the DLLs that the code uses, all the usual suspects: KERNEL32, NTDLL.DLL, USER32.dll, COMCTL32.dll, ole32.dll, SHELL32.dll, GDI32.dll, ADVAPI32.dll, IMM32.dll and SHLWAPI.dll. Nothing of use here.
So carry on looking through the file for any other obvious clues; maybe the time zone data is hard coded in the DLL. Again no.
So where did the time zone information get stored? How about the Registry? Look for any references to Registry paths in the CPL file; you should find a few. The first ones lead to information about time synchronization via a timeserver and then some Registry entries for storing the current time zone settings for the local machine. Then about halfway down the file you can find a reference to the Registry key: Software\Microsoft\Windows NT\CurrentVersion\Time Zones.
Eureka!
-
This key contains a subkey for each time zone that is listed in the combo box in the second tab of the timedate.cpl. The data in each of the subkeys has to provide all the data required to build a TIME_ZONE_INFORMATION structure so that the application can set the time zone for the system. Now we just have work out how the data is formatted.
Registry Information for Each Time Zone
Value
Meaning
Display
Extra information about the time zone for display purposes
Dlt
DaylightName; the name of daylight savings time on this machine
Index
Unknown
MapID
Presumably used for placing on the bitmap of the world in the CPL
Std
StandardName; the name of standard time on this machine
TZI
All the other data we need to fill a TIME_ZONE_INFORMATION structure
-
The string data is reasonably clear, as shown in the preceding table; we just need to work out how the byte data in the TZI (Time Zone Information) is stored. We start off by going back to the documentation on the structure, from which we can draw up the following table. So adding up the numbers, we are looking for 44 bytes of information, and the TZI value in the Registry is exactly 44 bytes long. Therefore, we must assume that we have all the data we need in the byte array. All we need to do is figure out which byte means what!
Break Down of Time Zone Information
Data Type
Name
Description
Bytes
LONG
Bias
In minutes
4
SYSTEMTIME
StandardDate
Month, day of week and day only
16
LONG
StandardBias
Mostly 0 (minutes)
4
SYSTEMTIME
DaylightDate
Month, day of week and day only
16
LONG
DaylightBias
Mostly –60 (minutes)
4
-
On your own, write a program that decodes the byte data in the TZI fields in the Registry, and try to work out how the time zone information data is stored in the TZI field. The best thing to do would be to write some tests that validate how we believe the time zone data is stored. We can run those tests when the OS changes and check whether the way time zone data is stored has changed.
You should have deduced the data was stored in the following order: Bias, Standard Bias, Daylight Bias, Standard Date, and Daylight Date. Okay, now we are ready to put together some code to use this information.
Below are some functions written in C# that use the time zone information from the Registry.
-
To populate a list with the available time zones, a static method on a class is shown first. It is static because it requires no instance data to be functional.
public static string[] GetTimeZones() { RegistryKey regKey = Registry.LocalMachine; regKey = regKey.OpenSubKey( @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\"); return regKey.GetSubKeyNames(); }
GetTimeZones Function
After a time zone has been selected, you must get the time zone information for the place selected. The next set of code demonstrates this.
-
Open the Registry key for the time zone provided.
-
Read in the TZI value to a byte array.
-
Use some helper functions to extract the data out of the byte array and into the member variables of the PlaceTime object.
The helper functions MakeUShort and MakeInt are equivalent to the traditional Windows MAKEWORD and MAKELONG macros. Remember that a Long in .NET world is 64 bits and not 32 bits as it was in the old Win32 world. GetValueFromBytes builds a 32-bit integer value from an array of bytes, using an offset from the beginning of the array at which to start "stripping" the bytes.
protected void GetTimeZoneInfo(string strTimeZone) { RegistryKey regKey = Registry.LocalMachine; regKey = regKey.OpenSubKey( @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\" + strTimeZone); System.Byte[] tziData = (Byte[])regKey.GetValue("TZI"); m_nBias = GetValueFromBytes(tziData, 0); m_nStandardBias = GetValueFromBytes(tziData, 4); m_nDaylightBias = GetValueFromBytes(tziData, 8); m_StandardDate = GetDateTimeFromBytes(tziData, 12); m_DaylightDate = GetDateTimeFromBytes(tziData, 28); } private static ushort MakeUShort(byte low, byte high) { return (ushort) (low | (high<< 8)); } private static int MakeInt(ushort low, ushort high) { return (int) (low | (high<< 16)); } private static int GetValueFromBytes(byte[] data, int offset) { int nResult = 0; ushort lowWord = MakeUShort(data[offset],data[offset+1]); ushort highWord = MakeUShort(data[offset+2],data[offset+3]); nResult = MakeInt( lowWord, highWord ); return nResult; } private static DateTime GetDateTimeFromBytes(byte[] data, int offset) { DateTime dateTime = DateTime.UtcNow; int Year = MakeUShort(data[offset],data[offset+1]); int Month = MakeUShort(data[offset+2],data[offset+3]); DayOfWeek DayofWeek = (DayOfWeek)MakeUShort(data[offset+4],data[offset+5]); int Day = MakeUShort(data[offset+6],data[offset+7]); int Hour = MakeUShort(data[offset+8],data[offset+9]); int Minute = MakeUShort(data[offset+10],data[offset+11]); int Second = MakeUShort(data[offset+12],data[offset+13]); int Milliseconds = MakeUShort(data[offset+14],data[offset+15]); dateTime = dateTime.AddMonths(Month - dateTime.Month); dateTime = dateTime.AddDays(1-dateTime.Day); dateTime = dateTime.AddHours(Hour - dateTime.Hour); dateTime = dateTime.AddMinutes(Minute - dateTime.Minute); dateTime = dateTime.AddSeconds(Second - dateTime.Second ); dateTime = dateTime.AddMilliseconds(Milliseconds – dateTime.Millisecond ); bool bFoundDay = false; int nCoveredRequiredDay = 0; while (dateTime.Month == Month) { if (dateTime.DayOfWeek == DayofWeek) { nCoveredRequiredDay +=1; if (nCoveredRequiredDay == Day) { bFoundDay = true; break; } } dateTime = dateTime.AddDays(1); } while(!bFoundDay) { dateTime = dateTime.AddDays(-1); if (dateTime.DayOfWeek == DayofWeek) { bFoundDay = true; } } return dateTime; }
GetTimeZoneInfo and Related Functions
The final helper function, GetDateTimeFromBytes, builds a .NET Framework Date-Time structure from the byte array passed in, starting at the offset provided. This is the more complicated of the methods here and requires some extra explanation.
The TZI byte array contains 2 sets of 16 bytes, which correspond to a Win32 SYSTEMTIME structure. These SYSTEMTIMEs contain the data for the Standard Date and Daylight Date in the TIME_ZONE_INFORMATION structure. The standard and daylight dates represent the date and time at which the transition occurs from daylight savings to standard time and back again.
The TIME_ZONE_INFORMATION structures contain the month, the day of the week (for example, Sunday), the day on which the transition occurs, along with the time. The day can be a value between 1 and 5. If the month is 4, the day is 1, and the day of the week is 0 (Sunday), that would represent the first Sunday in April. If the day value is 5, that always means the last of that day in the month. If the month is 9, the day is 5, and the day of the week is 6, which represents the last Saturday in September.
The GetDateTimeFromBytes function sets the DateTime structure to the first day of the month given and walks through the month until it finds the correct day of the transition. If the method fails to do this, it assumes (dangerous, I know) that we want the last occurrence of that day of the week in the month and starts walking backward through the month until it finds that day. I have no doubt this could be greatly optimized, but I leave that as another exercise for you!