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

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

(2021 年 8 月 8 日)土用の入り,丑の日を追加
(2021 年 5 月 8 日)PHP8 対応,リファラ・チェック改良

目次

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

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

サンプル・プログラム

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

解説:旧暦の計算

1583: /**
1584:  * グレゴオリオ暦=旧暦テーブル 作成
1585:  * @param   int $year 西暦年
1586:  * @return  double 太陽の黄経(視黄経)
1587: */
1588: function makeLunarCalendar($year) {
1589:     //旧暦の2033年問題により、2033年以降はエラーフラグを立てる
1590:     if ($year >= 2033) {
1591:         $this->error = TRUE;
1592:         $this->errmsg = '2033年以降は正しい旧暦計算ができません';
1593:     }
1594: 
1595:     unset($this->tblmoon);
1596:     $this->tblmoon = array();
1597: 
1598:     //前年の冬至を求める
1599:     for ($day = 1; $day <= 31; $day++) {
1600:         $lsun = $this->longitude_sun($year - 1, 12, $day, 0, 0, 0);
1601:         if (floor($lsun / 15.0) > 17)    break;
1602:     }
1603:     $d1 = $day - 1;   //冬至
1604: 
1605:     //翌年の雨水を求める
1606:     for ($day = 1; $day <= 31; $day++) {
1607:         $lsun = $this->longitude_sun($year + 1, 2, $day, 0, 0, 0);
1608:         if (floor($lsun / 15.0) > 22)    break;
1609:     }
1610:     $d2 = $day - 1;   //雨水
1611: 
1612:     //朔の日を求める
1613:     $cnt = 0;
1614:     $dd = $d1;
1615:     $mm = 12;
1616:     $yy = $year - 1;
1617:     while ($yy <= $year + 1) {
1618:         $dm = $this->getDaysInMonth($yy$mm);
1619:         while ($dd <= $dm) {
1620:             $age1 = $this->moon_age($yy$mm$dd,  0 - $this->TDIFF, 0, 0);    //Ver.3.11 bug-fix
1621:             $age2 = $this->moon_age($yy$mm$dd, 23 - $this->TDIFF, 59, 59);  //Ver.3.11 bug-fix
1622:             if ($age2 <= $age1) {
1623:                 $this->tblmoon[$cnt]['year']  = $yy;
1624:                 $this->tblmoon[$cnt]['month'] = $mm;
1625:                 $this->tblmoon[$cnt]['day']   = $dd;
1626:                 $this->tblmoon[$cnt]['age']   = $age1;
1627:                 $this->tblmoon[$cnt]['jd']    = $this->Gregorian2JD($yy$mm$dd, 0, 0, 0);
1628:                 $cnt++;
1629:             }
1630:             $dd++;
1631:         }
1632:         $mm++;
1633:         $dd = 1;
1634:         if ($mm > 12) {
1635:             $yy++;
1636:             $mm = 1;
1637:         }
1638:     }
1639: 
1640:     //二十四節気(中)を求める
1641:     $tblsun = array();
1642:     $cnt = 0;
1643:     $dd = $d1;
1644:     $mm = 12;
1645:     $yy = $year - 1;
1646:     while ($yy <= $year + 1) {
1647:         $dm = $this->getDaysInMonth($yy$mm);
1648:         while ($dd <= $dm) {
1649:             $l1 = $this->longitude_sun($yy$mm$dd,  0, 0, 0);
1650:             $l2 = $this->longitude_sun($yy$mm$dd, 24, 0, 0);
1651:             $n1 = floor($l1 / 15.0);
1652:             $n2 = floor($l2 / 15.0);
1653:             if (($n2 != $n1) && ($n2 % 2 == 0)) {
1654:                 $tblsun[$cnt]['jd'] = $this->Gregorian2JD($yy$mm$dd, 0, 0, 0);
1655:                 $oldmonth = floor($n2 / 2) + 2;
1656:                 if ($oldmonth > 12) $oldmonth -= 12;
1657:                 $tblsun[$cnt]['oldmonth']  = $oldmonth;
1658:                 $cnt++;
1659:             }
1660:             $dd++;
1661:         }
1662:         $mm++;
1663:         $dd = 1;
1664:         if ($mm > 12) {
1665:             $yy++;
1666:             $mm = 1;
1667:         }
1668:     }
1669: 
1670:     //月の名前を決める
1671:     $n1 = count($this->tblmoon);
1672:     $n2 = count($tblsun);
1673:     for ($i = 0; $i < $n1 - 1; $i++) {
1674:         for ($j = 0; $j < $n2$j++) {
1675:             if (($this->tblmoon[$i]['jd'] <= $tblsun[$j]['jd'])
1676:                 && ($this->tblmoon[$i + 1]['jd'] > $tblsun[$j]['jd'])) {
1677:                 $this->tblmoon[$i]['oldmonth'] = $tblsun[$j]['oldmonth'];
1678:                 $this->tblmoon[$i]['oldleap']  = FALSE;
1679:                 $this->tblmoon[$i + 1]['oldmonth'] = $tblsun[$j]['oldmonth'];
1680:                 $this->tblmoon[$i + 1]['oldleap']  = TRUE;
1681:                 break;
1682:             }
1683:         }
1684:     }
1685: }

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

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

1687: /**
1688:  * 旧暦を求める
1689:  * @param   int $year  西暦年
1690:  * @param   int $month 月
1691:  * @param   int $day   日
1692:  * @return  array(旧暦月,日,閏月フラグ)/FALSE:旧暦計算不能
1693: */
1694: function Gregorian2Lunar($year$month$day) {
1695:     //2033年問題チェック
1696:     if ($this->error)    return  FALSE;
1697: 
1698:     $jd = $this->Gregorian2JD($year$month$day, 0, 0, 0);
1699:     $str = '';
1700:     $n1 = count($this->tblmoon);
1701:     for ($i = 0; $i < $n1 - 1; $i++) {
1702:         if ($jd < $this->tblmoon[$i + 1]['jd']) {
1703:             $day = floor($jd - $this->tblmoon[$i]['jd']) + 1;
1704:             $items = array($this->tblmoon[$i]['oldmonth'], $day$this->tblmoon[$i]['oldleap']);
1705:             break;
1706:         }
1707:     }
1708:     return $items;
1709: }

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

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

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

1730: /**
1731:  * 干支を求める(下請け関数)
1732:  * @param   int $a1 十干の基準値
1733:  * @param   int $a2 十二支の基準値
1734:  * @param   int $n  計算したい値
1735:  * @return  string 干支
1736: */
1737: function __eto($a1$a2$n) {
1738: //十干
1739: static $table1 = array(
1740:  0 =>'',
1741:  1 =>'',
1742:  2 =>'',
1743:  3 =>'',
1744:  4 =>'',
1745:  5 =>'',
1746:  6 =>'',
1747:  7 =>'',
1748:  8 =>'',
1749:  9 =>''
1750: );
1751: 
1752: //十二支
1753: static $table2 = array(
1754:  0 =>'',
1755:  1 =>'',
1756:  2 =>'',
1757:  3 =>'',
1758:  4 =>'',
1759:  5 =>'',
1760:  6 =>'',
1761:  7 =>'',
1762:  8 =>'',
1763:  9 =>'',
1764: 10 =>'',
1765: 11 =>''
1766: );
1767: 
1768:     return $table1[abs($n - $a1) % 10] . $table2[abs($n - $a2) % 12];
1769: }

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

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

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

解説:土用の判定

0818: /**
0819:  * その日が土用かどうか
0820:  * @param   int $year, $month, $day  グレゴリオ暦による年月日
0821:  * @return  array(季節, 種類)
0822: */
0823: function isDoyo($year$month$day) {
0824:     static $table1 = array('' ,'', '', '');
0825:     static $table2 = array('in'=>'入り', 'ushi'=>'', 'out'=>'明け');
0826:     static $year0 = -1;           //西暦年保存用
0827:     static $doyo = array();      //土用情報保存用
0828: 
0829:     //土用情報を取得
0830:     if ($year0 != $year) {
0831:         $year0 = $year;
0832:         $doyo = $this->getDoyo($year0);
0833:     }
0834: 
0835:     //土用判定
0836:     foreach ($doyo as $key1=>$arr1) {
0837:         foreach ($arr1 as $key2=>$arr2) {
0838:             if (($arr2['year'] == $year0) && ($arr2['month'] == $month) && ($arr2['day'] == $day)) {
0839:                 return array($table1[$key1]$table2[$key2]);
0840:             }
0841:         }
0842:     }
0843:     return array('', '');
0844: }

土用の入り、明け、丑の日の求め方については、その日が土用かどうかを判定する isDoyo メソッドを用意した。

isDoyo メソッドは、まず、「PHP で土用を求める」で作った getDoyo メソッドを呼び出し、その年の土用に関する情報を static配列 $doyo にストックしておく。計算量を減らす目的で、次に呼び出されたときも同じ年なら、この配列 $doyo を参照する。

引数として与えられた年月日が土用の入り、明け、丑の日であるかどうか、この配列 $doyo をスキャンして判定する。

解説:カレンダー作成

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:             //土用
0239:             list($ss1$ss2) = $pcl->isDoyo($year$month$day);
0240:             if ($ss2 == '明け') {
0241:                 $ss2 = '';
0242:             } else if (($ss1 != '') && ($ss2 == '')) {
0243:                 $ss2 = '';
0244:             }
0245:             if ($ss1 != '' && $ss2 != '') {
0246:                 if ($solarterm != '') {
0247:                     $solarterm .= '<br />土用の' .  $ss2;
0248:                 } else {
0249:                     $solarterm = '土用の' . $ss2;
0250:                 }
0251:             }
0252:             $solarterm = ($solarterm == '') ? '<br />' :
0253:                 "<span class=\"small\">{$solarterm}</span><br />";
0254:             //七十二候
0255:             $solarterm72 = $pcl->getSolarTerm72($year$month$day);
0256:             $solarterm72 = ($solarterm72 == '') ? '<br />' :
0257:                 "<span class=\"small\">{$solarterm72}</span><br />";
0258:             //旧暦
0259:             list($oldmonth$oldday$oldleap) = $pcl->Gregorian2Lunar($year$month$day);
0260:             if (!$pcl->error) {
0261:                 $oldleap = $oldleap ? '' : '';
0262:                 $rokuyou = $pcl->rokuyou($oldmonth$oldday);
0263:                 $oldcal = sprintf('%s%d月%d日<br />(%s)', $oldleap$oldmonth$oldday$rokuyou);
0264:             //2033年問題回避
0265:             } else {
0266:                 $oldcal = '';
0267:             }
0268:             //日の干支
0269:             $eto_day = $pcl->eto_day($year$month$day);
0270:             //表示
0271: $html .=<<< EOT
0272: <td>
0273: <span class="large {$class}">{$day}</span><br />
0274: {$holiday}
0275: {$solarterm}
0276: {$solarterm72}
0277: <span class="small">{$eto_day}<br /></span>
0278: <span class="small">{$oldcal}</span>
0279: </td>
0280: 
0281: EOT;
0282:             $day++;
0283:             if ($day > $dim)    break;
0284:         } else {
0285:             $html .= "<td>&nbsp;</td>";
0286:         }
0287:         $cnt++;
0288:         $n++;
0289:         if ($cnt % 7 == 0)  $html .= "</tr>\n";
0290:     }
0291:     //最後の日以降の処理
0292:     $cnt++;
0293:     while ($cnt % 7 != 0) {
0294:         $html .= "<td>&nbsp;</td>";
0295:         $cnt++;
0296:         if ($cnt % 7 == 0)  $html .= "</tr>\n";
0297:     }
0298: 
0299:     $html .= "</table>\n";
0300: 
0301:     return $html;
0302: }

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

カレンダー本体を作成する処理では、まず、月の最初の曜日 $wn1 と、月の最後の日 $dim を計算しておく。
$wn1 と週の開始曜日 $start が一致するまでは変数 $flag が FALSE のままで、この時は日付を入れずに空のままにしておく。
$flag が TRUE になったら日付を入れてゆく。あわせて、二十四節気、旧暦の計算を行う。
土用の判定については、四季の土用の入りと丑の日のみを表示するようにしている。土用の明けは、立春、立夏、立秋、立冬の前日であるから、二十四節気の表示からわかるからだ。
$dim に達したらループを抜け出し、行末まで空白のセルを追加してゆく。

質疑応答

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

活用例

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

参考サイト

(この項おわり)
header