PHPでカレンダーを実装する

カレンダー

この記事では、PHPでカレンダーを作成する方法について解説していきます。

以下のデモページで、実際に動作するカレンダーが見られます。
現在の月を表示し、月を選択して移動することができるシンプルなカレンダーです。

https://calendar-404206.an.r.appspot.com/

ソースコードは以下のようになっています。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>カレンダー</title>
</head>
<?php
$timestamp = filter_input( INPUT_GET, 'month', FILTER_CALLBACK, [ 'options' => 'strtotime' ] ) ?: strtotime( 'first day of this month' );
?>
<body>
    <h1>カレンダー</h1>

    <div class="calendar">
        <div class="nav">
            <form action="/">
                <input type="month" name="month" value="<?php echo date( 'Y-m', $timestamp ); ?>">
                <button>移動</button>
            </form>
        </div>
        <table>
            <thead>
                <tr>
                    <th>日</th>
                    <th>月</th>
                    <th>火</th>
                    <th>水</th>
                    <th>木</th>
                    <th>金</th>
                    <th>土</th>
                </tr>
            </thead>
            <tbody>
                <?php
                    $lastday = date( 't', $timestamp );
                    $sunday = 1 - date( 'w', $timestamp );
                    while ( $sunday <= $lastday ) {
                        echo '<tr>';
                        $day = $sunday;
                        $sunday += 7;
                        while ( $day < $sunday ) {
                            echo '<td>';
                            if ( $day >= 1 && $day <= $lastday ) {
                                echo $day;
                            }
                            echo '</td>';
                            $day++;
                        }
                        echo '</tr>', PHP_EOL;
                    }
                ?>
            </tbody>
        </table>
    </div>
</body>
</html>

では、ソースコードの各部分について詳しく見ていきましょう。
まず、HTMLのヘッド部分では、文字コードやビューポート、タイトルなどを指定しています。
これは通常のHTMLページと同じです。

次に、PHPのコードが始まります。
ここでは、GETパラメータで指定された月のタイムスタンプを取得します。
GETパラメータで指定された月のタイムスタンプがない場合は、今月のタイムスタンプをデフォルト値として使用します。
これを$timestamp変数に代入します。

次に、HTMLのボディ部分に移ります。
ここでは、見出しとして<h1>カレンダー</h1>を表示します。

次に、カレンダーのメイン部分を<div class="calendar">で囲みます。
この中には、ナビゲーション部分とテーブル部分があります。

ナビゲーション部分は<div class="nav">で囲みます。
ここでは、フォームを使って月を選択できるようにします。
フォームの中には、<input type="month">というタイプの入力欄と、<button>移動</button>というボタンがあります。
入力欄のname属性はmonthにし、value属性には現在の月をdate( 'Y-m', $timestamp )で表示します。
これにより、ユーザーが月を選択して移動ボタンを押すと、GETパラメータで月が送信されます。

テーブル部分は<table>で作ります。
テーブルの中には、見出し行と本体行があります。

見出し行は<thead>で囲みます。
ここでは、日曜日から土曜日までの曜日を<th>で表示します。

本体行は<tbody>で囲みます。
ここでは、PHPのコードを使ってカレンダーの日付を生成します。

まず、指定された月の最終日をdate( 't', $timestamp )で取得し、$lastday変数に代入します。
次に、指定された月の最初の日曜日の日付を計算します。
これは、1から指定された月の1日の曜日番号(0が日曜日)を引くことで求められます。
これを$sunday変数に代入します。

次に、whileループを使ってカレンダーの行を生成します。
ループの条件は、日曜日の日付が最終日以下であることです。
ループ内では、まず<tr>で行を開始します。
次に、$day変数に日曜日の値を代入し、その値から7を足して次の日曜日の値を計算し、それを再び$sunday変数に代入します。
これにより、次回のループでは次の週の行が生成されます。

次に、whileループを使ってカレンダーの列(セル)を生成します。
ループの条件は、現在の日が次の日曜日未満であることです。
ループ内では、まず<td>でセルを開始します。
次に、現在の日が1以上かつ最終日以下であるかどうか判定します。
これは、指定された月以外の日付(前月や翌月)を表示しないためです。
もし条件が真ならば、現在の日をセル内に表示します。
最後に、セルを閉じるために</td>を出力し、現在の日に1を足して更新します。

これで一週間分(7列分)のセルが生成されました。
最後に、行を閉じるために</tr>と改行を出力します。

これでカレンダーの本体部分が完成しました。
最後に、HTMLの閉じタグを出力してページを終了します。

以上が、PHPでカレンダーを作成する方法の解説でした。
このカレンダーはシンプルですが、必要な機能は備えています。
もちろん、デザインや機能は自由にカスタマイズできます。
ぜひ、参考にしてみてください。

早期リターン嫌いな男 あの手この手で Bing AI を説得するが…AIのキレキレな返答に「一理ある」

chat

どんな質問にもチャットで答えてくれる Bing AI に、普段人には言えない、早期リターンに対する想いをぶつけてみました。


早期リターン嫌いな男 PHP で、受け取った数値の年がうるう年か判定するプログラムを書いて。

Bing こんにちは、これはBingです。PHPでうるう年を判定するプログラムを書く方法についてお探しですね。☺
ウェブ検索の結果²³⁴⁵によると、PHPにはdateという関数があり、その中に'L'というオプションがあります。このオプションは、指定した年がうるう年なら1を、そうでなければ0を返します。¹
例えば、次のようなコードで1998年がうるう年かどうか判定できます。

function isLeap ($check_year) {
  return (date ('L', mktime (0, 0, 0, 1, 1, $check_year))==1);
}
$check_year=1998;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは¹から引用しました。
この回答はお役に立ちましたか?
ソース: Bing との会話 2023/3/8(1) PHP Program to Check Leap Year – Letsfindcourse. https://letsfindcourse.com/php-coding-questions/php-program-to-check-leap-year アクセス日時 2023/3/8.
(2) PHP program to check leap year – W3schools. https://www.w3schools.blog/php-program-to-check-leap-year アクセス日時 2023/3/8.
(3) PHP Leap Year Program – javatpoint. https://www.javatpoint.com/php-leap-year アクセス日時 2023/3/8.
(4) PHP program to check if a year is leap year or not …. https://www.tutorialspoint.com/php-program-to-check-if-a-year-is-leap-year-or-not アクセス日時 2023/3/8.
(5) PHP Program to Check Leap Year – Letsfindcourse. https://letsfindcourse.com/php-coding-questions/php-program-to-check-leap-year アクセス日時 2023/3/8.

 はい、そんなオプションは知りませんでした。

Bing よかったです。☺
PHPには他にも便利な関数がたくさんあります。もし興味があれば、PHPの公式ドキュメントをご覧ください。

 では、そういった既存の関数やオプションを使わずにプログラミングできますか?

Bing はい、もちろんできます。date関数やオプションを使わないでうるう年を判定するには、数学的なロジックを使って年が4で割り切れるかどうかをチェックします。¹
ただし、これだけでは十分ではありません。グレゴリオ暦では、4で割り切れる年でも100で割り切れる年はうるう年ではありません。ただし、400で割り切れる年は例外としてうるう年になります。¹
例えば、次のようなコードで2000年がうるう年かどうか判定できます。

function isLeap ($check_year) {
  if ($check_year % 4 == 0) {
    if ($check_year % 100 == 0) {
      if ($check_year % 400 == 0) {
        return true;
      } else {
        return false;
      }
    } else {
      return true;
    }
  } else {
    return false;
  }
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは³から参考にしました。
この回答はお役に立ちましたか?
ソース: Bing との会話 2023/3/8(1) PHP – How to check whether a year is bisect (i.e. a leap year)?. https://stackoverflow.com/questions/5680115/php-how-to-check-whether-a-year-is-bisect-i-e-a-leap-year アクセス日時 2023/3/8.
(2) [ PHP ] 閏(うるう)年の判定を行う3通りの実装方法 – 偏差値40 …. https://hensa40.cutegirl.jp/archives/2817 アクセス日時 2023/3/8.
(3) Does php date function automatically factor in leap year. https://stackoverflow.com/questions/21033197/does-php-date-function-automatically-factor-in-leap-year アクセス日時 2023/3/8.

 それだとネストが深いので、リファクタリングしてください。

Bing ネストが深いと読みにくくなるので、リファクタリングするのは良い考えです。☺
次のようにコードを書き換えると、ネストを減らすことができます。

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは、早期リターンというテクニックを使っています。
この回答はお役に立ちましたか?

 早期リターンは構造化プログラミングの原則に反するため、可読性が低いと思います。

Bing 構造化プログラミングの原則についてお話しすると、関数やメソッドは一つの入り口と一つの出口を持つべきだという考え方です。
しかし、この原則は必ずしも絶対的なものではありません。場合によっては、早期リターンを使った方が可読性が高くなることもあります。
例えば、次のようなコードは、早期リターンを使わないで書かれています。

function isLeap ($check_year) {
  $result = false;
  if ($check_year % 4 == 0) {
    $result = true;
    if ($check_year % 100 == 0) {
      $result = false;
      if ($check_year % 400 == 0) {
        $result = true;
      }
    }
  }
  return $result;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは、ネストが深くなり、変数$resultの値が何度も変わるため、読みにくいと感じる人もいるかもしれません。
一方、早期リターンを使ったコードは、

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは、ネストが浅くなり、条件に合わない場合はすぐに終了するため、読みやすいと感じる人もいるかもしれません。
どちらのコードが良いかは、個人やチームの好みや規約によって異なります。
私はどちらのコードでも理解できますが、早期リターンを使った方がシンプルでエレガントだと思います。☺
この回答はお役に立ちましたか?

 こうやって書けばいいんじゃないですか?

function isLeap ($check_year) {
  $result = false;
  if ($check_year % 4 == 0) {
    $result = true;
  }
  if ($check_year % 100 == 0) {
    $result = false;
  }
  if ($check_year % 400 == 0) {
    $result = true;
  }
  return $result;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

Bing はい、そのコードでもうるう年の判定はできます。☺
ただし、このコードでは、条件に合致するたびに$resultの値が上書きされるため、無駄な処理が発生します。
例えば、$check_yearが4で割り切れる場合、$resulttrueになりますが、その後100で割り切れるかどうかをチェックします。もし100で割り切れる場合、$resultfalseになりますが、その後400で割り切れるかどうかをチェックします。もし400で割り切れる場合、$resultは再びtrueになります。
このように$resultの値が何度も変わると、コードの流れを追いにくくなったり、バグの原因になったりする可能性があります。
早期リターンを使ったコードでは、

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードでは、条件に合わない場合はすぐに関数から抜けるため、無駄な処理が発生しません。
例えば、$check_yearが4で割り切れない場合、return falseという文で関数を終了します。その後の処理は実行されません。
このように早期リターンを使った方が効率的で可読性も高いと思います。☺
この回答はお役に立ちましたか?

 確かに一理ありますね。
それでは、次のコードならどうですか?

function isLeap ($check_year) {
    if ($check_year % 400 == 0) {
        $result = true;
    } elseif ($check_year % 100 == 0) {
        $result = false;
    } elseif ($check_year % 4 == 0) {
        $result = true;
    } else {
        $result = false;
    }
    return $result;
}
$check_year=2000;
if (isLeap ($check_year)) {
    echo "$check_year is a Leap Year\n";
} else {
    echo "$check_year is not a Leap Year\n";
}

Bing このコードでもうるう年の判定はできます。☺
このコードでは、elseifを使って条件を分岐させています。これにより、ネストが浅くなり、$resultの値も一度しか変わりません。
しかし、このコードには少し問題があります。
それは、条件の順番です。
このコードでは、まず$check_yearが400で割り切れるかどうかをチェックしています。もし割り切れる場合、$resulttrueになります。
しかし、400で割り切れる年は必ず4で割り切れる年でもあります。つまり、最初の条件と最後の条件が重複しています。
これは無駄な処理であり、可読性も低くなります。
早期リターンを使ったコードでは、

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードでは、まず$check_yearが4で割り切れるかどうかをチェックしています。もし割り切れない場合、return falseという文で関数を終了します。
次に$check_yearが100で割り切れるかどうかをチェックしています。もし割り切れない場合、return trueという文で関数を終了します。
最後に$check_yearが400で割り切れるかどうかをチェックしています。もし割り切れる場合、return trueという文で関数を終了します。
このように早期リターンを使った方が条件の順番が自然でわかりやすいと思います。☺
この回答はお役に立ちましたか?

 次に示すコードは、早期リターンを使ったコードと条件の順番が一緒です。
どちらの方が、可読性が高いと思いますか?

function isLeap ($check_year) {
    if ($check_year % 4 != 0) {
        $result = false;
    } elseif ($check_year % 100 != 0) {
        $result = true;
    } elseif ($check_year % 400 != 0) {
        $result = false;
    } else {
        $result = true;
    }
    return $result;
}
$check_year=2000;
if (isLeap ($check_year)) {
    echo "$check_year is a Leap Year\n";
} else {
    echo "$check_year is not a Leap Year\n";
}

Bing このコードでもうるう年の判定はできます。☺
このコードでは、条件の順番が早期リターンを使ったコードと一緒です。これにより、条件の重複を避けることができます。
しかし、このコードにはまだ改善の余地があります。
それは、else文です。
このコードでは、最後のelse文で$resulttrueにしています。しかし、これは必要ありません。
なぜなら、最後のelse文に入るということは、$check_yearが4で割り切れてかつ100で割り切れてかつ400で割り切れるということです。
つまり、$check_yearが400で割り切れる場合と同じです。
その場合、$resulttrueにする必要はありません。return trueという文で関数を終了すれば十分です。
早期リターンを使ったコードでは、

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードでは、最後のreturn falseという文は必ずしも必要ではありません。なぜなら、この関数に入力される値は整数だからです。
もし小数や文字列などが入力された場合はどうなるでしょうか?
その場合はエラーが発生する可能性があります。そのため、入力値の型をチェックする処理を追加することも考えられます。
例えば、

function isLeap ($check_year) {
  
// 入力値の型チェック
if (!is_int($check_year)) {
    return "Invalid input";
}

// 入力値の範囲チェック
if ($check_year < -4713 || $check_year == -1 || $check_year == -0) {
    return "Invalid year";
}

// 西暦1年以前の場合
if ($check_year < 1) {

// ユリウス暦における閏年判定
    if ($check_year % -4 == -1) {
        return true;
    } else {
        return false;
    }

// 西暦1年以降の場合
} else {

// グレゴリオ暦における閏年判定
    if ($check_year % 4 != 0) {
        return false;
    }
    if ($check_year % 100 != 0) {
        return true;
    }
    if ($check_year % 400 == 0) {
        return true;
    }
    
}

return false;

}
$test_cases = array(-4713,-4712,-45,-44,-1,-0,0,1,2,3,4,5,99,100,101,
                    ...,
                    ...,
                    ...,
                    ...,
                    ...);
foreach($test_cases as $case){
   $result = isLeap($case);
   echo "$case: ";
   var_dump($result);
   echo "\n";
}

このように早期リターンを使った方がエラーハンドリングや特殊ケースの処理も簡単になります。☺
この回答はお役に立ちましたか?


以上で8回までの会話の上限に達してしまいました。実際にエンジニアとチャットしているような感覚でしたし、最後の方は Bing もアツくなって暴走しているみたいで面白かったです☻

あなたはどんな書き方が好きですか?

関連記事

ガード節より読みやすい!?途中リターンせずにネストを浅くする「ガードレール構文」の紹介

プログラムで値のチェック処理を書くとき、普通にif文を使っているとネストが深くなっていってしまいます。

function alertOverweight(user) {
  let result, height, weight;

  // 身長と体重が数値であることを確認
  if (!(isNaN(user.height) || isNaN(user.weight))) {
    height = user.height / 100; // 身長を取得
    weight = user.weight; // 体重を取得

    // 身長が0より大きいことを確認
    if (height > 0) {
      user.bmi = weight / height ** 2; // BMIを計算

      // BMIが25以上であることを確認
      if (user.bmi >= 25) {
        alert('体重を減らしましょう。');

        result = '過体重でした。';
      } else {
        result = 'BMIが25未満のため、過体重ではありませんでした。';
      }
    } else {
      result = '身長が0以下のため、BMIが計算できませんでした。';
    }
  } else {
    result = '身長もしくは体重が数値でないため、BMIが計算できませんでした。';
  }
  return result;
}

このようにネストが深くなると、コードの可読性が低下してしまいます。

ガード節のデメリット

ガード節は、このようなネストを解消するための手法として広く使われています。

function alertOverweight(user) {
  let height, weight;

  // 身長もしくは体重が数値でない場合は結果を返す
  if (isNaN(user.height) || isNaN(user.weight)) {
    return '身長もしくは体重が数値でないため、BMIが計算できませんでした。';
  }
  height = user.height / 100; // 身長を取得
  weight = user.weight; // 体重を取得

  // 身長が0以下の場合は結果を返す
  if (height <= 0) {
    return '身長が0以下のため、BMIが計算できませんでした。';
  }
  user.bmi = weight / height ** 2; // BMIを計算

  // BMIが25未満の場合は結果を返す
  if (user.bmi < 25) {
    return 'BMIが25未満のため、過体重ではありませんでした。';
  }
  alert('体重を減らしましょう。');

  return '過体重でした。';
}

このように途中リターンを使って、チェック処理で関数から抜け出すことでネストを無くしているのです。これは一見するとコードがフラットになりますが、構造化プログラミングの観点ではあまり好ましくありません。例えば、この関数は最終的に「過体重でした。」という結果を返していますが、様々な条件を満たした結果であることが分かり難いといった問題が生じます。

途中リターンしなくてもネストは浅くできる

この問題を解決するために生まれたのが、今回紹介するガードレール構文です。先ほどのコードを、ガードレール構文を使って書き直すと以下のようになります。

function alertOverweight(user) {
  let result, proceed, height, weight;
  
  result = '身長もしくは体重が数値でないため、BMIが計算できませんでした。';
  proceed = !(isNaN(user.height) || isNaN(user.weight)); // 身長と体重が数値であることを確認
  if (proceed) {
    height = user.height / 100; // 身長を取得
    weight = user.weight; // 体重を取得

    result = '身長が0以下のため、BMIが計算できませんでした。';
    proceed = height > 0; // 身長が0より大きいことを確認
  } if (proceed) {
    user.bmi = weight / height ** 2; // BMIを計算

    result = 'BMIが25未満のため、過体重ではありませんでした。';
    proceed = user.bmi >= 25; // BMIが25以上であることを確認
  } if (proceed) {
    alert('体重を減らしましょう。');

    result = '過体重でした。';
  }
  return result;
}

このコードでは、result変数とproceed変数を段階的に更新することで、チェック処理を進めていきます。一度条件を外れると、それ以降のif文はすべてスルーして、最後に設定された結果を返す仕組みです。

if文が処理の流れに沿って直線状に連なっている様子から、これを「ガードレール構文」と呼ぶことにしました。

これならネストを減らしつつ、2つの変数から一連のチェック処理を芋づる式に辿っていくこともできるため、コードの見通しがとても良くなります。

まとめ

途中リターンは再帰関数で使われることもありますが、通常の制御構造(if文やfor文等)で実装できる場合は、極力使わないようにしましょう。

以上、途中リターンせずにネストを浅くする「ガードレール構文」の紹介でした。是非皆さんもガード節の代わりに使ってみてください。

参考記事

https://memorandumrail.com/program-conditional-branch

https://marycore.jp/coding/why-goto-statement-is-bad

https://www.ninton.co.jp/archives/8065

関連記事