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 (a != null) {
//   :
  if (b > 0) {
//     :
    if (check(c)) {
//       :
      result = 'success';
    } else {
      result = 'c is failed';
    }
  } else {
    result = 'b <= 0';
  }
} else {
  result = 'a is null';
}
return result;

ネストが深くて読み難いですよね。

ガード節を使ってネストを浅くする場合

ネストを浅くする手法としては、ガード節が紹介されているのをよく目にします。

if (a == null) {
  return 'a is null';
}
// :
if (b <= 0) {
  return 'b <= 0';
}
// :
if (!check(c)) {
  return 'c is failed';
}
// :
return 'success';

これで、ネストを浅くすることができました。

しかし、その代わりにreturnを処理の途中で何回も書いています。途中returnを使って分岐構造を壊してしまうことで、分岐自体を見落としやすくなるといったデメリットもあります。

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

ガード節に代えて、以下のように書くのはどうでしょうか。

i = 0;
proceed = a != null;
if (proceed) {
  i++;
//   :
  proceed = b > 0;
}
if (proceed) {
  i++;
//   :
  proceed = check(c);
}
if (proceed) {
  i++;
//   :
}
result = [
  'a is null',
  'b <= 0',
  'c is failed',
  'success',
];
return result[i];

用意するのは、処理がどこまで進んだかを覚えておくための変数iと、次の処理に進めるかを判定するための変数proceedです。

一度判定がfalseになると、それ以降のifブロックはすべてスキップされるので、何回もreturnを書く必要がありません。また、処理の結果がresultにまとめられるところもポイントです。

途中returnを使わずにネストを浅くするテクニック、是非皆さんも試してみてください。

参考

プログラムの可読性を上げるための条件分岐を減らす方法7個 _ えんじにゃーブログ

https://eel3.hatenablog.com/entry/20121225/1356443485

http://tommy-dsp.hatenablog.com/entry/2017/10/31/000745

http://takmiy.hatenablog.com/entry/2020/02/12/104346

https://anopara.net/2014/06/27/do-not-write-deep-nested-code/

関連記事