PHPでシンプルな日程表を作ってみる

# 概要
日程表は任意の1年分とし、いつ誰がどのクライアントを担当するか、だけに絞ったシンプルなものです。DBからそのままデータを引っ張ると縦に長いのが出てきてイマイチだと思います。かといってDB構造は乱したくない。何をやりたいかというと、

![tate.png](https://qiita-image-store.s3.amazonaws.com/0/112959/9275c9be-97b7-8701-42cf-76f970b3d592.png)

↑これを

![yoko.png](https://qiita-image-store.s3.amazonaws.com/0/112959/476a33e3-365b-eecf-6712-884c2c771f77.png)

こんな感じに見せたいということです。だから別に日程表じゃなくても何でも良かったんですが。テーマとしては縦横転換とでも言った良いのかよく分かりませんが。

# 環境
PHP 5.5.19
SQLite 3.6.20

# コード

schedule.dbというファイルだけ作ったらテーブルやら初期設定はphp側から作っておきます。また、各人ごとに1年分の空レコードを予め付与しておくことにしています。

```setup.php
<?php

/* DB接続 */  
$db = new PDO("sqlite:schedule.db");

/* メンバーテーブル */  
$sql = "CREATE TABLE IF NOT EXISTS member (id INTEGER PRIMARY KEY, name TEXT)";
$stmt = $db->prepare($sql);
$stmt->execute();

/* メンバーを適当に作る */  
$members = ["Aさん", "Bさん", "Cさん", "Dさん", "Eさん", "Fさん", "Gさん", "Hさん", "Iさん", "Jさん", "Kさん"];
$stmt = $db->query("SELECT COUNT(*) FROM member");
if ($stmt->fetchColumn() == 0) {
  $db->beginTransaction();
  foreach ($members as $member) {
    $sql = "INSERT INTO member (name) VALUES (?)";
    $stmt = $db->prepare($sql);
    $stmt->execute(array($member));
  }
  $db->commit();
}

/* クライアントテーブル */  
$sql = "CREATE TABLE IF NOT EXISTS client (id INTEGER PRIMARY KEY, name TEXT)";
$stmt = $db->prepare($sql);
$stmt->execute();

/* クライアントを適当に作る */  
$clients = ["A社", "B社", "C社", "D社", "E社"]; 
$stmt = $db->query("SELECT COUNT(*) FROM client");
if ($stmt->fetchColumn() == 0) {
  $db->beginTransaction();
  foreach ($clients as $client) {
    $sql = "INSERT INTO client (name) VALUES (?)";
    $stmt = $db->prepare($sql);
    $stmt->execute(array($client));
  }
  $db->commit();
}

/* スケジュールテーブル（2016年度暦年とする） */
$sql = "CREATE TABLE IF NOT EXISTS schedule (id INTEGER PRIMARY KEY, yy INTEGER, mm INTEGER, dd INTEGER, member_id INTEGER, client_id INTEGER)";
$stmt = $db->prepare($sql);
$stmt->execute();

/* 人数分の１年分のスケジュールを予め準備する */

/* 1ヶ月31日ベース */
$stmt = $db->query("SELECT COUNT(*) FROM schedule");
if ($stmt->fetchColumn() == 0) {
  $count = count($members);
  $db->beginTransaction();
  for ($i=1; $i<=$count; $i++) {
    for ($j=1; $j<=12; $j++) {
      for ($k=1; $k<=31; $k++) {
        $sql = "INSERT INTO schedule (yy, mm, dd, member_id) VALUES (?, ?, ?, ?)";
        $stmt = $db->prepare($sql);
        $stmt->execute(array(2016, $j, $k, $i));
      }
    }
  }
  $db->commit();
}

/* 不正日付削除 */
$db->beginTransaction();
$db->exec("DELETE FROM schedule WHERE yy=2016 and (mm IN(2, 4, 6, 9, 11)) and (dd=31)");
if (checkdate(2, 29, 2016)) {
  $db->exec("DELETE FROM schedule WHERE yy=2016 and mm=2 and dd=30");
} else {
  $db->exec("DELETE FROM schedule WHERE yy=2016 and mm=2 and dd IN(29, 30)");
}
$db->commit();

/* DB切断 */
$db = null;
?>
```

次に各人に適当にクライアントを割り振ります。

```ramdom.php
<?php

$db = new PDO("sqlite:schedule.db");

/* 総日数 */
$stmt = $db->query("SELECT COUNT(*) FROM schedule");
$i = $stmt->fetchColumn();

/* 総人員 */
$stmt = $db->query("SELECT COUNT(*) FROM member");
$j = $stmt->fetchColumn();

/* 総クライアント数 */
$stmt = $db->query("SELECT COUNT(*) FROM client");
$k = $stmt->fetchColumn();

/* 各人のスケジュールに適当にクライアントを割り当てる */
$db->beginTransaction();
for ($m=0; $m<$j; $m++) {
  for ($n=372*$m+1; $n<=372*($m+1); $n++) {
    $o = mt_rand(1, $k);
    $sql = "UPDATE schedule SET client_id = ? WHERE id = ?";
    $stmt = $db->prepare($sql);
    $stmt->execute(array($o, $n));
  }
}
$db->commit();

$db = null;

?>
```

普通にDBの内容を表示するだけなら

```show.php

<?php
$db = new PDO("sqlite:schedule.db");
$stmt = $db->query("SELECT yy, mm, dd, a.name, b.name FROM schedule
 LEFT JOIN (SELECT id, name FROM member) AS a ON schedule.member_id=a.id
 LEFT JOIN (SELECT id, name FROM client) AS b ON schedule.client_id=b.id");
$rows = $stmt->fetchAll(PDO::FETCH_NUM);
echo "<table border='1'>";
foreach ($rows as $row) {
  echo "<tr>";
  for ($i=0; $i<5; $i++) {
    echo "<td>".$row[$i]."</td>";
  }
  echo "</tr>";
}
echo "</table>";
?>

```

これだと最初に示したような縦長な結果しか得られません。そこでarray_chunkとarray_mapを駆使して配列をあれこれ強引に操作して実現します。

```show2.php
<?php
$db = new PDO("sqlite:schedule.db");

/* メンバー情報を取得しておく */  
$stmt = $db->query("SELECT * FROM member");
$members = $stmt->fetchAll(PDO::FETCH_ASSOC);

/* 1年分の日付を昇順で取得して2次元配列に格納 */  
$stmt = $db->query("
  SELECT DISTINCT yy, mm, dd FROM schedule ORDER BY mm, dd ASC 
");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

/* 各メンバーのジョブを日付順、メンバー順に全部取得して配列に格納 */  
$stmt = $db->query("
  SELECT client.name AS client FROM schedule 
  LEFT JOIN (SELECT id, name FROM client) AS client ON schedule.client_id=client.id 
  ORDER BY mm, dd, member_id ASC 
");
$rows2 = $stmt->fetchAll(PDO::FETCH_ASSOC);

/* メンバーの人数でチャンクして2次元配列化する */  
$mcount = count($members);
$rows3 = array_chunk($rows2, $mcount);

/* 日付の配列とジョブの配列のヨコ結合もどきをする */  
$rows4 = array_map(null, $rows, $rows3);

/* ここまで来たらあとは吐き出すだけ */  

echo "<table border='1'>";
echo "<tr>";
echo "<td>年</td><td>月</td><td>日</td>";
foreach ($members as $member) {
  echo "<td>".$member["name"]."</td>";
}
echo "</tr>";

$cnt = count($rows4);
for ($i=0; $i<$cnt; $i++) {
  echo "<tr>";
  echo "<td>".$rows4[$i][0]["yy"]."</td>";
  echo "<td>".$rows4[$i][0]["mm"]."</td>";
  echo "<td>".$rows4[$i][0]["dd"]."</td>";
  for ($j=0; $j<$mcount; $j++) {
    echo "<td>".$rows4[$i][1][$j]["client"]."</td>";
  }
  echo "</tr>";
}

echo "</table>";

$db = null;

?>

```
# array_mapについて
[http://php.net/manual/ja/function.array-map.php]
ここに、
>通常、二つ以上の配列を使用する場合、 それらの長さは等しい必要があります。 これは、 コールバック関数が対応する要素に対して並行して適用されるためです。 配列の長さが等しくない場合、要素数の少ない配列は空の要素で拡張して、最も長い配列の要素数に合わせます。
この関数の面白い使用方法として、 配列の配列を構築するというものがあります。 これは、コールバック関数の名前として NULL を使用することにより、簡単に実行できるものです。

とあり、これを試したらたまたまうまく横結合できちゃったということなんですが恥ずかしながらその理屈っぽいところは全く理解していません（これもしかしてPHPのバグ？）。


# 最後に
メンバーやクライアント、日程変更は適当にフォーム作って更新できるようにすればよいと思いますが今回はその辺省略しています。あと、SQL段階でLEFT JOINを人数分作って無理やり横に結合する方法はSQLiteの場合だけかは分かりませんが命令文長過ぎエラーになるかと思います。なお、グーグルスプレッドシート共有すれば済むはなしかもしれません(GASのquery関数ならpivot1発で同じような変形ができますしね）。