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

# 概要

phpでふたつの日時の差分を判定するとき、DateTime::diff()は基本的に不適切である。

# 不適切さの詳細

- ふたつの日時が「○分以内だったら」「○秒以上だったら」という比較には使えない。
- DateTime::diff()の戻り値であるDateIntervalは、「○年○か月○日○時間○分○秒」というデータの持ち方をしている。
- たとえば「差分が5秒以上か」を判定したいとき、「60秒差」だったら、DateIntervalは「1分0秒（秒はゼロ）」なので、false判定になってしまう。

# 1. 例

## 1-1. サンプルコード

```php
$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. サンプルコード

```php
// デバッグ用関数。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. サンプルコード

差分そのものを用いて差分比較をするサンプルです。

```php
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. サンプルコード

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