2010年8月16日

任意の年の夏時間の実施状況を取得する(Windows Vista SP1以降)

アプリケーションの国際化対応(i18n)に必要なことはいろいろあります。画面、リソース文字列の翻訳と実行時ロケールによる切り替え(これらはRAD Studio/VCLの機能で対応できます)、通貨記号や小数点などの形式(これらはSysUtilsのグローバル変数から取得できます)、文字コードの問題(これはDelphi 2009以降のUnicode対応で一応の解決が得られました)、そして夏時間(DST、daylight saving time)への対応があります。ところが従来Windowsでは夏時間に関する情報をその年の分の情報しか持っておらず、GetTimeZoneInformation (ja)を使用しても十分な結果は得られませんでした。たとえば今年ではない特定の日付に夏時間が実施されていたのか、とか、UTCベースで与えられた今年以外の日時が現地時間でどうなるのか、などはWindowsに依存せず自前でロジックの実装と実施状況の保持を行う必要がありました。
さすがに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 件のコメント: