PHPで3ヶ月カレンダーを作る

(1/1)
PHP を使って、今月から再来月までの 3 ヶ月カレンダーを作る。
PHP で祝日を求める」「PHP で二十四節気一覧を作成」に、七十二候や旧暦、六曜、干支の計算を加えた。

(2021 年 5 月 8 日)PHP8 対応,リファラ・チェック改良

目次

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

PHPで3ヶ月カレンダーを作る

サンプル・プログラム

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

解説:旧暦の計算

1499: /**
1500:  * グレゴオリオ暦=旧暦テーブル 作成
1501:  * @param int $year 西暦年
1502:  * @return double 太陽の黄経(視黄経)
1503: */
1504: function makeLunarCalendar($year) {
1505:     //旧暦の2033年問題により、2033年以降はエラーフラグを立てる
1506:     if ($year >= 2033) {
1507:         $this->error = TRUE;
1508:         $this->errmsg = '2033年以降は正しい旧暦計算ができません';
1509:     }
1510: 
1511:     unset($this->tblmoon);
1512:     $this->tblmoon = array();
1513: 
1514:     //前年の冬至を求める
1515:     for ($day = 1; $day <= 31; $day++) {
1516:         $lsun = $this->longitude_sun($year - 1, 12, $day, 0, 0, 0);
1517:         if (floor($lsun / 15.0) > 17)    break;
1518:     }
1519:     $d1 = $day - 1;   //冬至
1520: 
1521:     //翌年の雨水を求める
1522:     for ($day = 1; $day <= 31; $day++) {
1523:         $lsun = $this->longitude_sun($year + 1, 2, $day, 0, 0, 0);
1524:         if (floor($lsun / 15.0) > 22)    break;
1525:     }
1526:     $d2 = $day - 1;   //雨水
1527: 
1528:     //朔の日を求める
1529:     $cnt = 0;
1530:     $dd = $d1;
1531:     $mm = 12;
1532:     $yy = $year - 1;
1533:     while ($yy <= $year + 1) {
1534:         $dm = $this->getDaysInMonth($yy$mm);
1535:         while ($dd <= $dm) {
1536:             $age1 = $this->moon_age($yy$mm$dd,  0 - $this->TDIFF, 0, 0);    //Ver.3.11 bug-fix
1537:             $age2 = $this->moon_age($yy$mm$dd, 23 - $this->TDIFF, 59, 59);  //Ver.3.11 bug-fix
1538:             if ($age2 <= $age1) {
1539:                 $this->tblmoon[$cnt]['year']  = $yy;
1540:                 $this->tblmoon[$cnt]['month'] = $mm;
1541:                 $this->tblmoon[$cnt]['day']   = $dd;
1542:                 $this->tblmoon[$cnt]['age']   = $age1;
1543:                 $this->tblmoon[$cnt]['jd']    = $this->Gregorian2JD($yy$mm$dd, 0, 0, 0);
1544:                 $cnt++;
1545:             }
1546:             $dd++;
1547:         }
1548:         $mm++;
1549:         $dd = 1;
1550:         if ($mm > 12) {
1551:             $yy++;
1552:             $mm = 1;
1553:         }
1554:     }
1555: 
1556:     //二十四節気(中)を求める
1557:     $tblsun = array();
1558:     $cnt = 0;
1559:     $dd = $d1;
1560:     $mm = 12;
1561:     $yy = $year - 1;
1562:     while ($yy <= $year + 1) {
1563:         $dm = $this->getDaysInMonth($yy$mm);
1564:         while ($dd <= $dm) {
1565:             $l1 = $this->longitude_sun($yy$mm$dd,  0, 0, 0);
1566:             $l2 = $this->longitude_sun($yy$mm$dd, 24, 0, 0);
1567:             $n1 = floor($l1 / 15.0);
1568:             $n2 = floor($l2 / 15.0);
1569:             if (($n2 != $n1) && ($n2 % 2 == 0)) {
1570:                 $tblsun[$cnt]['jd'] = $this->Gregorian2JD($yy$mm$dd, 0, 0, 0);
1571:                 $oldmonth = floor($n2 / 2) + 2;
1572:                 if ($oldmonth > 12) $oldmonth -= 12;
1573:                 $tblsun[$cnt]['oldmonth']  = $oldmonth;
1574:                 $cnt++;
1575:             }
1576:             $dd++;
1577:         }
1578:         $mm++;
1579:         $dd = 1;
1580:         if ($mm > 12) {
1581:             $yy++;
1582:             $mm = 1;
1583:         }
1584:     }
1585: 
1586:     //月の名前を決める
1587:     $n1 = count($this->tblmoon);
1588:     $n2 = count($tblsun);
1589:     for ($i = 0; $i < $n1 - 1; $i++) {
1590:         for ($j = 0; $j < $n2$j++) {
1591:             if (($this->tblmoon[$i]['jd'] <= $tblsun[$j]['jd'])
1592:                 && ($this->tblmoon[$i + 1]['jd'] > $tblsun[$j]['jd'])) {
1593:                 $this->tblmoon[$i]['oldmonth'] = $tblsun[$j]['oldmonth'];
1594:                 $this->tblmoon[$i]['oldleap']  = FALSE;
1595:                 $this->tblmoon[$i + 1]['oldmonth'] = $tblsun[$j]['oldmonth'];
1596:                 $this->tblmoon[$i + 1]['oldleap']  = TRUE;
1597:                 break;
1598:             }
1599:         }
1600:     }
1601: }

グレゴリオ暦から旧暦を求める手順は、「旧暦と六曜を作りましょう」を参考にした。

変換のための方程式を導出できないため、あらかじめ計算したい西暦年から変換テーブルを作る必要がある。これを行うのがユーザー関数 makeLunarCalendar である。
  1. 前年の冬至を求める。
  2. 翌年の雨水を求める。
  3. 1~2 の期間中の二十四節気(中)を配列 $tblsun に格納する。
  4. 朔日と中を比較して、月の名前と閏月を決めてゆく。

1603: /**
1604:  * 旧暦を求める
1605:  * @param int $year  西暦年
1606:  * @param int $month 月
1607:  * @param int $day   日
1608:  * @return array(旧暦月,日,閏月フラグ)/FALSE:旧暦計算不能
1609: */
1610: function Gregorian2Lunar($year$month$day) {
1611:     //2033年問題チェック
1612:     if ($this->error)    return  FALSE;
1613: 
1614:     $jd = $this->Gregorian2JD($year$month$day, 0, 0, 0);
1615:     $str = '';
1616:     $n1 = count($this->tblmoon);
1617:     for ($i = 0; $i < $n1 - 1; $i++) {
1618:         if ($jd < $this->tblmoon[$i + 1]['jd']) {
1619:             $day = floor($jd - $this->tblmoon[$i]['jd']) + 1;
1620:             $items = array($this->tblmoon[$i]['oldmonth'], $day$this->tblmoon[$i]['oldleap']);
1621:             break;
1622:         }
1623:     }
1624:     return $items;
1625: }

変換テーブルが用意できたら、ユーザー関数 Gregorian2Lunar を使って旧暦を求める。

この旧暦計算法は天保暦に基づいているが、天保暦には 2033 年問題がある。西暦 2033 年(令和 15 年)10 月は旧暦 9 月だが、11 月に入ると旧暦 11 月と、10 月が無くなってしまう問題である。
これは、1844 年(天保 14 年)に天保暦が導入されてから初めて起きる事態だが、天保暦は 1872 年(明治 5 年)に廃止されているために正式な解決法は用意されていない。
そこで、2033 年(令和 15 年)以降の旧暦計算はスキップするようにした。

解説:干支と十二支の計算

1646: /**
1647:  * 干支を求める(下請け関数)
1648:  * @param int $a1 十干の基準値
1649:  * @param int $a2 十二支の基準値
1650:  * @param int $n  計算したい値
1651:  * @return string 干支
1652: */
1653: function __eto($a1$a2$n) {
1654: //十干
1655: static $table1 = array(
1656:  0 =>'',
1657:  1 =>'',
1658:  2 =>'',
1659:  3 =>'',
1660:  4 =>'',
1661:  5 =>'',
1662:  6 =>'',
1663:  7 =>'',
1664:  8 =>'',
1665:  9 =>''
1666: );
1667: 
1668: //十二支
1669: static $table2 = array(
1670:  0 =>'',
1671:  1 =>'',
1672:  2 =>'',
1673:  3 =>'',
1674:  4 =>'',
1675:  5 =>'',
1676:  6 =>'',
1677:  7 =>'',
1678:  8 =>'',
1679:  9 =>'',
1680: 10 =>'',
1681: 11 =>''
1682: );
1683: 
1684:     return $table1[abs($n - $a1) % 10] . $table2[abs($n - $a2) % 12];
1685: }

干支には、年の干支月の干支日の干支 の 3種類がある。
干支は、甲・乙・丙・丁・戊・己・庚・辛・壬・癸の 10要素からなる十干 (じっかん) と、子・丑・寅・卯・辰・巳・午・未・申・酉・戌・亥の 12要素からなる十二支 (じゅうにし) の組み合わせからなる。
つまり、十干は年/月/日が 10 回ごとに繰り返され、十二支は年/月/日が 12 回ごとに繰り返されることになる。これを計算するのが __eto である。

次に、基準となる年月日を調べておく。
甲の年は西暦 1904 年(明治 37 年)、子の年は西暦 1900 年(明治 33 年)である。年の干支は、この 2 つを基準に計算する。
甲子の月は 1903 年(明治 36 年)11 月である。月の干支は、ここを基準に計算する。
甲子の日は 1902 年(明治 35 年)4 月 11 日である。日の干支は、ここを基準に計算する。

ちなみに、干支も十二支も古代中国で考え出されたものだが、十二支の方は、木星が黄道を 1 周するのがほぼ 12 年(公転周期 11.86 年)ということに対応している。古代中国では木星を暦に関わる重要な天体と位置づけており、歳星 (さいせい) という名で呼ばれていた。

解説:カレンダー作成

0181: /**
0182:  * 1ヶ月分のカレンダーを作成
0183:  * @param object $pcl  pahooCalendarオブジェクト
0184:  * @param int $start 週の開始曜日(0:日曜日, 1:月曜日...6:土曜日)
0185:  * @param int $year  西暦年
0186:  * @param int $month 月
0187:  * @return string HTMLコンテンツ/FALSE:エラー
0188: */
0189: function makeCalendar($pcl$start$year$month) {
0190:     if ($month < 1 && $month > 12)      return FALSE;
0191: 
0192:     $eto_year  = $pcl->eto_year($year);
0193:     $eto_month = $pcl->eto_month($year$month);
0194: 
0195: $html =<<< EOT
0196: <table class="calendar">
0197: <tr>
0198: <th colspan="7"><span class="large">{$year}年({$eto_year}) {$month}月({$eto_month})</span></th>
0199: </tr>
0200: <tr>
0201: 
0202: EOT;
0203: 
0204:     //曜日の行
0205:     for ($i = 0; $i < 7; $i++) {
0206:         $n  = ($start + $i) % 7;
0207:         if ($n == 6)        $class = 'blue';
0208:         else if ($n == 0)   $class = 'red';
0209:         else                $class = 'black';
0210:         $str = $pcl->__getWeekString($n);
0211:         $html .= "<th><span class=\"{$class}\">{$str}</span></th>";
0212:     }
0213:     $html .= "</tr>\n";
0214: 
0215:     //カレンダー本体
0216:     $wn1 = $pcl->getWeekNumber($year$month, 1); //月の最初の曜日
0217:     $dim = $pcl->getDaysInMonth($year$month);       //月の最後の日
0218:     $cnt = 0;
0219:     $flag = FALSE;
0220:     $n = $start;
0221:     $day = 1;
0222:     while (1) {
0223:         if ($cnt % 7 == 0)      $html .= "<tr>\n";
0224:         //曜日の色
0225:         if ($n % 7 == 6)        $class = 'blue';
0226:         else if ($n % 7 == 0)   $class = 'red';
0227:         else                    $class = 'black';
0228:         if ($n % 7 == $wn1$flag = TRUE;
0229:         //表示開始
0230:         if ($flag) {
0231:             //祝日
0232:             $holiday = $pcl->getHoliday($year$month$day);
0233:             if ($holiday != '') $class = 'red';
0234:             $holiday = ($holiday == FALSE) ? '<br />' :
0235:                 "<span class=\"small red\">{$holiday}</span><br />";
0236:             //二十四節気
0237:             $solarterm = $pcl->getSolarTerm($year$month$day);
0238:             $solarterm = ($solarterm == '') ? '<br />' :
0239:                 "<span class=\"small\">{$solarterm}</span><br />";
0240:             //七十二候
0241:             $solarterm72 = $pcl->getSolarTerm72($year$month$day);
0242:             $solarterm72 = ($solarterm72 == '') ? '<br />' :
0243:                 "<span class=\"small\">{$solarterm72}</span><br />";
0244:             //旧暦
0245:             list($oldmonth$oldday$oldleap) = $pcl->Gregorian2Lunar($year$month$day);
0246:             if (!$pcl->error) {
0247:                 $oldleap = $oldleap ? '' : '';
0248:                 $rokuyou = $pcl->rokuyou($oldmonth$oldday);
0249:                 $oldcal = sprintf('%s%d月%d日<br />(%s)', $oldleap$oldmonth$oldday$rokuyou);
0250:             //2033年問題回避
0251:             } else {
0252:                 $oldcal = '';
0253:             }
0254:             //日の干支
0255:             $eto_day = $pcl->eto_day($year$month$day);
0256:             //表示
0257: $html .=<<< EOT
0258: <td>
0259: <span class="large {$class}">{$day}</span><br />
0260: {$holiday}
0261: {$solarterm}
0262: {$solarterm72}
0263: <span class="small">{$eto_day}<br /></span>
0264: <span class="small">{$oldcal}</span>
0265: </td>
0266: 
0267: EOT;
0268:             $day++;
0269:             if ($day > $dim)    break;
0270:         } else {
0271:             $html .= "<td>&nbsp;</td>";
0272:         }
0273:         $cnt++;
0274:         $n++;
0275:         if ($cnt % 7 == 0)  $html .= "</tr>\n";
0276:     }
0277:     //最後の日以降の処理
0278:     $cnt++;
0279:     while ($cnt % 7 != 0) {
0280:         $html .= "<td>&nbsp;</td>";
0281:         $cnt++;
0282:         if ($cnt % 7 == 0)  $html .= "</tr>\n";
0283:     }
0284: 
0285:     $html .= "</table>\n";
0286: 
0287:     return $html;
0288: }

ユーザー関数 makeCalendar は 1 ヶ月分のカレンダーを作成する。
週の開始曜日を自由に設定できるようにするための工夫をしている。

カレンダー本体を作成する処理では、まず、月の最初の曜日 $wn1 と、月の最後の日 $dim を計算しておく。
$wn1 と週の開始曜日 $start が一致するまでは変数 $flag が FALSE のままで、この時は日付を入れずに空のままにしておく。
$flag が TRUE になったら日付を入れてゆく。あわせて、二十四節気、旧暦の計算を行う。

$dim に達したらループを抜け出し、行末まで空白のセルを追加してゆく。

質疑応答

【質問】

「PHP で 3 ヶ月カレンダーを作る」のページのソースなのですが「サンプル・プログラムの解説:カレンダー作成」のソースの 0171行目に「<tr>」の開始タグを入れたほうがいいと思います。曜日部分の行部分 TR の開始タグが無いようです。
間違えであればすいません。


【回答】

ご指摘ありがとうございます。<tr> タグが抜けていました。追加しました。

活用例

二十四節気・七十二候・干支・旧暦・六曜カレンダー:みんなの知識 ちょっと便利帳」では、このサンプル・プログラムを活用している。また、「歴代天皇の誕生日が祝日に!?」というユニークなサービスも提供している。ありがとうございます。

参考サイト

(この項おわり)
header