Mirror

Time Zones, Daylight Savings, and other Delights (Views: 716)

Problem/Question/Abstract:

Time Zones, Daylight Savings, and other Delights

Answer:

If your application uses dates and times, what will happen when it's deployed to users in different time zones? Have you taken the effects of Daylight Savings Time into consideration?

We know that many locations do not recognize Daylight Savings Time; but what about locations in the Southern Hemisphere, such as Brazil and portions of Australia, where its implementation is the opposite of that in the Northern Hemisphere? Your particular application may not be affected, but if you need to convert between local time and Universal Coordinated Time (UTC) for any reason, you should consider the effects that time-zone changes will have on your program.

Who cares about changes in time zones? Your users might. Consider the relatively trivial example of a telephone dialer: wouldn't it be nice if users were notified of the local time when calling a phone number outside of the local calling area? This would require a lookup table of area codes to time zones, but it would certainly be a reasonable enhancement to a dialing program. Some types of programs are vitally concerned with time-zone changes, particularly technical programs, or those relating to navigation and astronomy, to name a few.

You could ask your users to specify time-zone settings when installing your product, but they already specified their date, time, and time zone when they set up their computers. Besides, mobile computing is so pervasive, a user might easily work in multiple time zones in a single day. It seems intrusive to ask the user for information already in the registry. At the same time, it's your responsibility to confirm that their settings make sense, and to offer alternatives if necessary.

A computer running Windows 95/98/NT maintains its internal time as Universal Coordinated Time, and displays the local time based on the user's time-zone setting, and the current state of Daylight Savings Time.

If all your application needs is the current UTC or local time, we could simply call the Win32 API procedures GetSystemTime or GetLocalTime. These functions return a data structure of type SystemTime:

type
SystemTime = record
wYear: Word;
wMonth: Word;
wDayOfWeek: Word;
wDay: Word;
wHour: Word;
wMinute: Word;
wSecond: Word;
wMilliseconds: Word;
end;

Most of the elements of the SystemTime record are self-explanatory; wDayOfWeek is an unsigned 16-bit integer (i.e. Word) value in the range 0-6, which identifies the day of the week, corresponding to Sunday through Saturday.

The elements of SystemTime can be used to assign a Delphi DateTime variable:

var
ST: SystemTime;
DT: TDateTime;
begin
// Get Universal Coordinated Time (UTC).
ST := GetSystemTime(ST);
with ST do
DT := EncodeDate(wYear, wMonth, wDay) +
EncodeTime(wHour, wMinute, wSecond, wMilliseconds);
end;

Note that SysUtils.Now is implemented in exactly this fashion, using GetLocalTime (SysUtils.Now replaces GetCurrentTime, which is obsolete).

Similarly, to determine if Standard or Daylight Time is in effect, we can call the Win32 API function GetTimeZoneInformation:

var
Error: Double;
TZInfo: TTimeZoneInformation;
begin
Error := GetTimeZoneInformation(TZInfo);
case Error of
0: { Unknown };
1: { Standard Time };
2: { Daylight Time };
end;
end;

Time Zone Information

GetTimeZoneInformation also returns a data structure (a record) that contains the current time-zone settings, and the information needed to convert between local and UTC times:

type
TTIMEZONEINFORMATION = record
Bias: Longint;
StandardName: array[0..31] of WCHAR;
StandardDate: TSystemTime;
StandardBias: Longint;
DaylightName: array[0..31] of WCHAR;
DaylightDate: TSystemTime;
DaylightBias: Longint;
end;

Elements of TTimeZoneInformation are shown in Figure 1.
Bias
Difference in minutes between local time and UTC, based on the formula UTC = local time + Bias.
StandardName
Null-terminated string identifying Standard Time, e.g. Pacific Standard Time. May be empty, or set to user's preference with SetTimeZoneInformation.
StandardDate
Record of type SystemTime that specifies the date and time when Standard Time begins.
StandardBias
Minutes added to Bias during Standard Time (normally zero).
DaylightName
Null-terminated string identifying Daylight Time, e.g. Pacific Daylight Time. May be empty, or set to user's preference with SetTimeZoneInformation.
DaylightDate
Record of type SystemTime that specifies the date and time when Daylight Time begins.
DaylightBias
Minutes added to Bias during Daylight Time (normally 60).
Figure 1: Elements of TTimeZoneInformation.

The dates and times represented by StandardDate and DaylightDate are implemented as a set. It's not permissible to have only one or the other specified; either both dates are specified (meaning Daylight Savings Time is implemented), or both are unspecified, in which case wMonth must be set to zero as a flag. Dates may be stored in either of two formats:

Absolute. wYear, wMonth, wDay, wHour, wMinute, wSecond, and wMilliseconds are combined to refer to a specific date and time.
Relative. Also called "day-in-month" format, refers to a particular occurrence of a day of the week, e.g. the last Sunday of the month.

Relative dates are by far the more common, and are implemented as shown here:

wYear must be set to zero as a flag.
wMonth identifies the month in which the change occurs.
wDayOfWeek identifies the day of the week (0-6 corresponding to Sunday-Saturday).
wDay identifies which occurrence of wDayOfWeek (1-5 where 1 is the first occurrence, and 5 means "last wDayOfWeek in the month").

The resulting Relative date is combined with the encoded time from the SystemTime record to specify the exact date and time when the change occurs.

Using this information, we can create a function that will tell us whether Daylight time is in effect for a given date and time. We'll assume for this discussion that TZInfo is a global variable of type TTimeZoneInformation that was returned by an earlier call to GetTimeZoneInformation, perhaps during FormCreate, as shown in Listing One.

Converting between Local Time and Universal Time

We now have enough information to convert local times to UTC, and vice versa. On Windows NT, we might use SystemTimeToTzSpecificLocalTime, which converts UTC to local time in a specific time zone, but this function isn't available to users of Windows 95. We'll continue to assume that TZInfo is a global variable of type TTimeZoneInformation returned by an earlier call to GetTimeZoneInformation, as shown in Listing Two.

We have one more step to make the program complete. Our program can decipher time-zone information, and can convert local times to UTC and back, but how do we handle a situation where the time zone changes while our program is running? The answer is found in the Borland FAQ database (FAQ2020D). First, add the following code to the private declaration section of your application:

private

procedure WMTIMECHANGE(var Message: TWMTIMECHANGE);
message WM_TIMECHANGE;

Then, add this wmTimeChange procedure to the implementation section:

procedure TFormName.wmTimeChange(
var Message: TWMTIMECHANGE);
begin
// Get new Time Zone information.
Error := GetTimeZoneInformation(TZInfo);
// Use value returned by GetTimeZoneInformation
// for error trapping.
case Error of
0: { Unknown };
1: { Standard Time };
2: { Daylight Time };
end;
// Update the form using new date/time settings...
end;

The wmTimeChange procedure can perform whatever action is necessary to update your application with the newly-changed time zone.

Conclusion

Hopefully, you will find this little foray into the Win32 API to be time well spent. Your application is now ready for prime time. Feel free to check out my Time Zone application, discussed in the sidebar TIMEZONE.EXE.

TIMEZONE.EXE is an example application designed to demonstrate using Delphi to access time-zone information, to convert between local time and UTC, and to respond to system-wide changes in the time-zone setting (see Figure A).


Figure A: The Time Zone application.

The program starts up initialized to the current local date and time, and lists the current time-zone setting (top), as well as the UTC and date that correspond to the local time and date in the edit controls.

To keep the program simple, user interaction is limited to two edit controls and two buttons.

The user may select a date using the DateTimePicker. The user may enter a time in the Time edit control. If the string entered by the user fails to convert to a valid time, the exception handler clears the control and sets the time to midnight.

The display is updated when any of the following actions occur:

The user clicks one of the controls.
The user selects a date, and the pop-up calendar closes.
The user tabs from one field to another.
The user presses [Enter] after making an entry in the Time edit control.

Two buttons are provided: Now sets the date and time to the computer's current date and time; while Exit terminates the program.

Begin Listing One
function DaylightSavings(DT: TDateTime): Boolean;
var
D, M, Y, WeekNo: Word;
DTBegins, STBegins: TDateTime;
begin
// Get Year/Month/Day of DateTime passed as parameter.
DecodeDate(DT, Y, M, D);
// If TZInfo.DaylightDate.wMonth is zero,
// Daylight Time not implemented.
if (TZInfo.DaylightDate.wMonth = 0) then
Result := False
else //Daylight Time is implemented.
begin
// If wYear is zero, use relative SystemTime format.
if (TZInfo.StandardDate.wYear = 0) then
// Relative SystemTime format.
// Calculate DateTime Daylight Time begins using
// relative format. wDay defines which wDayOfWeek
// is used for time change: wDay of 1 identifies
// the first occurrence of wDayOfWeek in the month;
// 2..4 identify the second through fourth
// occurrence. A value of 5 identifies the last
// occurrence in the month.
begin
// Start at beginning of Daylight month.
DTBegins :=
EncodeDate(Y, TZInfo.DaylightDate.wMonth, 1);
case TZInfo.DaylightDate.wDay of
1, 2, 3, 4:
begin
// Get to first occurrence of wDayOfWeek.
// Key point: SysUtils.DayOfWeek is
// unary-based; TZInfo.Daylight.wDay is
// zero-based
while (SysUtils.DayOfWeek(DTBegins) - 1) <>
TZInfo.DaylightDate.wDayOfWeek do
DTBegins := DTBegins + 1;
WeekNo := 1;
if TZInfo.DaylightDate.wDay <> 1 then
repeat
DTBegins := DTBegins + 7;
Inc(WeekNo);
until WeekNo = TZInfo.DaylightDate.wDay;
// Encode time Daylight Time begins.
with TZInfo.DaylightDate do
DTBegins := DTBegins + EncodeTime(
wHour, wMinute, 0, 0);
end;
5:
begin
// Count down from end of month to day of
// week. Recall that we set DTBegins to the
// first day of the month; go to the first
// day of the next month and decrement.
DTBegins := IncMonth(DTBegins, 1);
DTBegins := DTBegins - 1;
// Find the last occurrence of
// the day of the week.
while SysUtils.DayOfWeek(DTBegins) - 1 <>
TZInfo.DaylightDate.wDayOfWeek do
DTBegins := DTBegins - 1;
// Encode time Daylight Time begins.
with TZInfo.DaylightDate do
DTBegins := DTBegins + EncodeTime(
wHour, wMinute, 0, 0);
end;
end; // case.
// Calculate DateTime Standard Time begins using
// relative format. Start at beginning of
// Standard month.
STBegins :=
EncodeDate(Y, TZInfo.StandardDate.wMonth, 1);
case TZInfo.StandardDate.wDay of
1, 2, 3, 4:
begin
while (SysUtils.DayOfWeek(STBegins) - 1) <>
TZInfo.StandardDate.wDayOfWeek do
STBegins := STBegins + 1;
WeekNo := 1;
if TZInfo.StandardDate.wDay <> 1 then
repeat
STBegins := STBegins + 7;
Inc(WeekNo);
until (WeekNo = TZInfo.StandardDate.wDay);
// Encode time Standard Time begins.
with TZInfo.StandardDate do
STBegins := STBegins + EncodeTime(
wHour, wMinute, 0, 0);
end;
5:
begin
// Count down from end of month to day of
// week. Recall we set DTBegins to first
// day of the month; go to the first day of
// the next month and decrement.
STBegins := IncMonth(STBegins, 1);
STBegins := STBegins - 1;
// Find last occurrence of day of the week.
while SysUtils.DayOfWeek(STBegins) - 1 <>
TZInfo.StandardDate.wDayOfWeek do
STBegins := STBegins - 1;
// Encode time at which Standard Time begins.
with TZInfo.StandardDate do
STBegins := STBegins + EncodeTime(
wHour, wMinute, 0, 0);
end;
end; // case.
end
else
begin // Absolute SystemTime format.
with TZInfo.DaylightDate do
begin
DTBegins := EncodeDate(wYear, wMonth, wDay) +
EncodeTime(wHour, wMinute, 0, 0);
end;
with TZInfo.StandardDate do
begin
STBegins := EncodeDate(wYear, wMonth, wDay) +
EncodeTime(wHour, wMinute, 0, 0);
end;
end;
// Finally! How does DT compare to DTBegins and
// STBegins?
if (TZInfo.DaylightDate.wMonth <
TZInfo.StandardDate.wMonth) then
// For Northern Hemisphere...
Result := (DT >= DTBegins) and (DT < STBegins)
else
// For Southern Hemisphere...
Result := (DT < STBegins) or (DT >= DTBegins);
end;
end;
end Listing One

begin  Listing Two
function LocalTimeToUniversal(LT: TDateTime): TDateTime;
var
UT: TDateTime;
TZOffset: Integer;
// Offset in minutes.
begin
// Initialize UT to something,
// so compiler doesn't complain.
UT := LT;
// Determine offset in effect for DateTime LT.
if DaylightSavings(LT) then
TZOffset := TZInfo.Bias + TZInfo.DaylightBias
else
TZOffset := TZInfo.Bias + TZInfo.StandardBias;
// Apply offset.
if (TZOffset > 0) then
// Time zones west of Greenwich.
UT := LT + EncodeTime(TZOffset div 60,
TZOffset mod 60, 0, 0)
else if (TZOffset = 0) then
// Time Zone = Greenwich.
UT := LT
else if (TZOffset < 0) then
// Time zones east of Greenwich.
UT := LT - EncodeTime(Abs(TZOffset) div 60,
Abs(TZOffset) mod 60, 0, 0);
// Return Universal Time.
Result := UT;
end;

function UniversalTimeToLocal(UT: TDateTime): TDateTime;
var
LT: TDateTime;
TZOffset: Integer;
begin
LT := UT;
// Determine offset in effect for DateTime UT.
if DaylightSavings(UT) then
TZOffset := TZInfo.Bias + TZInfo.DaylightBias
else
TZOffset := TZInfo.Bias + TZInfo.StandardBias;
// Apply offset.
if (TZOffset > 0) then
// Time zones west of Greenwich.
LT := UT - EncodeTime(TZOffset div 60,
TZOffset mod 60, 0, 0)
else if (TZOffset = 0) then
// Time Zone = Greenwich.
LT := UT
else if (TZOffset < 0) then
// Time zones east of Greenwich.
LT := UT + EncodeTime(Abs(TZOffset) div 60,
Abs(TZOffset) mod 60, 0, 0);
// Return Local Time.
Result := LT;
end;
End Listing Two


<< Back to main page