PHPで宇宙カレンダーを作る

(1/1)
宇宙の誕生から今日までを1年間に見立てた「宇宙カレンダー」という考え方がある。1月1日午前0時にビッグバンが起き、9月1日に太陽系が形成、11月6日に真核生物が誕生した‥‥という形のものだ。
アメリカの天文学者で作家の故カール・セーガン氏が考案したとされ、パパぱふぅは学生時代、セーガン氏が進行担当したテレビ番組「コスモス」や、来日した際の講演などを聞き、当時は電卓で計算し、ノートに記入したものだ。
今回は、PHPを使い、あらかじめ用意した年表データを宇宙カレンダーに変換し、表示するプログラムを作る。なお、年表データを、たとえば日本史や家族史に置き換えれば、オリジナルの宇宙カレンダーを作ることができる。

(2021年5月12日)バグ修正
(2021年1月16日)PHP8対応

目次

サンプル・プログラムの実行例

宇宙カレンダー

サンプル・プログラム

圧縮ファイルの内容
cosmicCalendar.phpサンプル・プログラム本体。
cosmicCalendar.xmlイベント・ファイル
pahooCalendar.php暦計算クラス pahooCalendar。
暦計算クラスの使い方は「PHPで日出没・月出没・月齢・潮を計算」を参照。include_path が通ったディレクトリに配置すること。

イベント・ファイル

イベント・ファイルの構造(xml) cosmicCalendar caption label キャプション description カレンダーの説明 start label 開始イベント before 現在から××年前:例 138E+8     description イベントの説明 event label イベント名 after 開始イベントからの年数:例 30E+4     description イベントの説明 event label イベント名 before 現在から××年前:例 135E+8     description イベントの説明 event label イベント名 dt 西暦表記:例 1945/8/15 00:00:00     description イベントの説明 event label イベント名 dt 紀元前表記:例 -1792/1/1 00:00:00     description イベントの説明
まず、上図のような構造のイベント・ファイル "cosmicCalendar.xml" を用意し、スクリプトと同じフォルダに配置する。
同梱したファイルには、宇宙の誕生から現在までの主要イベントを記入済みである。
このイベント・ファイルを編集すれば、日本史や家族史を使った宇宙カレンダー(365日カレンダーと呼んだ方がいいかもしれない)を表示することができる。

0001: <?xml version="1.0encoding="utf-8" ?>
0002: <!-- 年代記 -->
0003: <cosmicCalendar>
0004: <!-- キャプション -->
0005: <caption>
0006:     <label>宇宙カレンダー</label>
0007:     <description>宇宙誕生から現在までを1年に圧縮</description>
0008: </caption>
0009: <!-- 開始 -->
0010: <start>
0011:     <label>宇宙誕生</label>
0012:     <before unit="year">138E+8</before>
0013:     <description></description>
0014: </start>
0015: <finish>
0016:     <label>現在</label>
0017:     <before unit="year">0</before>
0018:     <description></description>
0019: </finish>
0020: 
0021: <!-- 以下、イベント・データベース -->
0022: <event>
0023:     <label>宇宙の晴れ上がり</label>
0024: <!-- 開始イベントからの年数を記載するときはafter -->
0025:     <after unit="year">30E+4</after>
0026:     <description></description>
0027: </event>
0028: <event>
0029:     <label>最初の恒星</label>
0030: <!-- 現在から××年前を記載するときはbefore -->
0031:     <before unit="year">135E+8</before>
0032:     <description></description>
0033: </event>
0034: <event>
0035:     <label>太平洋戦争終結</label>
0036: <!-- 西暦記載するときはdtでyyyy/mm/dd hh:mm:ss形式 -->
0037:     <dt>1945/8/15 00:00:00</dt>
0038:     <description></description>
0039: </event>
0040: <event>
0041:     <label>ハンムラビ王の即位</label>
0042: <!-- 紀元前はマイナス表記 -->
0043:     <dt>-1792/1/1 00:00:00</dt>
0044:     <description></description>
0045: </event>
0046: <event>
0047:     <label>最初の銀河の形成</label>
0048:     <before unit="year">132E+8</before>
0049:     <description></description>
0050: </event>

準備:外部クラス

0044: //暦計算クラス
0045: require_once('pahooCalendar.php');

超長期のカレンダー計算が必要なことから、ユーザークラス "pahooCalendar" に用意したユリウス日計算メソッド Gregorian2JD を利用する。
そこで、クラスファイル "pahooCalendar.php" を  require_once  し、オブジェクトを生成する。

解説:イベント・ファイルを読み込み、ソートする

0259: /**
0260:  * イベント・ファイルを読み込み、ソートする
0261:  * @param   string $fnameイベント・ファイル名
0262:  * @param   array  $caldbイベントを格納する配列
0263:  * @param   array  $captionキャプションを格納する配列
0264:  * @return  bool TRUE/FALSE
0265: */
0266: function readDB($fname, &$caldb, &$caption) {
0267:     $pcl = new pahooCalendar();  //pahooCalendarクラス
0268:     $pcl->setLanguage('jp');
0269: 
0270:     $jd_end = $pcl->Gregorian2JD(date('Y') + 1, 1, 1, 0, 0, 0);   //今年の最後
0271:     $dd = 365 + date('L');                                   //1年の日数
0272:     $res = FALSE;
0273: 
0274:     //イベント・ファイル読み込み
0275:     if (file_exists($fname)) {
0276:         $xml = simplexml_load_file($fname);
0277:         //キャプション
0278:         if (isset($xml->caption->label)) {
0279:             $caption['label'] = isset($xml->caption->label) ? (string)$xml->caption->label : '宇宙カレンダー';
0280:             $caption['description'] = isset($xml->caption->description) ? (string)$xml->caption->description : '宇宙誕生から現在までを1年に圧縮';
0281:         }
0282:         //開始
0283:         if (isset($xml->start->before)) {
0284:             $start = (double)$xml->start->before;
0285:             $unit = ($dd * 24 * 60 * 60) / $start;       //1秒当たり年数
0286:             $cnt = 0;
0287:             $caldb[$cnt]['label'] = (string)$xml->start->label;
0288:             $ss = $start * $unit;
0289:             list($caldb[$cnt]['dt'], $caldb[$cnt]['fmt']) = sigcalendar($ss);
0290:             $cnt++;
0291:         }
0292:         //イベント読み込み
0293:         foreach ($xml->event as $event) {
0294:             $caldb[$cnt]['label'] = (string)$event->label;
0295:             if (isset($event->before)) {
0296:                 $ss = ($start - (double)$event->before) * $unit;
0297:             } else if (isset($event->after)) {
0298:                 $ss = (double)$event->after * $unit;
0299:             } else if (isset($event->dt)) {
0300:                 sscanf((string)$event->dt, '%d/%d/%d %d:%d:%f', $year$month$day$hour$min$sec);
0301:                 $jd = $pcl->Gregorian2JD($year$month$day$hour$min$sec);
0302:                 $ss = ($start - ($jd_end - $jd) / $dd) * $unit;
0303:             } else {
0304:                 $ss = 0;
0305:             }
0306:             //カレンダー計算
0307:             list($caldb[$cnt]['dt'], $caldb[$cnt]['fmt']) = sigcalendar($ss);
0308:             $cnt++;
0309:         }
0310:         $res = TRUE;
0311:     }
0312:     $pcl = NULL;
0313: 
0314:     //ソート
0315:     usort($caldbfunction ($a$b) {
0316:         if (! isset($a['dt']))   return NULL;
0317:         if (! isset($b['dt']))   return NULL;
0318:         return $a['dt'] > $b['dt'] ? (+1) : (-1);
0319:     });
0320: 
0321:     return $res;
0322: }

ユーザー関数 readDB では、イベント・ファイルを  simplexml_load_file  で読み込んだら、要素を処理しやすいように配列 $caldb に読み込み、最後に、 usort  を使って年月日の古い順に並び替える。

個々のイベント要素を読み込む際は、年数の表記が before, after, dt によって場合分けを行い、宇宙誕生(startに記述)を基準点として、そこから何秒の距離にあるかを変数 $ss に代入する。
イベントを、基準点と、その距離に置換することで、宇宙史だけでなく、日本史や家族史のような、スケールの異なるカレンダーも作ることができる。

解説:カレンダー計算

0223: /**
0224:  * カレンダー計算
0225:  * @param   double $ss 秒数
0226:  * @return  array(カレンダー,フォーマット)
0227: */
0228: function sigcalendar($ss) {
0229:     $yyyy = date('Y');      //今年の西暦年
0230:     $tt = strtotime($yyyy .'/1/1 0:0:0') + $ss;
0231:     $dt = date('m/d H:i:s', $tt);
0232: 
0233:     //12/31 23:59:59以降→ミリ秒まで
0234:     if (preg_match('/^12\/31 23:59:59/', $dt) > 0) {
0235:         $fmt = 'm/d H:i:s';
0236:         $dt = date($fmt$tt);
0237:         $dt .= sprintf('.%03d', (($ss - floor($ss)) * 1000));
0238:     //12/31 20時以降→秒まで
0239:     } else if (preg_match('/^12\/31 2/', $dt) > 0) {
0240:         $fmt = 'm/d H:i:s';
0241:         $dt = date($fmt$tt);
0242:     //12/31→分まで
0243:     } else if (preg_match('/^12\/31/', $dt) > 0) {
0244:         $fmt = 'm/d H:i';
0245:         $dt = date($fmt$tt);
0246:     //12月→時まで
0247:     } else if (preg_match('/^12/', $dt) > 0) {
0248:         $fmt = 'm/d H:00';
0249:         $dt = date($fmt$tt);
0250:     //それ以前→日まで
0251:     } else {
0252:         $fmt = 'm/d';
0253:         $dt = date($fmt$tt);
0254:     }
0255: 
0256:     return array($dt$fmt);
0257: }

宇宙カレンダーでは、数十億年前の出来事を扱うが、年代が古くなればなるほど誤差が大きいし、イベントの数も少ない。
そこで、一定の境界条件を設け、カレンダーの時分秒を表示しなかったり、逆にミリ秒まで表示するようにした。
これを行うのがユーザー関数 sigcalendar である。

解説:データベースに表示用フラグを立てる

0324: /**
0325:  * データベースに表示用フラグを立てる:全部
0326:  * @param   array  $caldbデータベース
0327:  * @param   int    $ti    マークしたい時刻(省略時は現在時刻)
0328:  * @return  なし
0329: */
0330: function checkDB_all(&$caldb$ti=0) {
0331:     if ($ti == 0)   $ti = time();    //省略時
0332: 
0333:     $key = 0;
0334:     $flag = (-1);
0335:     foreach ($caldb as $rec) {
0336:         if (isset($rec['dt'])) {
0337:             $t0 = date($rec['fmt'], $ti);
0338:             if ($rec['dt'] < $t0) {
0339:                 $caldb[$key]['flag'] = $flag;
0340:             } else if ($flag == (-1)) {
0341:                 $flag++;
0342:                 $caldb[$key]['flag'] = $flag;
0343:                 $flag++;
0344:             } else {
0345:                 $caldb[$key]['flag'] = $flag;;
0346:             }
0347:             $key++;
0348:         }
0349:     }
0350: }

0352: /**
0353:  * データベースに表示用フラグを立てる:同じ月
0354:  * @param   array  $caldbデータベース
0355:  * @param   int    $ti    マークしたい時刻(省略時は現在時刻)
0356:  * @return  なし
0357: */
0358: function checkDB_month(&$caldb$ti=0) {
0359:     if ($ti == 0)   $ti = time();    //省略時
0360:     $t1 = date('m', $ti);
0361: 
0362:     $flag = (-2);
0363:     $key = 0;
0364:     foreach ($caldb as $rec) {
0365:         if (isset($rec['dt'])) {
0366:             $t0 = date($rec['fmt'], $ti);
0367:             if ($rec['dt'] < $t0) {
0368:                 $caldb[$key]['flag'] = $flag;
0369:             } else if ($flag == (-2)) {
0370:                 $caldb[$key]['flag'] = 0;
0371:                 $flag = (+2);
0372:             } else {
0373:                 $caldb[$key]['flag'] = $flag;
0374:             }
0375:             if (preg_match('/(^\d{2})/iu', $rec['dt'], $arr) > 0) {
0376:                 if (($arr[1] == $t1) && ($caldb[$key]['flag']) != 0) {
0377:                     $caldb[$key]['flag'] = (-1);
0378:                 }
0379:             }
0380:             $key++;
0381:         }
0382:     }
0383: }

0385: /**
0386:  * データベースに表示用フラグを立てる:「今ここ」と前後2件ずつ
0387:  * @param   array  $caldbデータベース
0388:  * @param   int    $ti    マークしたい時刻(省略時は現在時刻)
0389:  * @return  なし
0390: */
0391: function checkDB_now(&$caldb$ti=0) {
0392:     if ($ti == 0)   $ti = time();    //省略時
0393: 
0394:     $flag = (-2);
0395:     $key = 0;
0396:     foreach ($caldb as $rec) {
0397:         if (isset($rec['dt'])) {
0398:             $t0 = date($rec['fmt'], $ti);
0399:             if ($rec['dt'] < $t0) {
0400:                 $caldb[$key]['flag'] = $flag;
0401:                 $key++;
0402:             } else if ($flag == (-2)) {
0403:                 $flag++;
0404:                 for ($i = $key - 2; $i < $key$i++) {
0405:                     if (isset($caldb[$i]['label'])) $caldb[$i]['flag'] = $flag;
0406:                 }
0407:                 $flag++;
0408:                 $i = $key;
0409:                 if (isset($caldb[$i]['label'])) $caldb[$i]['flag'] = $flag;
0410:                 $flag++;
0411:                 $key++;
0412:                 $caldb[$key]['flag'] = $flag;
0413:                 $key++;
0414:                 if (isset($caldb[$key]['label']))   $caldb[$key]['flag'] = $flag;
0415:                 $key++;
0416:                 $flag++;
0417:             } else {
0418:                 $caldb[$key]['flag'] = $flag;
0419:                 $key++;
0420:             }
0421:         }
0422:     }
0423: }

宇宙カレンダーを全て表示すると大きな表になってしまうので、指定した基準日(デフォルトでは現在日時)を含む月のイベントだけ表示するモード、基準日の前後2件ずつ、合計5件のイベントだけ表示するモードを加えた。
こららを処理するのが、ユーザー関数 checkDB_all, checkDB_month, checkDB_now である。

解説:宇宙カレンダーを作成

0443: /**
0444:  * 宇宙カレンダーを作成:テーブル形式
0445:  * @param   array  $caldbデータベース
0446:  * @param   array  $captionキャプション
0447:  * @return  string 表示用コンテンツ
0448: */
0449: function makeCosmicCalendar_table($caldb$caption) {
0450: $outstr =<<< EOT
0451: <table class="stripe">
0452: <caption>{$caption['label']}</caption>
0453: <tr><th>日時</th><th>イベント</th></tr>
0454: 
0455: EOT;
0456: 
0457:     foreach ($caldb as $rec) {
0458:         if ($rec['flag'] >= (-1) && $rec['flag'] <= (+1)) {
0459:             $mark = ($rec['flag'] == 0) ? '&nbsp;&#x23F1;' : '';
0460: $outstr .=<<< EOT
0461: <tr>
0462: <td>{$rec['dt']}$mark</td>
0463: <td>{$rec['label']}</td>
0464: </tr>
0465: 
0466: EOT;
0467:         }
0468:     }
0469: $outstr .=<<< EOT
0470: </table>
0471: 
0472: EOT;
0473:     return $outstr;
0474: }

0425: /**
0426:  * 宇宙カレンダーを作成:テキスト形式
0427:  * @param   array  $caldb   データベース
0428:  * @param   array  $captionキャプション
0429:  * @return  string 表示用コンテンツ
0430: */
0431: function makeCosmicCalendar_text($caldb$caption) {
0432:     $outstr = "<hr />■{$caption['label']}({$caption['description']})<br />\n";
0433:     foreach ($caldb as $rec) {
0434:         if ($rec['flag'] >= (-1) && $rec['flag'] <= (+1)) {
0435:             $outstr .= $rec['dt'] . ' - ' . $rec['label'];
0436:             if ($rec['flag'] == 0)  $outstr .= '←いまココ';
0437:             $outstr .= "<br />\n";
0438:         }
0439:     }
0440:     return $outstr;
0441: }

最後に、宇宙カレンダーを作成する書式だが、HTMLの表形式で出力するユーザー関数 makeCosmicCalendar_table と、SNSへの投稿を想定してテキスト形式で出力するユーザー関数 makeCosmicCalendar_text の2種類を用意した。

参考サイト

(この項おわり)
header