カレンダー生成ロジック
HTMLのマークアップをロジックに組み込んでしまうと可読性が大きく下がるので、カレンダーを 2次元配列 として表現することにします。また、 2038年問題 に対応するために 日付・時刻関数 の代わりに DateTime クラスを用いることにします。
手続き型コーディング
誰もが理解できる最もオーソドックスな方法です。たぶん。
パラメータ(必要に応じてtry~catchブロックを設けます)
$date = new DateTimeImmutable('指定年-指定月-1 00:00:00');
$day = (int)$date->format('w'); // 曜日オフセット(※生成過程で書き換えられます)
$week = 0; // 週オフセット(※生成過程で書き換えられます)
$max = (int)$date->format('t'); // 合計日数
2次元配列の生成
for ($i = $day; $i > 0; --$i) {
// 1日のオフセット分空白セルを作る
$rows[$week][] = '';
}
for ($i = 1; $i <= $max; ++$i) {
// 土曜日をオーバーして右にはみ出すかどうかチェック
if ($day > 6) {
++$week; // 次の週に移動
$day = 0; // 日曜日にリセット
}
$rows[$week][] = $i; // 当該日のセルを作る
++$day; // 次の曜日に移動
}
for ($i = $day; $i <= 6; $i++) {
// 最終日より後も土曜日まで空白セルで埋める
$rows[$week][] = '';
}
関数型コーディング
パラメータ(必要に応じてtry~catchブロックを設けます)
$date = new DateTimeImmutable('指定年-指定月-1 00:00:00');
$max = (int)$date->format('t'); // 合計日数
$before = (int)$date->format('w'); // 曜日オフセット(前)
$after = (7 - ($before + $max) % 7) % 7; // 曜日オフセット(後)
著者案
2次元配列の生成
$rows = array_chunk(array_merge(
array_fill(0, $before, ''),
range(1, $max),
array_fill(0, $after, '')
), 7);
※ PHP5.5以前では array_fill 関数で0個を埋めることはできません。0かどうかの分岐が必要です。
@naga3 さん案
(コメント欄参照)
2次元配列の生成
$rows = array_chunk(array_map(
function ($i) use ($max) { return $i < 1 || $i > $max ? '' : $i; },
range(1 - $before, $max + $after)
), 7);
カレンダー生成スクリプトの実装
上記で紹介した関数型ロジックを用いて、実際にカレンダーを生成してみます。
style.css
.sun {
color: red;
}
.sat {
color: blue;
}
.today {
background: antiquewhite;
}
body {
background: #FBDADE;
}
table,tr,th,td {
border: solid 1px black;
margin-left: auto;
margin-right: auto;
background: white;
border: solid 1px silver;
border-collapse: collapse;
}
th {
text-align: center;
}
td {
text-align: right;
}
a {
text-decoration: none;
}
a:link,a:visited,a:active {
color: pink;
}
a:hover {
color: hotpink;
}
calendar.php
<?php
/* $_GETを展開 */
$py = filter_input(INPUT_GET, 'y');
$pm = filter_input(INPUT_GET, 'm');
/* 3ヵ月分のタイムスタンプを生成する */
try {
$dt = new DateTimeImmutable("$py-$pm-1 00:00:00");
$dt_prev = $dt->sub(new DateInterval('P1M'));
$dt_next = $dt->add(new DateInterval('P1M'));
} catch (Exception $e) {
// 失敗したときは今月を基準にする
$dt = new DateTimeImmutable('first day of this month 00:00:00');
$dt_prev = $dt->sub(new DateInterval('P1M'));
$dt_next = $dt->add(new DateInterval('P1M'));
}
/* リンク・タイトル・フォーム再表示用 */
$py = $dt->format('Y'); // これを行わない場合はXSS対策が別途必要
$pm = $dt->format('n'); // これを行わない場合はXSS対策が別途必要
$current = $dt->format('Y年n月');
$prev = $dt_prev->format('?\y=Y&\a\mp;\m=n');
$next = $dt_next->format('?\y=Y&\a\mp;\m=n');
/* カレンダー生成用パラメータ */
$max = (int)$dt->format('t'); // 合計日数
$before = (int)$dt->format('w'); // 曜日オフセット(前)
$after = (7 - ($before + $max) % 7) % 7; // 曜日オフセット(後)
$today = (int)(new DateTime)->format('d'); // 今日
/* 今日をハイライトするかどうか */
$hl = !$dt->diff(new DateTime('first day of this month 00:00:00'))->days;
/* カレンダー生成ロジック */
$rows = array_chunk(array_merge(
array_fill(0, $before, ''),
range(1, $max),
array_fill(0, $after, '')
), 7);
?>
<!DOCTYPE html>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="style.css">
<title>カレンダー生成</title>
<h1>カレンダー生成</h1>
<p>
<fieldset>
<legend>設定</legend>
<form action="" method="get">
<label><input type="text" name="y" value="<?=$py?>" size="4">年</label>
<label><input type="text" name="m" value="<?=$pm?>" size="2">月</label>
<label><input type="submit" value="生成"></label>
</form>
</fieldset>
</p>
<p>
<table>
<tr>
<th><a href="<?=$prev?>">←</a></th>
<th colspan="5"><h2><?=$current?></h2></th>
<th><a href="<?=$next?>">→</a></th>
</tr>
<tr>
<th class="sun">日</th>
<th>月</th>
<th>火</th>
<th>水</th>
<th>木</th>
<th>金</th>
<th class="sat">土</th>
</tr>
<?php foreach ($rows as $row): ?>
<tr>
<?php foreach ($row as $cell): ?>
<?php if ($hl && $cell === $today): ?>
<td class="today"><?=$cell?></td>
<?php else: ?>
<td><?=$cell?></td>
<?php endif; ?>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</table>
</p>