🍑

[PHP]日付のループ処理とアンチパターン

2023/01/14に公開

概要

日付でループ処理を回す方法とアンチパターン(という名の覚書き)

サンプルコード

以下、指定した都市に関し、特定期間(ある日付(11/29)~ある日付(12/01))の処理をするための内容。

<?php

$cities = ['Uva', 'Dimbula', 'Nuwara Eliya'];

$startDate = new Datetime('2022-11-29');
// 開始日の初期値を取得
$startDateOrigin = $startDate;
$endDate = new Datetime('2022-12-01');

foreach ($cities as $city) {
    // 開始日の初期化
    $startDate = clone $startDateOrigin;
    echo "[$city]" . PHP_EOL;
    while ($startDate <= $endDate) {
        echo $startDate->format('Ymd') . PHP_EOL;
        // API叩いたりとか何らかの処理
        $startDate->modify('+1 day');
    }
    echo PHP_EOL;
}

実行結果は以下の通り

実行結果
[Uva]
20221129
20221130
20221201

[Dimbula]
20221129
20221130
20221201

[Nuwara Eliya]
20221129
20221130
20221201

ポイント

  • Datetime型を使用する
  • 開始日の初期化時にcloneすること(Datetime(オブジェクト)は参照型のため)

アンチパターン

その1: 数値型で日付を加算する

日付型→文字列 or 数値型への変換がないので、一見スマートに見えるかもですが、期間が月を跨ぐ場合、もれなく事故ります。。

<?php

$cities = ['Uva', 'Dimbula', 'Nuwara Eliya'];

$startDate = 20221129;
// 開始日の初期値を取得
$startDateOrigin = $startDate;
$endDate = 20221201;

foreach ($cities as $city) {
    // 開始日の初期化
    $startDate = $startDateOrigin;
    echo "[$city]" . PHP_EOL;
    while ($startDate <= $endDate) {
        echo $startDate . PHP_EOL;
        // API叩いたりとか何らかの処理
        $startDate++;
    }
    echo PHP_EOL;
}

実行結果は以下の通り
ありえない年月日がズラズラと。。
テストで月跨ぎパターンを実施しないと処理が通るので、最悪リリースして忘れた頃にエラーが発生する可能性も・・・。

実行結果
[Uva]
20221129
20221130
20221132
20221133
:
:
20221199
20221200
20221201

[Dimbula]
20221129
20221130
20221132
20221133
:
:
20221199
20221200
20221201

[Nuwara Eliya]
20221129
20221130
20221132
20221133
:
:
20221199
20221200
20221201

その2: cloneのやり方ミス

clone元はcloneの変更の影響を受けないけれど、cloneはclone元の変更の影響を受けてしまいます。
cloneのやり方(場所?)をミスすると、想定通り動きません・・!

例えば以下のように、開始日の初期化時ではなく、開始日の初期値の取得時にcloneすると、せっかくのcloneが無効になってしまうため(開始日の初期化:$startDate = $startDateOrigin; で参照渡しに戻ってしまう)、意味をなさなくなってしまいます・・。

<?php

$cities = ['Uva', 'Dimbula', 'Nuwara Eliya'];

$startDate = new Datetime('20221129');
// 開始日の初期値を取得: 
$startDateOrigin = clone $startDate;
$endDate = new Datetime('20221201');

foreach ($cities as $city) {
    // 開始日の初期化
    $startDate = $startDateOrigin;
    echo "[$city]" . PHP_EOL;
    while ($startDate <= $endDate) {
        echo $startDate->format('Ymd') . PHP_EOL;
        // API叩いたりとか何らかの処理
        $startDate->modify('+1 day');
    }
    echo PHP_EOL;
}

実行結果は以下の通り。
$startDateOriginも$startDateに連動してしまうため、2回目以降のループ(while文)が回らなくなります。

実行結果
[Uva]
20221129
20221130
20221201

[Dimbula]

[Nuwara Eliya]

終わりに

見事に沼にハマって修正に時間をとってしまったので覚書きにしました
少なくとも日付のループは日付型で・・!

よりよいやり方などあればぜひ教えてください!

参考

Discussion