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

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

(2022年12月30日)1月は時分まで表示するようにした,cosmicCalendar.xml更新
(2022年7月21日)バグ修正,cosmicCalendar.xml更新

目次

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

宇宙カレンダー

サンプル・プログラム

圧縮ファイルの内容
cosmicCalendar.phpサンプル・プログラム本体。
cosmicCalendar.xmlイベント・ファイル
pahooCalendar.php暦計算クラス pahooCalendar。
暦計算クラスの使い方は「PHPで日出没・月出没・月齢・潮を計算」を参照。include_path が通ったディレクトリに配置すること。
cosmicCalendar.php 更新履歴
バージョン 更新日 内容
1.5.0 2022/12/30 1月は時分まで表示するようにした
1.42 2022/07/21 bug-fix,イベント・ファイル更新
1.41 2021/05/12 bug-fix
1.4 2021/01/16 PHP8対応
1.3 2020/01/03 リファラチェック追加

イベント・ファイル

イベント・ファイルの構造(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日カレンダーと呼んだ方がいいかもしれない)を表示することができる。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <!-- 年代記 -->
   3: <cosmicCalendar>
   4: <!-- キャプション -->
   5: <caption>
   6:     <label>宇宙カレンダー</label>
   7:     <description>宇宙誕生から現在までを1年に圧縮</description>
   8: </caption>
   9: <!-- 開始 -->
  10: <start>
  11:     <label>宇宙誕生</label>
  12:     <before unit="year">138E+8</before>
  13:     <description></description>
  14: </start>
  15: <finish>
  16:     <label>現在</label>
  17:     <before unit="year">0</before>
  18:     <description></description>
  19: </finish>
  20: 
  21: <!-- 以下、イベント・データベース -->
  22: <event>
  23:     <label>宇宙の晴れ上がり</label>
  24: <!-- 開始イベントからの年数を記載するときは after -->
  25:     <after unit="year">30E+4</after>
  26:     <description></description>
  27: </event>
  28: <event>
  29:     <label>天の川銀河の誕生</label>
  30: <!-- 現在から××年前を記載するときは before -->
  31:     <before unit="year">110E+8</before>
  32:     <description></description>
  33: </event>
  34: <event>
  35:     <label>太平洋戦争終結</label>
  36: <!-- 西暦記載するときは dt  yyyy/mm/dd hh:mm:ss形式 -->
  37:     <dt>1945/8/15 00:00:00</dt>
  38:     <description></description>
  39: </event>
  40: <event>
  41:     <label>ハンムラビ王の即位</label>
  42: <!-- 紀元前はマイナス表記 -->
  43:     <dt>-1792/1/1 00:00:00</dt>
  44:     <description></description>
  45: </event>
  46: <event>
  47:     <label>最初の恒星</label>
  48: <!--  -->
  49:     <after unit="year">1.8E+8</after>
  50:     <description>2018年3月1日に、米アリゾナ州立大などの国際研究チームが発表した。</description>

準備:外部クラスなど

  38: //表示幅(単位:ピクセル)
  39: define('WIDTH', 600);
  40: 
  41: //イベント・ファイル
  42: define('FILE_COSMIC_CALENDAR', 'cosmicCalendar.xml');
  43: 
  44: //暦計算クラス
  45: require_once('pahooCalendar.php');

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

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

 276: /**
 277:  * イベント・ファイルを読み込み、ソートする
 278:  * @param  string $fname イベント・ファイル名
 279:  * @param  array  $caldb イベントを格納する配列
 280:  * @param  array  $caption キャプションを格納する配列
 281:  * @return    bool TRUE/FALSE
 282: */
 283: function readDB($fname, &$caldb, &$caption) {
 284:     $pcl = new pahooCalendar();     //pahooCalendarクラス
 285:     $pcl->setLanguage('jp');
 286: 
 287:     $jd_end = $pcl->Gregorian2JD(date('Y'+ 1, 1, 1, 0, 0, 0); //今年の最後
 288:     $dd = 365 + date('L');                                  //1年の日数
 289:     $res = FALSE;
 290: 
 291:     //イベント・ファイル読み込み
 292:     if (file_exists($fname)) {
 293:         $xml = simplexml_load_file($fname);
 294:         //キャプション
 295:         if (isset($xml->caption->label)) {
 296:             $caption['label'] = isset($xml->caption->label? (string)$xml->caption->label : '宇宙カレンダー';
 297:             $caption['description'] = isset($xml->caption->description? (string)$xml->caption->description : '宇宙誕生から現在までを1年に圧縮';
 298:         }
 299:         //開始
 300:         if (isset($xml->start->before)) {
 301:             $start = (double)$xml->start->before;
 302:             $unit = ($dd * 24 * 60 * 60) / $start;     //1秒当たり年数
 303:             $cnt = 0;
 304:             $caldb[$cnt]['label'] = (string)$xml->start->label;
 305:             $ss = $start * $unit;
 306:             list($caldb[$cnt]['dt'], $caldb[$cnt]['fmt']) = sigcalendar($ss);
 307:             $cnt++;
 308:         }
 309:         //イベント読み込み
 310:         foreach ($xml->event as $event) {
 311:             $caldb[$cnt]['label'] = (string)$event->label;
 312:             if (isset($event->before)) {
 313:                 $ss = ($start - (double)$event->before* $unit;
 314:             } else if (isset($event->after)) {
 315:                 $ss = (double)$event->after * $unit;
 316:             } else if (isset($event->dt)) {
 317:                 sscanf((string)$event->dt, '%d/%d/%d %d:%d:%f', $year, $month, $day, $hour, $min, $sec);
 318:                 $jd = $pcl->Gregorian2JD($year, $month, $day, $hour, $min, $sec);
 319:                 $ss = ($start - ($jd_end - $jd) / $dd* $unit;
 320:             } else {
 321:                 $ss = 0;
 322:             }
 323:             //カレンダー計算
 324:             list($caldb[$cnt]['dt'], $caldb[$cnt]['fmt']) = sigcalendar($ss);
 325:             $cnt++;
 326:         }
 327:         $res = TRUE;
 328:     }
 329:     $pcl = NULL;
 330: 
 331:     //ソート
 332:     usort($caldb, function ($a, $b) {
 333:         if (! isset($a['dt']))  return NULL;
 334:         if (! isset($b['dt']))  return NULL;
 335:         return $a['dt'> $b['dt'? (+1: (-1);
 336:     });
 337: 
 338:     return $res;
 339: }

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

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

解説:カレンダー計算

 223: /**
 224:  * カレンダー計算
 225:  * @param  double $ss 秒数
 226:  * @return    array(カレンダー,フォーマット)
 227: */
 228: function sigcalendar($ss) {
 229:     $yyyy = date('Y');      //今年の西暦年
 230:     $tt = (int)strtotime($yyyy .'/1/1 0:0:0'+ $ss;
 231:     $dt = date('m/d H:i:s', $tt);
 232: 
 233:     //01/01 00:00:01以前→ミリ秒まで
 234:     if (preg_match('/^01\/01 01:01:01/', $dt> 0) {
 235:         $fmt = 'm/d H:i:s';
 236:         $dt = date($fmt, $tt);
 237:         $dt .sprintf('.%03d', (($ss - floor($ss)) * 1000));
 238:     //01/01 4時以前→秒まで
 239:     } else if (preg_match('/^12\/31 0[0-4]./', $dt> 0) {
 240:         $fmt = 'm/d H:i:s';
 241:         $dt = date($fmt, $tt);
 242:     //01/01→分まで
 243:     } else if (preg_match('/^01\/01/', $dt> 0) {
 244:         $fmt = 'm/d H:i';
 245:         $dt = date($fmt, $tt);
 246:     //1月→時まで
 247:     } else if (preg_match('/^01/', $dt> 0) {
 248:         $fmt = 'm/d H:00';
 249:         $dt = date($fmt, $tt);
 250:     //12/31 23:59:59以降→ミリ秒まで
 251:     } else if (preg_match('/^12\/31 23:59:59/', $dt> 0) {
 252:         $fmt = 'm/d H:i:s';
 253:         $dt = date($fmt, $tt);
 254:         $dt .sprintf('.%03d', (($ss - floor($ss)) * 1000));
 255:     //12/31 20時以降→秒まで
 256:     } else if (preg_match('/^12\/31 2/', $dt> 0) {
 257:         $fmt = 'm/d H:i:s';
 258:         $dt = date($fmt, $tt);
 259:     //12/31→分まで
 260:     } else if (preg_match('/^12\/31/', $dt> 0) {
 261:         $fmt = 'm/d H:i';
 262:         $dt = date($fmt, $tt);
 263:     //12月→時まで
 264:     } else if (preg_match('/^12/', $dt> 0) {
 265:         $fmt = 'm/d H:00';
 266:         $dt = date($fmt, $tt);
 267:     //それ以前→日まで
 268:     } else {
 269:         $fmt = 'm/d';
 270:         $dt = date($fmt, $tt);
 271:     }
 272: 
 273:     return array($dt, $fmt);
 274: }

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

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

 341: /**
 342:  * データベースに表示用フラグを立てる:全部
 343:  * @param  array  $caldb データベース
 344:  * @param  int    $ti    マークしたい時刻(省略時は現在時刻)
 345:  * @return    なし
 346: */
 347: function checkDB_all(&$caldb, $ti=0) {
 348:     if ($ti == 0)   $ti = time();   //省略時
 349: 
 350:     $key = 0;
 351:     $flag = (-1);
 352:     foreach ($caldb as $rec) {
 353:         if (isset($rec['dt'])) {
 354:             $t0 = date($rec['fmt'], $ti);
 355:             if ($rec['dt']  <$t0) {
 356:                 $caldb[$key]['flag'] = $flag;
 357:             } else if ($flag == (-1)) {
 358:                 $flag++;
 359:                 $caldb[$key]['flag'] = $flag;
 360:                 $flag++;
 361:             } else {
 362:                 $caldb[$key]['flag'] = $flag;;
 363:             }
 364:             $key++;
 365:         }
 366:     }
 367: }

 369: /**
 370:  * データベースに表示用フラグを立てる:同じ月
 371:  * @param  array  $caldb データベース
 372:  * @param  int    $ti    マークしたい時刻(省略時は現在時刻)
 373:  * @return    なし
 374: */
 375: function checkDB_month(&$caldb, $ti=0) {
 376:     if ($ti == 0)   $ti = time();   //省略時
 377:     $t1 = date('m', $ti);
 378: 
 379:     $flag = (-2);
 380:     $key = 0;
 381:     foreach ($caldb as $rec) {
 382:         if (isset($rec['dt'])) {
 383:             $t0 = date($rec['fmt'], $ti);
 384:             if ($rec['dt']  <$t0) {
 385:                 $caldb[$key]['flag'] = $flag;
 386:             } else if ($flag == (-2)) {
 387:                 $caldb[$key]['flag'] = 0;
 388:                 $flag = (+2);
 389:             } else {
 390:                 $caldb[$key]['flag'] = $flag;
 391:             }
 392:             if (preg_match('/(^\d{2})/iu', $rec['dt'], $arr> 0) {
 393:                 if (($arr[1] == $t1&& ($caldb[$key]['flag']) !0) {
 394:                     $caldb[$key]['flag'] = (-1);
 395:                 }
 396:             }
 397:             $key++;
 398:         }
 399:     }
 400: }

 402: /**
 403:  * データベースに表示用フラグを立てる:「今ここ」と前後2件ずつ
 404:  * @param  array  $caldb データベース
 405:  * @param  int    $ti    マークしたい時刻(省略時は現在時刻)
 406:  * @return    なし
 407: */
 408: function checkDB_now(&$caldb, $ti=0) {
 409:     if ($ti == 0)   $ti = time();   //省略時
 410: 
 411:     $flag = (-2);
 412:     $key = 0;
 413:     foreach ($caldb as $rec) {
 414:         if (isset($rec['dt'])) {
 415:             $t0 = date($rec['fmt'], $ti);
 416:             if ($rec['dt']  <$t0) {
 417:                 $caldb[$key]['flag'] = $flag;
 418:                 $key++;
 419:             } else if ($flag == (-2)) {
 420:                 $flag++;
 421:                 for ($i = $key - 2$i  <$key$i++) {
 422:                     if (isset($caldb[$i]['label'])) $caldb[$i]['flag'] = $flag;
 423:                 }
 424:                 $flag++;
 425:                 $i = $key;
 426:                 if (isset($caldb[$i]['label'])) $caldb[$i]['flag'] = $flag;
 427:                 $flag++;
 428:                 $key++;
 429:                 $caldb[$key]['flag'] = $flag;
 430:                 $key++;
 431:                 if (isset($caldb[$key]['label']))   $caldb[$key]['flag'] = $flag;
 432:                 $key++;
 433:                 $flag++;
 434:             } else {
 435:                 $caldb[$key]['flag'] = $flag;
 436:                 $key++;
 437:             }
 438:         }
 439:     }
 440: }

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

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

 460: /**
 461:  * 宇宙カレンダーを作成:テーブル形式
 462:  * @param  array  $caldb データベース
 463:  * @param  array  $caption キャプション
 464:  * @return    string 表示用コンテンツ
 465: */
 466: function makeCosmicCalendar_table($caldb, $caption) {
 467:     $outstr =<<< EOT
 468: <table class="stripe">
 469: <caption>{$caption['label']}</caption>
 470: <tr><th>日時</th><th>イベント</th></tr>
 471: 
 472: EOT;
 473: 
 474:     foreach ($caldb as $rec) {
 475:         if ($rec['flag'>= (-1&& $rec['flag'] = (<+1)) {
 476:             $mark = ($rec['flag'] == 0? '&nbsp;&#x23F1;' : '';
 477:             $outstr .=<<< EOT
 478: <tr>
 479: <td>{$rec['dt']}$mark</td>
 480: <td>{$rec['label']}</td>
 481: </tr>
 482: 
 483: EOT;
 484:         }
 485:     }
 486:     $outstr .=<<< EOT
 487: </table>
 488: 
 489: EOT;
 490:     return $outstr;
 491: }

 442: /**
 443:  * 宇宙カレンダーを作成:テキスト形式
 444:  * @param  array  $caldb   データベース
 445:  * @param  array  $caption キャプション
 446:  * @return    string 表示用コンテンツ
 447: */
 448: function makeCosmicCalendar_text($caldb, $caption) {
 449:     $outstr = "<hr />■{$caption['label']}({$caption['description']})<br />\n";
 450:     foreach ($caldb as $rec) {
 451:         if ($rec['flag'>= (-1&& $rec['flag'] = (<+1)) {
 452:             $outstr .$rec['dt'. ' - ' . $rec['label'];
 453:             if ($rec['flag'] == 0)  $outstr .'←いまココ';
 454:             $outstr ."<br />\n";
 455:         }
 456:     }
 457:     return $outstr;
 458: }

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

参考サイト

(この項おわり)
header