CodeZine(コードジン)

特集ページ一覧

PHPにおける日付と時刻の混乱

PEARライブラリ活用 (1)

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2007/11/06 14:00

目次

PEAR Dateの問題点

 PEAR Dateには2つの問題があります。それらを順に見ていきましょう。

Data_Spanの仕様

 任意の2つの日時の間の長さを計算できることがわかったので、私の祖母が生まれてから今日で何日になるかを調べてみます。

$now=new Date(); 
$birthday=new Date('1923-03-08T00:00:00+09:00'); // 祖母の誕生日 

$span=new Date_Span(); 
if(!$span->setFromDateDiff($now,$birthday)) echo '失敗'; 
                                                   // 失敗してはいない

echo $span->toDays(); // -18807.6105324 (間違い)

 期間が負になっているので、明らかに間違っているはずなのですが、エラーは発生していません。「負になったら間違い」なのかと思ってもう少し調べると、そうでなくても間違う例がすぐ見つかります。

$date1=new Date('2000-01-01T00:00:00+00:00'); 
$date2=new Date('1700-01-01T00:00:00+00:00'); 

$span->setFromDateDiff($date1,$date2); 
echo $span->toDays(); // 10151.4607407

 約300年なので、11万日くらいになるはずなのですが、1万日にしかなっていません。

 原因は実は、期間の処理に内部的にはintを使っていることです。次のように確認できます。

$span=new Date_Span(2147483647); // intの最大値  
echo $span->toDays(); // 24855.1348032日に相当する

$date2=new Date($date1); // date1をコピーする  
$date2->addSpan($span); // 期間を追加 
echo $date2->getDate(); // 2068-01-19 03:14:07 

$date2->addSeconds(1); // さらに1秒加える 
echo $date2->getDate(); // 2068-01-19 03:14:08 (正しい)
$span->setFromDateDiff($date1,$date2); // 期間を調べる 
echo $span->toDays(); // -24856.1771759 (間違い)

 2147483647秒(約68年)までは正しく処理できていますが、それより1秒でも長くなると間違えます。

暦の扱い

 PEAR Dateのもう1つの問題は、暦の扱いにあります。

 現在最も広く使われている暦(グレゴリオ暦)では、「西暦が100の倍数のときは400で割れるとき、100の倍数でないときは4の倍数のとき」が閏年になります。一方、ユリウス暦では「西暦が4の倍数のとき」が閏年です。グレゴリオ暦は1582年10月15日にイタリアで採用されました。切り替えの際に春分の日が3月21日になる形で採用したため、ユリウス暦の1582年10月4日の翌日がグレゴリオ暦の1582年10月15日ということになりました。

 グレゴリオ暦の採用時期は国によってさまざまで、イギリスではユリウス暦の1752年9月2日の翌日がグレゴリオ暦のは1752年9月14日です(UNIXのコマンドcalはこれを採用していますが、「cal -j 9 1752」とすれば分かるように完璧ではありません)。日本はもう少し複雑で、明治5年12月2日の翌日が明治6年1月1日なのですが、グレゴリオ暦における閏年の決め方が導入されたのは明治31年です(この勅令は電子政府の総合窓口で「閏年」を検索すると見つかります)。

神武天皇即位紀元年数ノ四ヲ以テ整除シ得ヘキ年ヲ閏年トス但シ紀元年数ヨリ六百六十ヲ減シテ百ヲ以テ整除シ得ヘキモノノ中更ニ四ヲ以テ商ヲ整除シ得サル年ハ平年トス(明治三十一年五月十一日勅令第九十号)

 このように、暦の切り替わった時期は国によってさまざまなのですが、このパッケージが対応しているのは1582年に切り替わった場合のみです(本来はロケールによって変わるべきでしょう。ロケールは時間によって変わるはずなので、鶏と卵になりそうですが)。

 1582年以降はグレゴリオ暦になっているので、例えば1700年は閏年ではありません(ユリウス暦では閏年です)。

echo (Date_Calc::isLeapYear(1700) ? '閏年' : '閏年ではない'); 
// 閏年ではない (正しい)

 ここまではいいのですが、1000年より前に閏年が無くなるというのは乱暴です(閏年かどうかを返すメソッドisLeapYear()ソースを読むと分かります)。

echo (Date_Calc::isLeapYear(800) ? '閏年' : '閏年ではない'); 
// 閏年ではない (間違い)

 1000年以降についてはユリウス暦とグレゴリオ暦を切り替えられている印象ですが、もう少し詳しく見ると、あまりよくありません。

echo (Date_Calc::isLeapYear(1100) ? '閏年' : '閏年ではない'); 
// 閏年 (正しい)

$date1=new Date('11000228000000'); // 1100年2月28日 
echo $date1->getNextDay()->getDate(); // 翌日は1100-03-01 00:00:00
                                      //(間違い)

 1100年は、isLeapYear()の結果では閏年のはずです。これはユリウス暦のルールにあっています。しかし、実際に2月28日の翌日を調べるてみると3月1日になっていてこれは間違いです。グレゴリオ暦を過去に適用するならあっているとも言えるのですが、その場合はisLeapYear()が間違いということになります。

 もう1つ例を挙げましょう。

echo Date_Calc::getDaysInMonth(10,1582);
// 21 (1582年10月は21日しかなかった。正しい)

$date1=new Date('15821016000000'); // 1582年10月16日 
$date2=$date1->getPrevDay()->getPrevDay(); // 2日さかのぼると、 
echo $date2->getDate(); // 1582-10-14 00:00:00 になる(間違い)

 先に述べたように、1582年10月4日の翌日は10月15日なので、この年の10月は21日しかありません。Date_Calc::getDaysInMonth()はこのことを正しく示しています。しかしながら、1582年10月16日から2日さかのぼっても1582年10月4日にはなりません。グレゴリオ暦を過去にも適用しているのです。

 ちなみに、月の日数を数える関数cal_days_in_month(int $calendar,int $month,int $year)は暦を指定できるので便利なのですが、1582年10月のように、途中で暦が切り替わる月には使えません。

 以上から、グレゴリオ暦が採用された1582年10月15日より前の日時については、表現できるとはいっても正しく処理されるわけではないということが分かります。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

バックナンバー

連載:PEARライブラリ活用

もっと読む

著者プロフィール

  • WINGSプロジェクト 矢吹 太朗(ヤブキ タロウ)

    <WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂...

  • 山田 祥寛(ヤマダ ヨシヒロ)

    静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for ASP/ASP.NET。執筆コミュニティ「WINGSプロジェクト」代表。 主な著書に「入門シリーズ(サーバサイドAjax/XM...

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5