概要
phpでふたつの日時の差分を判定するとき、DateTime::diff()は基本的に不適切である。
不適切さの詳細
- ふたつの日時が「○分以内だったら」「○秒以上だったら」という比較には使えない。
- DateTime::diff()の戻り値であるDateIntervalは、「○年○か月○日○時間○分○秒」というデータの持ち方をしている。
- たとえば「差分が5秒以上か」を判定したいとき、「60秒差」だったら、DateIntervalは「1分0秒(秒はゼロ)」なので、false判定になってしまう。
1. 例
1-1. サンプルコード
$dt1 = new DateTime('2001-01-01 00:00:00');
$dt2 = new DateTime('2002-03-04 05:06:07'); // 1年2か月3日5時間6分7秒後
$diff = $dt1->diff($dt2);
// デバッグ用関数。DateIntervalの指定プロパティを出力する。
function test(DateInterval $di, string $propName, string $comment) {
echo $propName . ' = ' . $di->$propName . ' (' . $comment . ')' . PHP_EOL;
}
test($diff, 'y', '年');
test($diff, 'm', '月。1年2か月後なので14を所望する。');
test($diff, 'd', '日。14か月と3日後なので368を所望する。');
test($diff, 'h', '時。368*24+5=44,160を所望する。');
test($diff, 'i', '分。44160*60+6=2,649,606を所望する。');
test($diff, 's', '秒。2,649,606*60+7=158,976,367を所望する。');
1-1. サンプル結果
y = 1 (年)
m = 2 (月。1年2か月後なので14を所望する。)
d = 3 (日。14か月と3日後なので368を所望する。)
h = 5 (時。368*24+5=44,160を所望する。)
i = 6 (分。44160*60+6=2,649,606を所望する。)
s = 7 (秒。2,649,606*60+7=158,976,367を所望する。)
1-3. サンプル所感
「○年○か月○日○時間○分○秒」という表現を使いたいことがあれば役立つかもしれない。
2. 例外
2-1. 「年数」の評価は、最大単位なのでそのまま使える
たとえば「分」のプロパティmは、59分後なら59だが、60分後は上位単位であるhに吸収され、0になってしまう。
しかし「年」は、上位単位が存在しないので、そのようなことは起こらない。
2-2. 「日数」の評価は、プロパティ「days」でいける
daysというプロパティがある。
これは時間差判定のうえで期待通りの挙動をする。
2-2-1. サンプルコード
// デバッグ用関数。DateIntervalの指定プロパティを出力する。
function test(DateInterval $di, string $propName, string $comment) {
echo $propName . ' = ' . $di->$propName . ' (' . $comment . ')' . PHP_EOL;
}
$dt3 = new DateTime('2001-01-01 12:00:00'); // 基準日時(12時)
$dt4 = new DateTime('2001-01-02 11:59:59'); // 翌日の11:59:59
$dt5 = new DateTime('2001-01-02 12:00:00'); // 翌日の12:00:00
$dt6 = new DateTime('2002-01-01 11:59:59'); // 翌年同日の11:59:59
$dt7 = new DateTime('2002-01-01 12:00:00'); // 翌年同日の12:00:00
$diff3_4 = $dt3->diff($dt4);
$diff3_5 = $dt3->diff($dt5);
$diff3_6 = $dt3->diff($dt6);
$diff3_7 = $dt3->diff($dt7);
test($diff3_4, 'days', '12:00と翌日11:59:59のdays');
test($diff3_5, 'days', '12:00と翌日12:00:00のdays');
test($diff3_6, 'days', '1/1 12:00と翌年同日11:59:59のdays');
test($diff3_7, 'days', '1/1 12:00と翌年同日12:00:00のdays');
2-2-2. サンプル結果
days = 0 (12:00と翌日11:59:59のdays)
days = 1 (12:00と翌日12:00:00のdays)
days = 364 (1/1 12:00と翌年同日11:59:59のdays)
days = 365 (1/1 12:00と翌年同日12:00:00のdays)
サンプル所感
日数の比較は可能。
seconds, minutes, hours, months, の実装を待ちたい。
せめてsecondsだけでも。
ていうかなんでdaysだけなんだよ。ひとつだけならsecondsにしてよ。
3. DateTime::diff()がだめなら、何を使えばいいのか
3-1. 差分そのものが必要なとき
私はDateTime::format('U') (unixtime)の差分を使ってます。
3-1-1. サンプルコード
差分そのものを用いて差分比較をするサンプルです。
function isDiffMoreThanSeconds(string $s1, string $s2, int $sec) {
// 引数をDateTime型に
$d1 = new DateTime($s1);
$d2 = new DateTime($s2);
// それぞれのunixtimeを取得
$u1 = $d1->format('U');
$u2 = $d2->format('U');
// 差分
$diffSecond = $u2 - $u1;
$isMoreThan = $diffSecond >= $sec ? true : false;
echo sprintf("%s, %s, 差は%s秒以上か? → %s\n", $s1, $s2, $sec, ($isMoreThan?'true':'false'));
}
isDiffMoreThanSeconds('2024-01-01 00:00:00', '2024-01-01 00:00:04', 5);
isDiffMoreThanSeconds('2024-01-01 00:00:00', '2024-01-01 00:00:05', 5);
isDiffMoreThanSeconds('2024-01-01 00:00:00', '2024-01-01 23:59:59', 86400); // 1日は86400秒
isDiffMoreThanSeconds('2024-01-01 00:00:00', '2024-01-02 00:00:00', 86400);
3-1-2. サンプル結果
2024-01-01 00:00:00, 2024-01-01 00:00:04, 差は5秒以上か? → false
2024-01-01 00:00:00, 2024-01-01 00:00:05, 差は5秒以上か? → true
2024-01-01 00:00:00, 2024-01-01 23:59:59, 差は86400秒以上か? → false
2024-01-01 00:00:00, 2024-01-02 00:00:00, 差は86400秒以上か? → true
3-1-3. サンプル所感
レガシー感がありますが分かりやすいのでは
3-2. (差分の値そのものは必要ではなく)差が○○以上なら、○○以下なら、という判定をしたいとき
比較したい2つのうちの小さい方に○○(差分)を足したうえで比較する
@oswe99489 さんのコード(DateTime::modify版)をコメントで追記しました
3-2-1. サンプルコード
function isDiffMoreThanSeconds(string $s1, string $s2, int $sec) {
// 引数をDateTime型に
$d1 = new DateTime($s1);
$d2 = new DateTime($s2);
/* --- $d1を$sec秒プラスする --- */
// DateTime::add版
$d1->add(new DateInterval('PT' . $sec . 'S'));
// DateTime::modify版
// $d1->modify("+{$sec} second");
$isMoreThan = ($d1 <= $d2) ? true : false;
echo sprintf("%s, %s, 差は%s秒以上か? → %s\n", $s1, $s2, $sec, ($isMoreThan?'true':'false'));
}
3-2-2. サンプル結果
3-3-1の isDiffMoreThanSeconds を差し替えると同じ結果になるので省略します。
3-2-3. サンプル所感
unixtimeを使わない方法を探したらこうなりました。
けどあまり直感的ではないと思ってます。
「わかりやすさ」だいじ。
慣れの問題かもですが。
4. この記事を書いた理由などポエム
DateTime::diffで日時差分判定するコードが立て続けにプルリクエストに上がってきたから。
あと雑に調べたら、検索上位にそんなサンプルを書いたページがちらほらしたから。
もっとも、公式 https://www.php.net/manual/ja/class.dateinterval.php を見て、そういう用途に使えると期待するのは仕方ないかなと思います。
そして軽い挙動確認では、秒単位でテストするときに分以上の差分などは考慮せず、いける!となってバグが埋め込まれるという。
プロパティがy,m,d,h,i,sと、まるでdate_formatなので、そこで違和感に気づけたのが幸いでした。
DateInterval オブジェクトが保持している情報は、 ある date/time オブジェクトから別の date/time オブジェクトに情報を移す手順です。
とあるので、 DateTime::add
DateTime::sub
に用いる前提のクラスということなのでしょう。きっと。
そのわりには days
プロパティなんて謎なものもありますが。
なんだかんだやっぱり seconds
プロパティを実装してほしいです。