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

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

(2023年10月15日)sigcalendar()のbug-fix, pahooInputData導入
(2022年12月30日)1月は時分まで表示するようにした,cosmicCalendar.xml更新
(2022年7月21日)バグ修正,cosmicCalendar.xml更新

目次

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

宇宙カレンダー

サンプル・プログラム

圧縮ファイルの内容
cosmicCalendar.phpサンプル・プログラム本体。
cosmicCalendar.xmlイベント・ファイル
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
pahooCalendar.php暦計算クラス pahooCalendar。
暦計算クラスの使い方は「PHPで日出没・月出没・月齢・潮を計算」を参照。include_path が通ったディレクトリに配置すること。
cosmicCalendar.php 更新履歴
バージョン 更新日 内容
1.6.0 2023/10/15 sigcalendar()のbug-fix, pahooInputData導入
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対応
pahooInputData.php 更新履歴
バージョン 更新日 内容
1.5.0 2024/01/28 exitIfExceedVersion() 追加
1.4.2 2024/01/28 exitIfLessVersion() メッセージ修正
1.4.1 2023/09/30 コメントの訂正
1.4.0 2023/09/09 $_GET, $_POST参照をfilter_input()関数に置換
1.3.0 2023/07/11 roundFloat() 追加
pahooCalendar.php 更新履歴
バージョン 更新日 内容
4.5.0 2024/03/17 ヒジュラ暦メソッドを追加
4.4.1 2024/03/17 getCabinetOfficeHolidayTable() -- bug-fix
4.4.0 2024/02/25 内閣府の祝日表を参照できるようにした
4.3.2 2023/02/11 getSolarTerm72() 表記改訂:水澤腹堅→水沢腹堅
4.3.1 2023/02/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>

準備:外部クラスなど

  53: //表示幅(単位:ピクセル)
  54: define('WIDTH', 600);
  55: 
  56: //イベント・ファイル
  57: define('FILE_COSMIC_CALENDAR', 'cosmicCalendar.xml');
  58: 
  59: //暦計算クラス
  60: require_once('pahooCalendar.php');

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

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

 249: /**
 250:  * イベント・ファイルを読み込み、ソートする
 251:  * @param   string $fname イベント・ファイル名
 252:  * @param   array  $caldb イベントを格納する配列
 253:  * @param   array  $caption キャプションを格納する配列
 254:  * @return  bool TRUE/FALSE
 255: */
 256: function readDB($fname, &$caldb, &$caption) {
 257:     $pcl = new pahooCalendar();     //pahooCalendarクラス
 258:     $pcl->setLanguage('jp');
 259: 
 260:     $jd_end = $pcl->Gregorian2JD(date('Y'+ 1, 1, 1, 0, 0, 0); //今年の最後
 261:     $dd = 365 + date('L');                                  //1年の日数
 262:     $res = FALSE;
 263: 
 264:     //イベント・ファイル読み込み
 265:     if (file_exists($fname)) {
 266:         $xml = simplexml_load_file($fname);
 267:         //キャプション
 268:         if (isset($xml->caption->label)) {
 269:             $caption['label'] = isset($xml->caption->label? (string)$xml->caption->label : '宇宙カレンダー';
 270:             $caption['description'] = isset($xml->caption->description? (string)$xml->caption->description : '宇宙誕生から現在までを1年に圧縮';
 271:         }
 272:         //開始
 273:         if (isset($xml->start->before)) {
 274:             $start = (double)$xml->start->before;
 275:             $unit = ($dd * 24 * 60 * 60) / $start;      //1秒当たり年数
 276:             $cnt = 0;
 277:             $caldb[$cnt]['label'] = (string)$xml->start->label;
 278:             $ss = $start * $unit;
 279:             list($caldb[$cnt]['dt'], $caldb[$cnt]['fmt']) = sigcalendar($ss);
 280:             $cnt++;
 281:         }
 282:         //イベント読み込み
 283:         foreach ($xml->event as $event) {
 284:             $caldb[$cnt]['label'] = (string)$event->label;
 285:             if (isset($event->before)) {
 286:                 $ss = ($start - (double)$event->before* $unit;
 287:             } else if (isset($event->after)) {
 288:                 $ss = (double)$event->after * $unit;
 289:             } else if (isset($event->dt)) {
 290:                 sscanf((string)$event->dt, '%d/%d/%d %d:%d:%f', $year, $month, $day, $hour, $min, $sec);
 291:                 $jd = $pcl->Gregorian2JD($year, $month, $day, $hour, $min, $sec);
 292:                 $ss = ($start - ($jd_end - $jd) / $dd* $unit;
 293:             } else {
 294:                 $ss = 0;
 295:             }
 296:             //カレンダー計算
 297:             list($caldb[$cnt]['dt'], $caldb[$cnt]['fmt']) = sigcalendar($ss);
 298:             $cnt++;
 299:         }
 300:         $res = TRUE;
 301:     }
 302:     $pcl = NULL;
 303: 
 304:     //ソート
 305:     usort($caldb, function ($a, $b) {
 306:         if (! isset($a['dt']))  return NULL;
 307:         if (! isset($b['dt']))  return NULL;
 308:         return $a['dt'> $b['dt'? (+1: (-1);
 309:     });
 310: 
 311:     return $res;
 312: }

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

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

解説:カレンダー計算

 196: /**
 197:  * カレンダー計算
 198:  * @param   double $ss 秒数
 199:  * @return  array(カレンダー,フォーマット)
 200: */
 201: function sigcalendar($ss) {
 202:     $yyyy = date('Y');      //今年の西暦年
 203:     $tt = (int)strtotime($yyyy .'/1/1 0:0:0'+ (int)$ss;
 204:     $dt = date('m/d H:i:s', $tt);
 205: 
 206:     //01/01 00:00:01以前→ミリ秒まで
 207:     if (preg_match('/^01\/01 01:01:01/', $dt> 0) {
 208:         $fmt = 'm/d H:i:s';
 209:         $dt = date($fmt, $tt);
 210:         $dt .sprintf('.%03d', (($ss - floor($ss)) * 1000));
 211:     //01/01 4時以前→秒まで
 212:     } else if (preg_match('/^12\/31 0[0-4]./', $dt> 0) {
 213:         $fmt = 'm/d H:i:s';
 214:         $dt = date($fmt, $tt);
 215:     //01/01→分まで
 216:     } else if (preg_match('/^01\/01/', $dt> 0) {
 217:         $fmt = 'm/d H:i';
 218:         $dt = date($fmt, $tt);
 219:     //1月→時まで
 220:     } else if (preg_match('/^01/', $dt> 0) {
 221:         $fmt = 'm/d H:00';
 222:         $dt = date($fmt, $tt);
 223:     //12/31 23:59:59以降→ミリ秒まで
 224:     } else if (preg_match('/^12\/31 23:59:59/', $dt> 0) {
 225:         $fmt = 'm/d H:i:s';
 226:         $dt = date($fmt, $tt);
 227:         $dt .sprintf('.%03d', (($ss - floor($ss)) * 1000));
 228:     //12/31 20時以降→秒まで
 229:     } else if (preg_match('/^12\/31 2/', $dt> 0) {
 230:         $fmt = 'm/d H:i:s';
 231:         $dt = date($fmt, $tt);
 232:     //12/31→分まで
 233:     } else if (preg_match('/^12\/31/', $dt> 0) {
 234:         $fmt = 'm/d H:i';
 235:         $dt = date($fmt, $tt);
 236:     //12月→時まで
 237:     } else if (preg_match('/^12/', $dt> 0) {
 238:         $fmt = 'm/d H:00';
 239:         $dt = date($fmt, $tt);
 240:     //それ以前→日まで
 241:     } else {
 242:         $fmt = 'm/d';
 243:         $dt = date($fmt, $tt);
 244:     }
 245: 
 246:     return array($dt, $fmt);
 247: }

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

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

 314: /**
 315:  * データベースに表示用フラグを立てる:全部
 316:  * @param   array  $caldb データベース
 317:  * @param   int    $ti    マークしたい時刻(省略時は現在時刻)
 318:  * @return  なし
 319: */
 320: function checkDB_all(&$caldb, $ti=0) {
 321:     if ($ti == 0)   $ti = time();   //省略時
 322: 
 323:     $key = 0;
 324:     $flag = (-1);
 325:     foreach ($caldb as $rec) {
 326:         if (isset($rec['dt'])) {
 327:             $t0 = date($rec['fmt'], $ti);
 328:             if ($rec['dt'< $t0) {
 329:                 $caldb[$key]['flag'] = $flag;
 330:             } else if ($flag == (-1)) {
 331:                 $flag++;
 332:                 $caldb[$key]['flag'] = $flag;
 333:                 $flag++;
 334:             } else {
 335:                 $caldb[$key]['flag'] = $flag;;
 336:             }
 337:             $key++;
 338:         }
 339:     }
 340: }

 342: /**
 343:  * データベースに表示用フラグを立てる:同じ月
 344:  * @param   array  $caldb データベース
 345:  * @param   int    $ti    マークしたい時刻(省略時は現在時刻)
 346:  * @return  なし
 347: */
 348: function checkDB_month(&$caldb, $ti=0) {
 349:     if ($ti == 0)   $ti = time();   //省略時
 350:     $t1 = date('m', $ti);
 351: 
 352:     $flag = (-2);
 353:     $key = 0;
 354:     foreach ($caldb as $rec) {
 355:         if (isset($rec['dt'])) {
 356:             $t0 = date($rec['fmt'], $ti);
 357:             if ($rec['dt'< $t0) {
 358:                 $caldb[$key]['flag'] = $flag;
 359:             } else if ($flag == (-2)) {
 360:                 $caldb[$key]['flag'] = 0;
 361:                 $flag = (+2);
 362:             } else {
 363:                 $caldb[$key]['flag'] = $flag;
 364:             }
 365:             if (preg_match('/(^\d{2})/iu', $rec['dt'], $arr> 0) {
 366:                 if (($arr[1] == $t1&& ($caldb[$key]['flag']) !0) {
 367:                     $caldb[$key]['flag'] = (-1);
 368:                 }
 369:             }
 370:             $key++;
 371:         }
 372:     }
 373: }

 375: /**
 376:  * データベースに表示用フラグを立てる:「今ここ」と前後2件ずつ
 377:  * @param   array  $caldb データベース
 378:  * @param   int    $ti    マークしたい時刻(省略時は現在時刻)
 379:  * @return  なし
 380: */
 381: function checkDB_now(&$caldb, $ti=0) {
 382:     if ($ti == 0)   $ti = time();   //省略時
 383: 
 384:     $flag = (-2);
 385:     $key = 0;
 386:     foreach ($caldb as $rec) {
 387:         if (isset($rec['dt'])) {
 388:             $t0 = date($rec['fmt'], $ti);
 389:             if ($rec['dt'< $t0) {
 390:                 $caldb[$key]['flag'] = $flag;
 391:                 $key++;
 392:             } else if ($flag == (-2)) {
 393:                 $flag++;
 394:                 for ($i = $key - 2$i < $key$i++) {
 395:                     if (isset($caldb[$i]['label'])) $caldb[$i]['flag'] = $flag;
 396:                 }
 397:                 $flag++;
 398:                 $i = $key;
 399:                 if (isset($caldb[$i]['label'])) $caldb[$i]['flag'] = $flag;
 400:                 $flag++;
 401:                 $key++;
 402:                 $caldb[$key]['flag'] = $flag;
 403:                 $key++;
 404:                 if (isset($caldb[$key]['label']))   $caldb[$key]['flag'] = $flag;
 405:                 $key++;
 406:                 $flag++;
 407:             } else {
 408:                 $caldb[$key]['flag'] = $flag;
 409:                 $key++;
 410:             }
 411:         }
 412:     }
 413: }

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

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

 433: /**
 434:  * 宇宙カレンダーを作成:テーブル形式
 435:  * @param   array  $caldb データベース
 436:  * @param   array  $caption キャプション
 437:  * @return  string 表示用コンテンツ
 438: */
 439: function makeCosmicCalendar_table($caldb, $caption) {
 440:     $outstr =<<< EOT
 441: <table class="stripe">
 442: <caption>{$caption['label']}</caption>
 443: <tr><th>日時</th><th>イベント</th></tr>
 444: 
 445: EOT;
 446: 
 447:     foreach ($caldb as $rec) {
 448:         if ($rec['flag'>= (-1&& $rec['flag'<= (+1)) {
 449:             $mark = ($rec['flag'] == 0? '&nbsp;&#x23F1;' : '';
 450:             $outstr .=<<< EOT
 451: <tr>
 452: <td>{$rec['dt']}$mark</td>
 453: <td>{$rec['label']}</td>
 454: </tr>
 455: 
 456: EOT;
 457:         }
 458:     }
 459:     $outstr .=<<< EOT
 460: </table>
 461: 
 462: EOT;
 463:     return $outstr;
 464: }

 415: /**
 416:  * 宇宙カレンダーを作成:テキスト形式
 417:  * @param   array  $caldb   データベース
 418:  * @param   array  $caption キャプション
 419:  * @return  string 表示用コンテンツ
 420: */
 421: function makeCosmicCalendar_text($caldb, $caption) {
 422:     $outstr = "<hr />■{$caption['label']}({$caption['description']})<br />\n";
 423:     foreach ($caldb as $rec) {
 424:         if ($rec['flag'>= (-1&& $rec['flag'<= (+1)) {
 425:             $outstr .$rec['dt'. ' - ' . $rec['label'];
 426:             if ($rec['flag'] == 0)  $outstr .'←いまココ';
 427:             $outstr ."<br />\n";
 428:         }
 429:     }
 430:     return $outstr;
 431: }

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

参考サイト

(この項おわり)
header