さすがに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 件のコメント:
コメントを投稿