0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DateTime::diff()は時間差判定には(ほぼ)使えない

Last updated at Posted at 2024-10-05

概要

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 プロパティを実装してほしいです。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?