日本標準時と 1888 年問題

FreeBSD で mktime() に tm_year が 0 以下の struct tm を渡すと返り値が -1 になるという問題を聞いて,それくらいなら tm_year > 0 が成立するように補正した上で mktime() に渡し,後から補正した分を元に戻してやればいいんじゃないかと思ったのですが,上手く行きませんでしたという話です.

struct tm の tm_year について, Ubuntu 12.04 の manpage では以下のように説明されてます.つまり, 1900 年とそれより前の時間を指定すると失敗することになります.かなり昔になるので,普通に使っていて問題になることは稀だと思います.とはいえ,使わないとは限りませんし,困ったものです.

tm_year   The number of years since 1900.

そういうわけで,この問題を回避する方法を考えてみました.

最初に思い付いたのは tm_year が 1 になるように補正するという案なのですが,閏年や夏時間に関する補正が悩みの種になりました(※1).ちなみに,閏年になるのは西暦が 400 の倍数になるときか, 100 の倍数でない 4 の倍数になるときです(※2).夏時間については tm_isdst で確認できます.

※1 未来の時刻について夏時間の問題を解決する方法はないので,そういうのは考えないものとします.どうしても解決したいのであれば,政治的に夏時間をなくすなり,タイムマシーンを解決するなりしなくちゃ駄目な気がします.

※2 西暦 1 年の前年が紀元前 1 年になって地味に気持ち悪いという罠がありますけど,そういうのは今回は考えないものとします.

ちなみに,夏時間が使われ始めたのは 1916 年かららしく,それより前の年代では気にしなくても大丈夫なようです.

閏年の問題を解決する手段として考えたのが, tm_year に 400 の倍数を足して 0 より大きな値にするという案です.閏年の数は一定になるため,境界条件に頭を悩ませる必要がありません.

それから,夏時間については, tm_year の補正でマシマシにして(※3),夏時間を補正するためのテーブルに引っかからないようにすることを考えました.

※3 tm_year が 401 以上になるように補正します. 401 以上にすれば 2300 年近くになるまでは問題にならないと思ったわけです.補正がテーブル以外の方法でおこなわれていたら駄目ですが….

以上の案にもとづいて考えたのが以下の関数です.

time_t my_mktime(struct tm *tm) {
  if (tm->tm_year > 0) {
    return std::mktime(tm);
  }

  // 閏年の影響を避けるため, 400 年単位でずらします.
  // マシマシするために + 2 としています.
  long tm_year_offset = ((-tm->tm_year / 400) + 2) * 400L;
  tm->tm_year += tm_year_offset;
  std::time_t seconds_since_epoch = std::mktime(tm);
  tm->tm_year -= tm_year_offset;

  // ずらした分を元に戻します.
  // 400 年あたり,閏年(366 日)が 97 年とそれ以外(365 日)が 303 年です.
  seconds_since_epoch -= (tm_year_offset / 400) *
      ((303L * 365 * 24 * 60 * 60) + (97L * 366 * 24 * 60 * 60));
  return seconds_since_epoch;
}

そして,単純に mktime() を呼び出したときの結果を比べてみたところ(※4),一致したり一致しなかったりするという不思議な結果になりました.具体的には,正解より 1139 大きな値を返すことがありました.

※4 Ubuntu 12.04 の mktime() は tm_year <= 0 でも期待通りの結果を返してくれるので,それを正解としました.

一致しないときの誤差は常に 1139 なので意味がある数値だと判断せざるをえないのですが,中途半端な数値なので何が何やらです.「1139 秒」をウェブで検索してみましたが,有用な情報は手に入りませんでした.

仕方がないので,結果が一致しない条件を調べてみると, tm_year < -12 のときだということがわかりました.すなわち, 1888 年より前では一致せず, 1888 年以降では一致します.この情報を使えば,正確に補正できるようになりました.新たな関数は以下のようになります.

time_t my_mktime(struct tm *tm) {
  // ずらした分を元に戻すところまでは省略します.

  // 以下の補正を加えることで,結果が一致するようになりました.
  if (tm->tm_year < -12) {
    seconds_since_epoch -= 1139;
  }

  return seconds_since_epoch;
}

そういうわけで, 1888 年に何かがあったのだろうと思って調べてみたところ, 1888 年 1 月 1 日のできごとに日本標準時実施と書かれていました.

さらに, 1888 年問題でウェブで検索をしてみると,以下の記事が見つかりました.時間の問題というのは,実に奥深いものです.

そういうわけで,タイムゾーンが東京の時刻については,なんとか補正できることがわかりました.一方で,補正方法はタイムゾーンによって違うことが予想されます.将来的にタイムゾーンの変更がおこなわれる可能性も否定できませんし,すべてのタイムゾーンに対処しようとすれば,面倒な問題になりそうです.