最もシンプルなカレンダー生成ロジック

# **カレンダー生成ロジック**

<ins>HTMLのマークアップをロジックに組み込んでしまうと可読性が大きく下がる</ins>ので、カレンダーを **2次元配列** として表現することにします。また、 **[2038年問題](http://ja.wikipedia.org/wiki/2038%E5%B9%B4%E5%95%8F%E9%A1%8C)** に対応するために  **[日付・時刻関数](http://www.php.net/manual/ja/ref.datetime.php)** の代わりに **[DateTime](http://www.php.net/manual/ja/class.datetime.php)** クラスを用いることにします。

## **手続き型コーディング**

誰もが理解できる最もオーソドックスな方法です。たぶん。

```php:パラメータ(必要に応じてtry～catchブロックを設けます)
$date = new DateTimeImmutable('指定年-指定月-1 00:00:00');
$day  = (int)$date->format('w'); // 曜日オフセット(※生成過程で書き換えられます)
$week = 0;                       // 週オフセット(※生成過程で書き換えられます)
$max  = (int)$date->format('t'); // 合計日数
```

```php: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][] = '';
}
```

## **関数型コーディング**

```php:パラメータ(必要に応じて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; // 曜日オフセット(後)
```

### **著者案**

```php:2次元配列の生成
$rows = array_chunk(array_merge(
    array_fill(0, $before, ''),
    range(1, $max),
    array_fill(0, $after, '')
), 7);
```

※ PHP5.5以前では **[array_fill](http://www.php.net//manual/ja/function.array-fill.php)** 関数で0個を埋めることはできません。0かどうかの分岐が必要です。

### **@naga3 さん案**

（コメント欄参照）

```php:2次元配列の生成
$rows = array_chunk(array_map(
    function ($i) use ($max) { return $i < 1 || $i > $max ? '' : $i; },
    range(1 - $before, $max + $after)
), 7);
```

# **カレンダー生成スクリプトの実装**

上記で紹介した関数型ロジックを用いて、実際にカレンダーを生成してみます。

![カレンダー](https://qiita-image-store.s3.amazonaws.com/0/25060/e1489a7b-9ec4-4b5f-67f4-f5efab1bd6f2.png)

## **style.css**

```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
<?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>
```