さすがにMicrosoftもこれは駄目だと考えたのか、Windows Server 2008(=Windows Vista SP1)でGetTimeZoneInformationForYearが実装され、これ以降のOSでは指定した年のタイムゾーンの設定情報を取得できるようになりました。GetTimeZoneInformationForYearの第2パラメータ(pdtzi)はタイムゾーンを明示的に指定するときに使用しますが、通常はNULL(現在のタイムゾーン)でしょう。第3パラメータ(ptzi)はTIME_ZONE_INFORMATION構造体で、ここに指定した年と指定したタイムゾーンに関する設定情報が格納されます。
ところがTIME_ZONE_INFORMATION構造体のStandardDate/DaylightDateメンバはSYSTEMTIME構造体であるにもかかわらずSYSTEMTIMEの仕様から外れたデータが格納されることがあるため、解釈は一筋縄では行きません。まずwMonthが0のときは夏時間が無効であることを意味します。次にwYearが0のときは直接的に日付を表さず、wDayOfWeekが曜日でwDayが何番目か(第1○曜日..第5○曜日)という形で表現されます。さらにwDayに5が格納されている場合は第5○曜日ではなく最終○曜日という意味を含んでおり、その月に第5○曜日がなければ第4○曜日になります。さらに時刻部分(wHour/wMinute/wSecond/wMilliseconds)が23:59:59:999になっている場合、実際にはその翌日を意味します。つまり×月第○曜日の翌日、ということです。
さらにGetTimeZoneInformationForYearの指定年の情報を取得できる、という仕様そのものにも問題があります。例えばフランスでは3月最終日曜日に夏時間が始まってその年の10月最終日曜日に終わります。でも南半球だと夏時間は当年中に終わりません。例えばシドニー(オーストラリア東部標準時)の夏時間は10月第1日曜日から次の年の4月第1日曜日までです(2010/08/16現在)。つまりStandardDateとDaylightDateの関係を見て、必要に応じて翌年の情報も取得する必要があるわけです。
ではまずGetTimeZoneInformationForYearに必要な定義を用意します。ですがこの関数はWindows Vista(GOLD)およびそれ以前のOSには存在しません。ということで実行時にGetProcAddress (ja)で動的リンクすることにします。
type
_TIME_DYNAMIC_ZONE_INFORMATION = record
Bias: Longint;
StandardName: array [0..31] of WCHAR;
StandardDate: SYSTEMTIME;
StandardBias: Longint;
DaylightName: array [0..31] of WCHAR;
DaylightDate: SYSTEMTIME;
DaylightBias: Longint;
TimeZoneKeyName: array [0..127] of WCHAR;
DynamicDaylightTimeDisabled: BOOL;
end;
{$EXTERNALSYM _TIME_DYNAMIC_ZONE_INFORMATION}
DYNAMIC_TIME_ZONE_INFORMATION = _TIME_DYNAMIC_ZONE_INFORMATION;
{$EXTERNALSYM DYNAMIC_TIME_ZONE_INFORMATION}
PDYNAMIC_TIME_ZONE_INFORMATION = ^DYNAMIC_TIME_ZONE_INFORMATION;
{$EXTERNALSYM PDYNAMIC_TIME_ZONE_INFORMATION}
TGetTimeZoneInformationForYear = function (wYear: Word;
pdtzi: PDYNAMIC_TIME_ZONE_INFORMATION;
var ptzi: TIME_ZONE_INFORMATION): BOOL; stdcall;
次に上記の面倒な部分の処理です。function CompareSystemTime(ST1: TSystemTime; ST2: TSystemTime): Integer;
begin
Result := ST1.wYear - ST2.wYear;
if Result = 0 then
begin
Result := ST1.wMonth - ST2.wMonth;
if Result = 0 then
begin
Result := ST1.wDay - ST2.wDay;
end;
end;
end;
CompareSystemTimeはTSystemTime型の日付の比較を行います(翌年に繰り越すかどうかの判断で必要になります)。function CanonicalizeSystemTime(Year: Integer; const ST: TSystemTime): TSystemTime;
var
D: TDateTime;
DoW: Integer;
begin
Result := ST;
with Result do
begin
if wYear > 0 then
begin
{ Absolute date }
Exit;
end;
wYear := Year;
{ Get DoW of first date of the month }
D := EncodeDate(wYear,wMonth,1);
DoW := DayOfWeek(D) - 1; // 0 is Sunday
{ Convert to date of the month }
wDay := (wDayOfWeek + (wDay * 7) + 1) - (DoW + Ord(wDayOfWeek >= DoW) * 7);
while wDay > MonthDays[IsLeapYear(Year),wMonth] do
begin
wDay := wDay - 7;
end;
{ Next day }
if (wHour = 23) and (wMinute = 59) then
begin
wHour := 0;
wMinute := 0;
wSecond := 0;
wMilliseconds := 0;
DecodeDate(EncodeDate(wYear,wMonth,wDay) + 1,wYear,wMonth,wDay);
wDayOfWeek := (wDayOfWeek + 1) mod 7;
end;
end;
end;
CanonicalizeSystemTimeはTSystemTimeの相対表現を指定年の絶対表現に変換します。resourcestring
// RFirst = '1st ';
// RSecond = '2nd ';
// RThird = '3rd ';
// RFourth = '4th ';
// RLast = 'last ';
// RNext = '''s next day';
RFirst = '第1';
RSecond = '第2';
RThird = '第3';
RFourth = '第4';
RLast = '最終';
RNext = 'の翌日';
const
WeekOfMonth: array [1..4] of String =
(RFirst, RSecond, RThird, RFourth);
function SystemTimeToDescription(const ST: TSystemTime): String;
begin
if ST.wYear = 0 then
begin
Result := LongMonthNames[ST.wMonth];
case ST.wDay of
1..4:
begin
Result := Result + WeekOfMonth[ST.wDay];
end;
else
begin
Result := Result + RLast;
end;
end;
Result := Result + LongDayNames[ST.wDayOfWeek + 1];
if (ST.wHour = 23) and (ST.wMinute = 59) then
begin
Result := Result + RNext;
end;
end
else
begin
Result := FormatDateTime(LongDateFormat,SystemTimeToDateTime(ST));
end;
end;
SystemTimeToDescriptionはTSystemTimeの相対表現を文字列化します。いよいよ指定年の夏時間の実施状況を取得する関数です。EGetProcAddressはkernel32.dllにGetTimeZoneInformationForYearが存在しないときにraiseされる例外です(Delphi 2010以降では定義済なのでこの定義は不要)。
type
{ EGetProcAddress }
EGetProcAddress = class(Exception)
end;
function GetDSTInfoByYear(Year: Integer;
var Offset: Integer; var Name: String;
var DTStart: TDateTime;
var DescriptionStart: String;
var DTEnd: TDateTime;
var DescriptionEnd: String): Boolean;
var
GetTimeZoneInformationForYear: TGetTimeZoneInformationForYear;
TZI: TIME_ZONE_INFORMATION;
STS: TSystemTime;
STD: TSystemTime;
begin
{ Check GetTimeZoneInformationForYear function }
@GetTimeZoneInformationForYear := GetProcAddress(GetModuleHandle(kernel32),
'GetTimeZoneInformationForYear');
if Assigned(GetTimeZoneInformationForYear) = False then
begin
raise EGetProcAddress.Create('Could not load ' +
'GetTimeZoneInformationForYear' +
' from ' + kernel32);
end;
if (GetTimeZoneInformationForYear(Year,nil,TZI) = False) or
(TZI.StandardDate.wMonth = 0) then
begin
Result := False;
Exit;
end;
{ Offset }
Offset := TZI.DaylightBias;
{ Name }
Name := TZI.DaylightName;
{ Convert to absolute }
STS := CanonicalizeSystemTime(Year,TZI.StandardDate);
STD := CanonicalizeSystemTime(Year,TZI.DaylightDate);
{ Start date }
DTStart := SystemTimeToDateTime(STD);
DescriptionStart := SystemTimeToDescription(TZI.DaylightDate);
if CompareSystemTime(STS,STD) > 0 then
begin
{ Northern hemisphere }
DTEnd := SystemTimeToDateTime(STS);
DescriptionEnd := SystemTimeToDescription((TZI.StandardDate));
end
else
begin
{ Southern hemisphere }
if (GetTimeZoneInformationForYear(Year + 1,nil,TZI) = True) and
(TZI.StandardDate.wMonth > 0) then
begin
{ Convert to absolute }
STS := CanonicalizeSystemTime(Year + 1,TZI.StandardDate);
{ End date }
DTEnd := SystemTimeToDateTime(STS);
DescriptionEnd := SystemTimeToDescription((TZI.StandardDate));
end
else
begin
DTEnd := 0;
DescriptionEnd := '';
end;
end;
Result := True;
end;
GetTimeZoneInformationForYearのアドレスを取得して(取得できない場合は例外が送出されます)呼び出し、取得した情報の解釈を行い(必要なら翌年の情報も取得します)、正常に終了したらTrueを返します。情報の取得に失敗したり、指定した年に夏時間が実施されない場合はFalseを返します。ただしMicrosoftは原則として半年に1回しか夏時間に関する更新情報を配信しないため、南半球でその年(の下半期)に始まる夏時間の情報がない(前年から継続している夏時間の情報しかない)と、その年の01/01から前年に始まった夏時間の終了までの情報が重複して取得されます。夏時間の情報は重複しないと思っていると落とし穴にはまるかもしれませんので注意が必要です。
0 件のコメント:
コメントを投稿