Home > PHP > DateTimeクラスの落とし穴と対策 : PHP Advent Calendar jp 2011 Day 7

DateTimeクラスの落とし穴と対策 : PHP Advent Calendar jp 2011 Day 7

  • 2011-12-07 (水) 21:28
  • PHP


PHP Advent Calendar jp 2011 7日目担当の @scriptwork です。

DateTimeクラス は PHP5.3 で日付や時刻の加減算を行う add / subメソッド や 差分を計算する diffメソッド などが追加され、日付と時刻を取り扱う面倒な処理をサポートしてくれるクラスです。

DateTimeクラス が実装されるまでは PHP関数 の strtotime() と date() で日付と時刻の加減算を行なっていましたが、2038年問題 への対応もあり、新しくコードを書くなら DateTimeクラス を使うのが良さそうですね。

とはいえ、現在の PHP5.3.8 と PHP5.4.0RC2 の DateTimeクラス は 日付の加減算 で strtotime() と 同じ問題 をかかえていますので、使い所を押さえておく必要があります。

DateTimeクラスの問題とは何か?

まずは正常なケースとして、日付に対する日数の加減算で DateTimeクラス の動作を確認してみましょう。
// 日数の加算
$day = new DateTime( '2011-2-28' );
echo $day->format( 'Y-m-d' );                         // 2011-02-28
$day->add( new DateInterval('P1D') );                 // 1日後
echo ' +1 day ', $day->format( 'Y-m-d' ), '<br>';     // 2011-03-01

// 日数の減算
$day->setDate( 2011, 3, 1 );
echo $day->format( 'Y-m-d' );                         // 2011-03-01
$day->sub( new DateInterval('P1D') );                 // 1日前
echo ' -1 day ', $day->format( 'Y-m-d' ), '<br>';     // 2011-02-28
1日づつの単純な加減算ですが、カレンダーどうりに 月をまたいだ日付 を返すことに成功しています。
// 週の加算
$day->setDate( 2011, 2, 22 );
echo $day->format( 'Y-m-d' );                         // 2011-02-22
$day->add( new DateInterval('P1W') );                 // 1週間後
echo ' +1 week ', $day->format( 'Y-m-d' ), '<br>';    // 2011-03-01

// 週の減算
$day->setDate( 2011, 3, 7 );
echo $day->format( 'Y-m-d' );                         // 2011-03-07
$day->sub( new DateInterval('P1W') );                 // 1週間前
echo ' -1 week ', $day->format( 'Y-m-d' ), '<br>';    // 2011-02-28
同様に 月をまたぐ週の加減算 も問題ありません。
次に月の加減算です。
// 月の加算
$day->setDate( 2011, 1, 31 );
echo $day->format( 'Y-m-d' );                         // 2011-01-31
$day->add( new DateInterval('P1M') );                 // 1ヶ月後
echo ' +1 month ', $day->format( 'Y-m-d' ), '<br>';   // 2011-03-03

// 月の減算
$day->setDate( 2011, 3, 29 );
echo $day->format( 'Y-m-d' );                         // 2011-03-29
$day->sub( new DateInterval('P1M') );                 // 1ヶ月前
echo ' -1 month ', $day->format( 'Y-m-d' ), '<br>';   // 2011-03-01
おや?
1月31日 の 1ヶ月後 が 3月3日 ?
3月29日 の 1ヶ月前 が 3月1日 ?

本当はどちらも 2月28日 を返してほしいのですが、日付や月数のパラメーターを変えて試してみると、月の加減算 で演算前後の『 月の日数 』が違うと日付が狂ってしまうことがわかりました。

最初は自分の使い方が悪いのかと思ったのですが、 DateTimeクラス の addメソッドsubメソッド それぞれのドキュメントに注意点(例3)が書いてあり、どうも現状は仕様(?)扱いになっているようです。

対策方法

対策として DateTimeクラス を extend しようかとも思いましたが、まずはDRYということで誰かコードを書いていないか探してみると Zend Framework の Zend_Dateクラス を発見。
さっそく、試して見ると…
require_once 'Zend/Date.php';

// 月の加算
$day1 = new Zend_Date( array('year'=>2011, 'month'=>1, 'day'=>31) );
echo $day1->toString('yyyy-MM-dd');                             // 2011-01-31
$day1->add( 1, Zend_Date::MONTH );                              // 1ヶ月後
echo ' +1 month ',  $day1->toString('yyyy-MM-dd'),  '<br>';     // 2011-02-28

// 月の減算
$day2 = new Zend_Date( array('year'=>2011, 'month'=>3, 'day'=>29) );
echo $day2->toString('yyyy-MM-dd');                             // 2011-03-29
$day2->sub( 1, Zend_Date::MONTH );                              // 1ヶ月前
echo ' -1 month ',  $day2->toString('yyyy-MM-dd'),  '<br>';     // 2011-02-28
こんな感じで、Zend_Dateクラス は 月の加減算OKです。他にも日、週、年の加減算も試しましたが問題ありません!

理想としては PHP5.4 で DateTimeクラス が改修されると良いのですが、CakePHP など他のフレームワークを使っていても require_once で使えますので、当面の対策として Zend_Dateクラス お勧めです。

おまけ( クライアントサイドの対策 )

PHP Advent Calendarの記事ではありますが、クライアントサイドということで JavaScript も試したところ、同じ現象を確認しました(汗)。
// ■ 月の加算
var day = new Date( 2011, 0, 31 );
document.writeln( day.toLocaleDateString() + ' +1 month ' );    // 2011年1月31日
day.setMonth( day.getMonth() + 1 );                             // 1ヶ月後
document.writeln( day.toLocaleDateString() + '<br>' );          // 2011年3月3日

// ■ 月の減算
day.setMonth(2); day.setDate(29);
document.writeln( day.toLocaleDateString() + ' -1 month ' );    // 2011年3月29日
day.setMonth( day.getMonth() - 1 );                             // 1ヶ月前
document.writeln( day.toLocaleDateString() + '<br>' );          // 2011年3月1日
JavaScript も 日数の加減算 は OK だけど、月の加減算 が NG です。
※ 週の加減算 は javascript に setWeek() と getWeek() が無いので確認を省略しました

そこで、やはり DRY(笑) ということで流行りの Moment.js を導入。
// 月の加算
var day = moment([2011, 0, 31]); // JavaScriptなので、月は0から
document.writeln( moment(day).format("YYYY-MM-DD") + ' +1 month ' );    // 2011-01-31
day.add('months', 1);                                                   // 1ヶ月後
document.writeln( moment(day).format("YYYY-MM-DD") + '<br>' );          // 2011-02-28

// 月の減算
day = moment([2011, 2, 29]); // JavaScriptなので、月は0から
document.writeln( moment(day).format("YYYY-MM-DD") + ' -1 month ' );    // 2011-03-29
day.subtract('months', 1);                                              // 1ヶ月前
document.writeln( moment(day).format("YYYY-MM-DD") + '<br>' );          // 2011-02-28
1月31日 の 1ヶ月後 は 2月28日
3月29日 の 1ヶ月前 も 2月28日

Moment.js 正解です。

Zend_Date と同様に Moment.js で 日、週、年の加減算も対応していることを確認しましたので、日付操作はこの2つで決まりですね。

それでは、明日の PHP Advent Calendar jp 2011 8日目は @koyhoge さんです。

Home > PHP > DateTimeクラスの落とし穴と対策 : PHP Advent Calendar jp 2011 Day 7

Fedora 20
アーカイブ

Return to page top