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

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

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

宇宙カレンダー

サンプル・プログラム

イベント・ファイル

イベント・ファイルの構造(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.0" encoding="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>

準備:外部クラス

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

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

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

0215: /**
0216:  * イベント・ファイルを読み込み、ソートする
0217:  * @param string $fname イベント・ファイル名
0218:  * @param array  $caldb イベントを格納する配列
0219:  * @param array  $caption キャプションを格納する配列
0220:  * @return bool TRUE/FALSE
0221: */
0222: function readDB($fname, &$caldb, &$caption) {
0223:     $pcl = new pahooCalendar();  //pahooCalendarクラス
0224:     $pcl->setLanguage('jp');
0225: 
0226:     $jd_end = $pcl->Gregorian2JD(date('Y') + 1, 1, 1, 0, 0, 0);    //今年の最後
0227:     $dd = 365 + date('L');                                    //1年の日数
0228:     $res = FALSE;
0229: 
0230:     //イベント・ファイル読み込み
0231:     if (file_exists($fname)) {
0232:         $xml = simplexml_load_file($fname);
0233:         //キャプション
0234:         if (isset($xml->caption->label)) {
0235:             $caption['label'] = isset($xml->caption->label) ? (string)$xml->caption->label : '宇宙カレンダー';
0236:             $caption['description'] = isset($xml->caption->description) ? (string)$xml->caption->description : '宇宙誕生から現在までを1年に圧縮';
0237:         }
0238:         //開始
0239:         if (isset($xml->start->before)) {
0240:             $start = (double)$xml->start->before;
0241:             $unit = ($dd * 24 * 60 * 60) / $start;       //1秒当たり年数
0242:             $cnt = 0;
0243:             $caldb[$cnt]['label'] = (string)$xml->start->label;
0244:             $ss = $start * $unit;
0245:             list($caldb[$cnt]['dt'], $caldb[$cnt]['fmt']) = sigcalendar($ss);
0246:             $cnt++;
0247:         }
0248:         //イベント読み込み
0249:         foreach ($xml->event as $event) {
0250:             $caldb[$cnt]['label'] = (string)$event->label;
0251:             if (isset($event->before)) {
0252:                 $ss = ($start - (double)$event->before) * $unit;
0253:             } else if (isset($event->after)) {
0254:                 $ss = (double)$event->after * $unit;
0255:             } else if (isset($event->dt)) {
0256:                 sscanf((string)$event->dt, '%d/%d/%d %d:%d:%f', $year$month$day$hour$min$sec);
0257:                 $jd = $pcl->Gregorian2JD($year$month$day$hour$min$sec);
0258:                 $ss = ($start - ($jd_end - $jd) / $dd) * $unit;
0259:             } else {
0260:                 $ss = 0;
0261:             }
0262:             //カレンダー計算
0263:             list($caldb[$cnt]['dt'], $caldb[$cnt]['fmt']) = sigcalendar($ss);
0264:             $cnt++;
0265:         }
0266:         $res = TRUE;
0267:     }
0268:     $pcl = NULL;
0269: 
0270:     //ソート
0271:     usort($caldbfunction ($a$b) {
0272:         if (! isset($a['dt'])) return NULL;
0273:         if (! isset($b['dt'])) return NULL;
0274:         return $a['dt'] > $b['dt'] ? (+1) : (-1);
0275:     });
0276: 
0277:     return $res;
0278: }

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

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

解説:カレンダー計算

0179: /**
0180:  * カレンダー計算
0181:  * @param double $ss 秒数
0182:  * @return array(カレンダー,フォーマット)
0183: */
0184: function sigcalendar($ss) {
0185:     $yyyy = date('Y');       //今年の西暦年
0186:     $tt = strtotime($yyyy .'/1/1 0:0:0') + $ss;
0187:     $dt = date('m/d H:i:s', $tt);
0188: 
0189:     //12/31 23:59:59以降→ミリ秒まで
0190:     if (preg_match('/^12\/31 23:59:59/', $dt) > 0) {
0191:         $fmt = 'm/d H:i:s';
0192:         $dt = date($fmt$tt);
0193:         $dt .= sprintf('.%03d', (($ss - floor($ss)) * 1000));
0194:     //12/31 20時以降→秒まで
0195:     } else if (preg_match('/^12\/31 2/', $dt) > 0) {
0196:         $fmt = 'm/d H:i:s';
0197:         $dt = date($fmt$tt);
0198:     //12/31→分まで
0199:     } else if (preg_match('/^12\/31/', $dt) > 0) {
0200:         $fmt = 'm/d H:i';
0201:         $dt = date($fmt$tt);
0202:     //12月→時まで
0203:     } else if (preg_match('/^12/', $dt) > 0) {
0204:         $fmt = 'm/d H:00';
0205:         $dt = date($fmt$tt);
0206:     //それ以前→日まで
0207:     } else {
0208:         $fmt = 'm/d';
0209:         $dt = date($fmt$tt);
0210:     }
0211: 
0212:     return array($dt$fmt);
0213: }

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

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

0280: /**
0281:  * データベースに表示用フラグを立てる:全部
0282:  * @param array  $caldb データベース
0283:  * @param int    $ti    マークしたい時刻(省略時は現在時刻)
0284:  * @return なし
0285: */
0286: function checkDB_all(&$caldb$ti=0) {
0287:     if ($ti == 0)   $ti = time();    //省略時
0288: 
0289:     $key = 0;
0290:     $flag = (-1);
0291:     foreach ($caldb as $rec) {
0292:         if (isset($rec['dt'])) {
0293:             $t0 = date($rec['fmt'], $ti);
0294:             if ($rec['dt'] < $t0) {
0295:                 $caldb[$key]['flag'] = $flag;
0296:             } else if ($flag == (-1)) {
0297:                 $flag++;
0298:                 $caldb[$key]['flag'] = $flag;
0299:                 $flag++;
0300:             } else {
0301:                 $caldb[$key]['flag'] = $flag;;
0302:             }
0303:             $key++;
0304:         }
0305:     }
0306: }
0307: 
0308: /**
0309:  * データベースに表示用フラグを立てる:同じ月
0310:  * @param array  $caldb データベース
0311:  * @param int    $ti    マークしたい時刻(省略時は現在時刻)
0312:  * @return なし
0313: */
0314: function checkDB_month(&$caldb$ti=0) {
0315:     if ($ti == 0)   $ti = time();    //省略時
0316:     $t1 = date('m', $ti);
0317: 
0318:     $flag = (-2);
0319:     $key = 0;
0320:     foreach ($caldb as $rec) {
0321:         if (isset($rec['dt'])) {
0322:             $t0 = date($rec['fmt'], $ti);
0323:             if ($rec['dt'] < $t0) {
0324:                 $caldb[$key]['flag'] = $flag;
0325:             } else if ($flag == (-2)) {
0326:                 $caldb[$key]['flag'] = 0;
0327:                 $flag = (+2);
0328:             } else {
0329:                 $caldb[$key]['flag'] = $flag;
0330:             }
0331:             if (preg_match('/(^\d{2})/iu', $rec['dt'], $arr) > 0) {
0332:                 if (($arr[1] == $t1) && ($caldb[$key]['flag']) != 0) {
0333:                     $caldb[$key]['flag'] = (-1);
0334:                 }
0335:             }
0336:             $key++;
0337:         }
0338:     }
0339: }
0340: 
0341: /**
0342:  * データベースに表示用フラグを立てる:「今ここ」と前後2件ずつ
0343:  * @param array  $caldb データベース
0344:  * @param int    $ti    マークしたい時刻(省略時は現在時刻)
0345:  * @return なし
0346: */
0347: function checkDB_now(&$caldb$ti=0) {
0348:     if ($ti == 0)   $ti = time();    //省略時
0349: 
0350:     $flag = (-2);
0351:     $key = 0;
0352:     foreach ($caldb as $rec) {
0353:         if (isset($rec['dt'])) {
0354:             $t0 = date($rec['fmt'], $ti);
0355:             if ($rec['dt'] < $t0) {
0356:                 $caldb[$key]['flag'] = $flag;
0357:                 $key++;
0358:             } else if ($flag == (-2)) {
0359:                 $flag++;
0360:                 for ($i = $key - 2; $i < $key$i++) {
0361:                     if (isset($caldb[$i]['label']))  $caldb[$i]['flag'] = $flag;
0362:                 }
0363:                 $flag++;
0364:                 $i = $key;
0365:                 if (isset($caldb[$i]['label']))  $caldb[$i]['flag'] = $flag;
0366:                 $flag++;
0367:                 $key++;
0368:                 $caldb[$key]['flag'] = $flag;
0369:                 $key++;
0370:                 if (isset($caldb[$key]['label']))    $caldb[$key]['flag'] = $flag;
0371:                 $key++;
0372:                 $flag++;
0373:             } else {
0374:                 $caldb[$key]['flag'] = $flag;
0375:                 $key++;
0376:             }
0377:         }
0378:     }
0379: }

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

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

0381: /**
0382:  * 宇宙カレンダーを作成:テキスト形式
0383:  * @param array  $caldb   データベース
0384:  * @param array  $caption キャプション
0385:  * @return string 表示用コンテンツ
0386: */
0387: function makeCosmicCalendar_text($caldb$caption) {
0388:     $outstr = "■{$caption['label']}({$caption['description']})<br />\n";
0389:     foreach ($caldb as $rec) {
0390:         if ($rec['flag'] >= (-1) && $rec['flag'] <= (+1)) {
0391:             $outstr .= $rec['dt'] . ' - ' . $rec['label'];
0392:             if ($rec['flag'] == 0)  $outstr .= '←いまココ';
0393:             $outstr .= "<br />\n";
0394:         }
0395:     }
0396:     return $outstr;
0397: }
0398: 
0399: /**
0400:  * 宇宙カレンダーを作成:テーブル形式
0401:  * @param array  $caldb データベース
0402:  * @param array  $caption キャプション
0403:  * @return string 表示用コンテンツ
0404: */
0405: function makeCosmicCalendar_table($caldb$caption) {
0406: $outstr =<<< EOT
0407: <table class="stripe">
0408: <caption>{$caption['label']}</caption>
0409: <tr><th>日時</th><th>イベント</th></tr>
0410: 
0411: EOT;
0412: 
0413:     foreach ($caldb as $rec) {
0414:         if ($rec['flag'] >= (-1) && $rec['flag'] <= (+1)) {
0415:             $mark = ($rec['flag'] == 0) ? '&nbsp;&#x23F1;' : '';
0416: $outstr .=<<< EOT
0417: <tr>
0418: <td>{$rec['dt']}$mark</td>
0419: <td>{$rec['label']}</td>
0420: </tr>
0421: 
0422: EOT;
0423:         }
0424:     }
0425:     return $outstr;
0426: }

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

参考サイト

(この項おわり)
header