PHPで万年カレンダーを作る

(1/1)
万年カレンダー作るPHPプログラムを紹介する。
PHPで祝日を求める」「PHPで二十四節気一覧を作成」に、雑節や七十二候、旧暦、六曜、干支、一粒万倍日の計算を加えた。

(2022年6月5日)pahooInputData.php分離,カレンダー作成期間追加,タイトル変更
(2022年5月28日)旧暦2033年問題解決案を追加
(2022年5月1日)彼岸,社日,八十八夜,入梅,二百十日,二百二十日を追加
(2022年4月9日)一粒万倍日の基準となる節月の計算で、境界条件を修正
(2022年4月2日)一粒万倍日の基準となる節月の計算で、太陽黄経の小数点以下を切り上げるようにした
(2022年3月28日)一粒万倍日を追加,表示改良
(2021年8月8日)土用の入り,丑の日を追加
(2021年5月8日)PHP8対応,リファラ・チェック改良

目次

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

PHPで万年カレンダーを作る

サンプル・プログラム

圧縮ファイルの内容
tripleCalendar.phpサンプル・プログラム本体。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「PHPでGET/POSTでフォームから値を受け取る」「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
pahooCalendar.php暦計算クラス pahooCalendar。
暦計算クラスの使い方は「PHPで日出没・月出没・月齢・潮を計算」を参照。include_path が通ったディレクトリに配置すること。

準備

0039: //カレンダーの横幅(ピクセル)
0040: define('CALENDAR_WIDTH', 600);
0041: 
0042: //旧暦2033年問題解決案 0:解決しない,1:案1,2:案2,3:対応案3
0043: define('RESOLVE2033', 1);
0044: 
0045: //入力可能な西暦年
0046: define('YEAR_MIN', 2000);       //最小値【変更不可】
0047: define('YEAR_MAX', 2099);       //最大値【変更不可】
0048: 
0049: //入力可能な作成期間(ヶ月)
0050: define('PERIOD_MIN', 1);        //最小値【変更不可】
0051: define('PERIOD_MAX', 36);       //最大値【変更不可】
0052: define('PERIOD_DEF', 3);        //初期値
0053: 
0054: //データ入力に関わる関数群:include_pathが通ったディレクトリに配置
0055: require_once('pahooInputData.php');
0056: 
0057: //暦計算クラス:include_pathが通ったディレクトリに配置
0058: require_once('pahooCalendar.php');

各種定数は変更可能である。
後述する旧暦2033年問題解決案は定数 RESOLVE2033 に定義しておく。解決しないを選択すると、西暦2033年(令和15年)以降の旧暦は計算しなくなる。
データ入力に関わる関数群は別ファイル "pahooInputData.php" に分離しており、include_path が通ったディレクトリに配置すること。
また、暦計算クラス・ファイル "pahooCalendar.php" もinclude_pathが通ったディレクトリに配置すること。

解説:旧暦の計算

1806: /**
1807:  * グレゴオリオ暦=旧暦テーブル 作成
1808:  * @param   int $year   西暦年
1809:  * @param   int $method 2033年問題解決案
1810:  *              0:解決しない,1:案1,2:案2,3:案3(省略時:0)
1811:  * @return  なし
1812: */
1813: function makeLunarCalendar($year$method=0) {
1814:     $this->resolve2033 = $method;
1815: 
1816:     if (($method == 0) && ($year >= 2033)) {
1817:         $this->error = TRUE;
1818:         $this->errmsg = '2033年以降は正しい旧暦計算ができません';
1819:         return;
1820:     }
1821: 
1822:     unset($this->tblmoon);
1823:     $this->tblmoon = array();
1824: 
1825:     //前年の冬至を求める
1826:     for ($day = 1; $day <= 31; $day++) {
1827:         $lsun = $this->longitude_sun($year - 1, 12, $day, 0, 0, 0);
1828:         if (floor($lsun / 15.0) > 17)    break;
1829:     }
1830:     $d1 = $day - 1;       //冬至
1831: 
1832:     //翌年の雨水を求める
1833:     for ($day = 1; $day <= 31; $day++) {
1834:         $lsun = $this->longitude_sun($year + 1, 2, $day, 0, 0, 0);
1835:         if (floor($lsun / 15.0) > 22)    break;
1836:     }
1837:     $d2 = $day - 1;       //雨水
1838: 
1839:     //朔の日を求める
1840:     $cnt = 0;
1841:     $dd = $d1;
1842:     $mm = 12;
1843:     $yy = $year - 1;
1844:     while ($yy <= $year + 1) {
1845:         $dm = $this->getDaysInMonth($yy$mm);
1846:         while ($dd <= $dm) {
1847:             $age1 = $this->moon_age($yy$mm$dd,  0 - $this->TDIFF, 0, 0);    //Ver.3.11 bug-fix
1848:             $age2 = $this->moon_age($yy$mm$dd, 23 - $this->TDIFF, 59, 59);  //Ver.3.11 bug-fix
1849:             if ($age2 <= $age1) {
1850:                 $this->tblmoon[$cnt]['year']  = $yy;
1851:                 $this->tblmoon[$cnt]['month'] = $mm;
1852:                 $this->tblmoon[$cnt]['day']   = $dd;
1853:                 $this->tblmoon[$cnt]['age']   = $age1;
1854:                 $this->tblmoon[$cnt]['jd']    = $this->Gregorian2JD($yy$mm$dd, 0, 0, 0);
1855:                 $cnt++;
1856:             }
1857:             $dd++;
1858:         }
1859:         $mm++;
1860:         $dd = 1;
1861:         if ($mm > 12) {
1862:             $yy++;
1863:             $mm = 1;
1864:         }
1865:     }
1866: 
1867:     //中気(二十四節気の偶数番)を求める
1868:     $tblsun = array();
1869:     $cnt = 0;
1870:     $dd = $d1;
1871:     $mm = 12;
1872:     $yy = $year - 1;
1873:     while ($yy <= $year + 1) {
1874:         $dm = $this->getDaysInMonth($yy$mm);
1875:         while ($dd <= $dm) {
1876:             $l1 = $this->longitude_sun($yy$mm$dd,  0, 0, 0);
1877:             $l2 = $this->longitude_sun($yy$mm$dd, 24, 0, 0);
1878:             $n1 = floor($l1 / 15.0);
1879:             $n2 = floor($l2 / 15.0);
1880:             if (($n2 != $n1) && ($n2 % 2 == 0)) {
1881:                 $tblsun[$cnt]['jd'] = $this->Gregorian2JD($yy$mm$dd, 0, 0, 0);
1882:                 $oldmonth = floor($n2 / 2) + 2;
1883:                 if ($oldmonth > 12) $oldmonth -= 12;
1884:                 $tblsun[$cnt]['oldmonth']  = $oldmonth;
1885:                 $cnt++;
1886:             }
1887:             $dd++;
1888:         }
1889:         $mm++;
1890:         $dd = 1;
1891:         if ($mm > 12) {
1892:             $yy++;
1893:             $mm = 1;
1894:         }
1895:     }
1896: 
1897:     //月の名前を決める
1898:     $n1 = count($this->tblmoon);
1899:     $n2 = count($tblsun);
1900:     for ($i = 0; $i < $n1 - 1; $i++) {
1901:         for ($j = 0; $j < $n2$j++) {
1902:             if (($this->tblmoon[$i]['jd'] <= $tblsun[$j]['jd'])
1903:                 && ($this->tblmoon[$i + 1]['jd'] > $tblsun[$j]['jd'])) {
1904:                 $this->tblmoon[$i]['oldmonth'] = $tblsun[$j]['oldmonth'];
1905:                 $this->tblmoon[$i]['oldleap']  = FALSE;
1906:                 $this->tblmoon[$i + 1]['oldmonth'] = $tblsun[$j]['oldmonth'];
1907:                 $this->tblmoon[$i + 1]['oldleap']  = TRUE;
1908:                 break;
1909:             }
1910:         }
1911:     }
1912: 
1913:     //冬至・春分・夏至・秋分の補正 - version 3.72
1914:     foreach ($this->tblmoon as $key=>$arr) {
1915:         $yy1 = $this->tblmoon[$key]['year'];
1916:         $mm1 = $this->tblmoon[$key]['month'];
1917:         $dd1 = $this->tblmoon[$key]['day'];
1918:         $l1 = $this->longitude_sun($yy1$mm1$dd1, 0, 0, 0);
1919:         if (isset($this->tblmoon[$key + 1])) {
1920:             $yy2 = $this->tblmoon[$key + 1]['year'];
1921:             $mm2 = $this->tblmoon[$key + 1]['month'];
1922:             $dd2 = $this->tblmoon[$key + 1]['day'];
1923:         } else {
1924:             break;
1925:         }
1926:         $l2 = $this->longitude_sun($yy2$mm2$dd2, 0, 0, 0);
1927:         if ((($l1 > 345 || $l1 <= 0)) && (($l2 > 345) || $l2 < 15)) {
1928:             $this->tblmoon[$key]['oldmonth'] = 11;
1929:         } else if (($l1 > 75) && ($l2 < 90)) {
1930:             $this->tblmoon[$key]['oldmonth'] = 2;
1931:         } else if (($l1 > 165) && ($l2 < 180)) {
1932:             $this->tblmoon[$key]['oldmonth'] = 5;
1933:         } else if (($l1 > 255) && ($l2 < 270)) {
1934:             $this->tblmoon[$key]['oldmonth'] = 8;
1935:         }
1936:     }
1937: 
1938:     //2033年問題の解決案
1939:     if ($method > 0) {
1940:         $this->resolveLunarCalendar2033($method);
1941:     }
1942: }

グレゴリオ暦の年月日を引数にして、旧暦を求める方程式はない。そこで、西暦年を引数とし、その年の旧暦を計算して配列 $tblmoon に格納する方法を採用した。ただ、365日全部の対応を配列に格納するのは無駄なので、旧暦月の1日(朔日)と西暦年月日の対応(旧暦変換テーブル)を配列に格納することにした。

旧暦を求めたいときには、事前にメソッド makeLunarCalendar を実行する必要がある。
また、makeLunarCalendar は、直前の冬至から1年分の旧暦変換テーブルしか作成しないことに留意されたい。後述するが、3ヵ月超のカレンダーを生成するには、makeLunarCalendar を複数回コールする必要がある。
引数 $method は、後述する旧暦2033年問題の解決案の番号を指定する。これは省略可能で、省略時には解決を行わず、西暦2033年(令和15年)以降の旧暦は計算できなくなる。

グレゴリオ暦から旧暦を求める手順は、「旧暦と六曜を作りましょう」を参考にした。
天保暦のルールに則り、次の流れで旧暦を決めてゆく。
  1. 前年の冬至を求める。
  2. 翌年の雨水を求める。
  3. 1~2の期間中の中気(※)を配列 $tblsun に格納する。
  4. 朔日と中を比較して、月の名前と閏月を決めてゆく。
  5. 冬至を含む月は11月、春分を含む月は2月、夏至を含む月は5月、秋分を含む月は8月となるように調整する。
※中気とは、二十四節気のうちの偶数番である大寒、雨水、春分、‥‥冬至を指す。

2024: /**
2025:  * 旧暦を求める
2026:  * @param   int $year  西暦年
2027:  * @param   int $month 月
2028:  * @param   int $day   日
2029:  * @return  array(旧暦月,日,閏月フラグ)/FALSE:旧暦計算不能
2030: */
2031: function Gregorian2Lunar($year$month$day) {
2032:     //2033年問題チェック
2033:     if (($this->resolve2033 == 0) && ($year >= 2033)) {
2034:         $this->error = TRUE;
2035:         $this->errmsg = '2033年以降は正しい旧暦計算ができません';
2036:         return FALSE;
2037:     }
2038: 
2039:     //旧暦を求める
2040:     $items = array();
2041:     $jd = $this->Gregorian2JD($year$month$day, 0, 0, 0);
2042:     $str = '';
2043:     $n1 = count($this->tblmoon);
2044:     for ($i = 0; $i < $n1 - 1; $i++) {
2045:         if ($jd < $this->tblmoon[$i + 1]['jd']) {
2046:             $day = floor($jd - $this->tblmoon[$i]['jd']) + 1;
2047:             $items = array($this->tblmoon[$i]['oldmonth'], $day$this->tblmoon[$i]['oldleap']);
2048:             break;
2049:         }
2050:     }
2051: 
2052:     //旧暦テーブルが無い
2053:     if (count($items) == 0) {
2054:         $this->error  = TRUE;
2055:         $this->errmsg = '旧暦テーブルがありません';
2056:         return FALSE;
2057:     }
2058: 
2059:     return $items;
2060: }

旧暦変換テーブルが用意できたら、メソッド Gregorian2Lunar を使って旧暦を求める。
旧暦変換テーブルにマッチする値がないときには、エラーを返す。

解説:旧暦2033年問題の解決案

この旧暦計算法は天保暦に基づいているが、天保暦には2033年問題がある。西暦2033年(令和15年)10月は旧暦9月だが、11月に入ると旧暦11月となり、10月が無くなってしまう問題である。
これは、1844年(天保14年)に天保暦が導入されてから初めて起きる事態だが、天保暦は1872年(明治5年)に廃止されているために正式な解決法は用意されていない。2033年(令和15年)7月から2034年(令和16年)3月までの旧暦計算に影響を及ぼす。
一方で、国立天文台が次のような解決案を提示している(国立天文台は旧暦を決める機関ではないので、あくまで「提案」)。また、日本カレンダー暦文化振興協会も、「2033年旧暦閏月問題の見解」として、案1が望ましいと述べているものの、「これをベースに広く意見を求めるという手順が望ましい」と結論づけている。

案1‥‥2033年12月21日の冬至を優先し、冬至を含む月を11月とする。翌12月は中気がないので閏11月となる。
案2‥‥2033年9月23日の秋分を優先し、秋分を含む月を8月とする。前8月は中気がないので閏7月となる。
案3‥‥2033年12月21日の冬至を優先し、2024年(令和6年)2月18日の雨水が1月になると考え、翌2月は中気がないので閏1月となる。
旧暦2033年問題の解決案
朔日中気1中気2案1案2案3
2033/07/26処暑(08/23)---7月7月7月
2033/08/25------8月閏7月8月
2033/09/23秋分(09/23)---9月8月9月
2033/10/23霜降(10/23)---10月9月10月
2033/11/22小雪(11/22)冬至(12/21)11月10月11月
2033/12/22------閏11月11月12月
2034/01/20大寒(01/20)雨水(02/18)12月12月1月
2034/02/19------1月1月閏1月
2034/03/20春分(03/20)---2月2月2月

1944: /**
1945:  * 旧暦2033年問題の解決案
1946:  * @param   int $method 2033年問題解決案
1947:  *              1:案1,2:案2,3:案3(省略時:1)
1948:  * @return  なし
1949:  * 参考URL    https://www.pahoo.org/e-soul/webtech/php02/php02-45-01.shtm
1950: */
1951: function resolveLunarCalendar2033($method=1) {
1952:     //補正テーブル
1953:     //案1
1954:     static $table01 = array(
1955:         //西暦年, 月, 日, 旧暦月, 閏月か否か
1956:         array(2033,  7, 26,  7, FALSE),
1957:         array(2033,  8, 25,  8, FALSE),
1958:         array(2033,  9, 23,  9, FALSE),
1959:         array(2033, 10, 23, 10, FALSE),
1960:         array(2033, 11, 22, 11, FALSE),
1961:         array(2033, 12, 22, 11, TRUE),
1962:         array(2034,  1, 20, 12, FALSE),
1963:         array(2034,  2, 19,  1, FALSE),
1964:         array(2034,  3, 20,  2, FALSE),
1965:     );
1966:     //案2
1967:     static $table02 = array(
1968:         //西暦年, 月, 日, 旧暦月, 閏月か否か
1969:         array(2033,  7, 26,  7, FALSE),
1970:         array(2033,  8, 25,  7, TRUE),
1971:         array(2033,  9, 23,  8, FALSE),
1972:         array(2033, 10, 23,  9, FALSE),
1973:         array(2033, 11, 22, 10, FALSE),
1974:         array(2033, 12, 22, 11, FALSE),
1975:         array(2034,  1, 20, 12, FALSE),
1976:         array(2034,  2, 19,  1, FALSE),
1977:         array(2034,  3, 20,  2, FALSE),
1978:     );
1979:     //案3
1980:     static $table03 = array(
1981:         //西暦年, 月, 日, 旧暦月, 閏月か否か
1982:         array(2033,  7, 26,  7, FALSE),
1983:         array(2033,  8, 25,  8, FALSE),
1984:         array(2033,  9, 23,  9, FALSE),
1985:         array(2033, 10, 23, 10, FALSE),
1986:         array(2033, 11, 22, 11, FALSE),
1987:         array(2033, 12, 22, 12, FALSE),
1988:         array(2034,  1, 20,  1, FALSE),
1989:         array(2034,  2, 19,  1, TRUE),
1990:         array(2034,  3, 20,  2, FALSE),
1991:     );
1992: 
1993:     //案を選ぶ
1994:     switch ($method) {
1995:     case 1:
1996:         $table = $table01;
1997:         break;
1998:     case 2:
1999:         $table = $table02;
2000:         break;
2001:     case 3:
2002:         $table = $table03;
2003:         break;
2004:     //何もしない
2005:     default:
2006:         return;
2007:     }
2008: 
2009:     foreach ($this->tblmoon as $key=>$arr) {
2010:         if (($arr['year'] == 2033) && ($arr['month'] >= 7)) {
2011:             $this->tblmoon[$key]['day']      = $table[$arr['month'] - 7][2];
2012:             $this->tblmoon[$key]['oldmonth'] = $table[$arr['month'] - 7][3];
2013:             $this->tblmoon[$key]['oldleap']  = $table[$arr['month'] - 7][4];
2014:             $this->tblmoon[$key]['jd'] = $this->Gregorian2JD($this->tblmoon[$key]['year'], $this->tblmoon[$key]['month'], $this->tblmoon[$key]['day'], 0, 0, 0);
2015:         } else if (($arr['year'] == 2034) && ($arr['month'] <= 3)) {
2016:             $this->tblmoon[$key]['day']      = $table[$arr['month'] + 5][2];
2017:             $this->tblmoon[$key]['oldmonth'] = $table[$arr['month'] + 5][3];
2018:             $this->tblmoon[$key]['oldleap']  = $table[$arr['month'] + 5][4];
2019:             $this->tblmoon[$key]['jd'] = $this->Gregorian2JD($this->tblmoon[$key]['year'], $this->tblmoon[$key]['month'], $this->tblmoon[$key]['day'], 0, 0, 0);
2020:         }
2021:     }
2022: }

前述のメソッド makeLunarCalendar で算出した旧暦テーブル $tblmoon に対し、旧暦2033年問題の解決案を resolveLunarCalendar2033 で適用する。引数に解決案1~3を選べるようにした。
方法は、上述の表をあらかじめ用意しておき、$tblmoon の内容を置換するものである。

天保暦は、公式には、天保15年1月1日(1844年2月18日)から明治5年12月2日(1872年12月31日)までの約29年しか使われていない。にもかかわらず、その後、150年近くにわたり「旧暦」としてカレンダーに載っている。
旧暦計算のコードをご覧になれば分かるとおり、じつに複雑で計算コストがかかる。
じつは、旧暦の平均太陽年は現在の太陽暦(グレゴリオ暦)より誤差が小さい。にもかかわらずグレゴリオ暦が普及したのは、その算出方法がシンプルだからだろう。

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

2081: /**
2082:  * 干支を求める(下請け関数)
2083:  * @param   int $a1 十干の基準値
2084:  * @param   int $a2 十二支の基準値
2085:  * @param   int $n  計算したい値
2086:  * @return  string 干支
2087: */
2088: function __eto($a1$a2$n) {
2089: //十干
2090: static $table1 = array(
2091:  0 =>'',
2092:  1 =>'',
2093:  2 =>'',
2094:  3 =>'',
2095:  4 =>'',
2096:  5 =>'',
2097:  6 =>'',
2098:  7 =>'',
2099:  8 =>'',
2100:  9 =>''
2101: );
2102: 
2103: //十二支
2104: static $table2 = array(
2105:  0 =>'',
2106:  1 =>'',
2107:  2 =>'',
2108:  3 =>'',
2109:  4 =>'',
2110:  5 =>'',
2111:  6 =>'',
2112:  7 =>'',
2113:  8 =>'',
2114:  9 =>'',
2115: 10 =>'',
2116: 11 =>''
2117: );
2118: 
2119:     return $table1[abs($n - $a1) % 10] . $table2[abs($n - $a2) % 12];
2120: }

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

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

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

解説:土用の判定

0883: /**
0884:  * その日が土用かどうか
0885:  * @param   int $year, $month, $day  グレゴリオ暦による年月日
0886:  * @return  array(季節, 種類)
0887: */
0888: function isDoyo($year$month$day) {
0889:     static $table1 = array('' ,'', '', '');
0890:     static $table2 = array('in'=>'入り', 'ushi'=>'', 'out'=>'明け');
0891:     static $year0 = -1;           //西暦年保存用
0892:     static $doyo = array();      //土用情報保存用
0893: 
0894:     //土用情報を取得
0895:     if ($year0 != $year) {
0896:         $year0 = $year;
0897:         $doyo = $this->getDoyo($year0);
0898:     }
0899: 
0900:     //土用判定
0901:     foreach ($doyo as $key1=>$arr1) {
0902:         foreach ($arr1 as $key2=>$arr2) {
0903:             if (($arr2['year'] == $year0) && ($arr2['month'] == $month) && ($arr2['day'] == $day)) {
0904:                 return array($table1[$key1]$table2[$key2]);
0905:             }
0906:         }
0907:     }
0908:     return array('', '');
0909: }

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

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

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

解説:彼岸の計算

0911: /**
0912:  * その年の彼岸を求める
0913:  * @param   int $year 西暦年
0914:  * @return  array   [0]['in']['year','month','day']     春の彼岸入り
0915:  *                     ['out']['year','month','day']    春の彼岸明け
0916:  *                  [1]                                 秋の土用の入り/明け
0917: */
0918: function getHigan($year) {
0919:     static $higan = array();     //彼岸情報保存用
0920: 
0921:     //春の彼岸
0922:     $day = $this->getVernalEquinox($year);
0923:     $higan[0]['in']['year']   = (int)$year;
0924:     $higan[0]['in']['month']  = (int)3;
0925:     $higan[0]['in']['day']    = (int)($day - 3);
0926:     $higan[0]['out']['year']  = (int)$year;
0927:     $higan[0]['out']['month'] = (int)3;
0928:     $higan[0]['out']['day']   = (int)($day + 3);
0929: 
0930:     //秋の彼岸
0931:     $day = $this->getAutumnalEquinox($year);
0932:     $higan[1]['in']['year']   = (int)$year;
0933:     $higan[1]['in']['month']  = (int)9;
0934:     $higan[1]['in']['day']    = (int)($day - 3);
0935:     $higan[1]['out']['year']  = (int)$year;
0936:     $higan[1]['out']['month'] = (int)9;
0937:     $higan[1]['out']['day']   = (int)($day + 3);
0938: 
0939:     return $higan;
0940: }

彼岸とは、雑節の1つで、春分の日または秋分の日を中日 (ちゅうにち) として、前後各3日を合わせた各7日間を指す。各々の初日を彼岸(の)入り、最終日を彼岸明けと呼ぶ。

彼岸入り、明けの求め方については、その年の彼岸を求める getHigan メソッドを用意した。カレンダー計算の冒頭で実行する。
まず、getVernalEquinox メソッドで春分の日を求め、そこから3日前を彼岸入りに、3日後を彼岸明けとして登録する。
秋の彼岸については、getAutumnalEquinox メソッドで秋分の日を求め、同様に登録する。

解説:雑節の計算

0942: /**
0943:  * その年の雑節を求める
0944:  * @param   int $year 西暦年
0945:  * @return  array [雑節]['year','month','day']
0946:  *                  雑節‥‥社日,八十八夜,入梅,二百十日,二百二十日
0947:  *                  次の雑節は個別のメソッドを利用する
0948:  *                  節分→ isSetsubun()
0949:  *                  彼岸→ getHigan()
0950:  *                  半夏生→ getSolarTerm72()
0951:  *                  土用→isDoyo()
0952: */
0953: function getZassetsu($year) {
0954:     static $zassetsu = array();      //雑節情報保存用
0955: 
0956:     //社日計算テーブル
0957:     static $table_sha = array(
0958:         '' => +4,
0959:         '' => +3,
0960:         '' => +2,
0961:         '' => +1,
0962:         '' => 0,
0963:         '' => -1,
0964:         '' => -2,
0965:         '' => -3,
0966:         '' => -4,
0967:     );
0968: 
0969:     //春社を求める
0970:     $label = '春社';
0971:     $day = $this->getVernalEquinox($year);        //春分の日
0972:     $month = 3;
0973: 
0974:     $eto = $this->eto_day($year$month$day);
0975:     //癸の日の場合
0976:     if (preg_match('/癸/ui', $eto) > 0) {
0977:         $zassetsu[$label]['year']  = $year;
0978:         $zassetsu[$label]['month'] = $month;
0979:         $l1 = $this->longitude_sun($year$month$day, 12, 0, 0);
0980:         //春分が午前中なら前の戊の日
0981:         if (($l1 < 0) || ($l1 >= 360)) {
0982:             $zassetsu[$label]['day'] = $day - 5;
0983:         //春分が午後なら前の戊の日
0984:         } else {
0985:             $zassetsu[$label]['day'] = $day + 5;
0986:         }
0987:     //それ以外の場合
0988:     } else {
0989:         foreach ($table_sha as $key=>$val) {
0990:             if (preg_match('/' . $key . '/ui', $eto) > 0) {
0991:                 $zassetsu[$label]['year']  = $year;
0992:                 $zassetsu[$label]['month'] = $month;
0993:                 $zassetsu[$label]['day']   = $day + $val;
0994:                 break;
0995:             }
0996:         }
0997:     }
0998: 
0999:     //秋社を求める
1000:     $label = '秋社';
1001:     $day = $this->getAutumnalEquinox($year);      //秋分の日
1002:     $month = 9;
1003:     $eto = $this->eto_day($year$month$day);
1004:     //癸の日の場合
1005:     if (preg_match('/癸/ui', $eto) > 0) {
1006:         $zassetsu[$label]['year']  = $year;
1007:         $zassetsu[$label]['month'] = $month;
1008:         $l1 = $this->longitude_sun($year$month$day, 12, 0, 0);
1009:         //秋分が午前中なら前の戊の日
1010:         if ($l1 <= 180) {
1011:             $zassetsu[$label]['day'] = $day - 5;
1012:         //秋分が午後なら前の戊の日
1013:         } else {
1014:             $zassetsu[$label]['day'] = $day + 5;
1015:         }
1016:     //それ以外の場合
1017:     } else {
1018:         foreach ($table_sha as $key=>$val) {
1019:             if (preg_match('/' . $key . '/ui', $eto) > 0) {
1020:                 $zassetsu[$label]['year']  = $year;
1021:                 $zassetsu[$label]['month'] = $month;
1022:                 $zassetsu[$label]['day']   = $day + $val;
1023:                 break;
1024:             }
1025:         }
1026:     }
1027: 
1028:     //立春を求める
1029:     $month = 2;
1030:     $day   = 1;
1031:     while ($day < 10) {
1032:         if ($this->getSolarTerm($year$month$day) == '立春')       break;
1033:         $day++;
1034:     }
1035:     $ve = $this->AD2JD($year$month$day, 0, 0, 0);
1036: 
1037:     //八十八夜
1038:     $label = '八十八夜';
1039:     list($zassetsu[$label]['year'], $zassetsu[$label]['month'], $zassetsu[$label]['day']) = $this->JD2Gregorian($ve + 87);
1040: 
1041:     //入梅
1042:     $label = '入梅';
1043:     $month = 6;
1044:     $day = 1;
1045:     while ($day < 30) {
1046:         //太陽黄経
1047:         $l2 = $this->longitude_sun($year$month$day, 24, 0, 0);
1048:         if ($l2 >= 80) {
1049:             $zassetsu[$label]['year'] = $year;
1050:             $zassetsu[$label]['month'] = $month;
1051:             $zassetsu[$label]['day'] = $day;
1052:             break;
1053:         }
1054:         $day++;
1055:     }
1056: 
1057:     //二百十日
1058:     $label = '二百十日';
1059:     list($zassetsu[$label]['year'], $zassetsu[$label]['month'], $zassetsu[$label]['day']) = $this->JD2Gregorian($ve + 209);
1060: 
1061:     //二百二十日
1062:     $label = '二百二十日';
1063:     list($zassetsu[$label]['year'], $zassetsu[$label]['month'], $zassetsu[$label]['day']) = $this->JD2Gregorian($ve + 219);
1064: 
1065:     return $zassetsu;
1066: }

雑節のうち、節分isSetsubunメソッドで、彼岸は getHiganメソッドで、半夏生は getSolarTerm72メソッドで、土用は isDoyoメソッドを使って、それぞれ求めることができる。
ここでは、残る社日、八十八夜、入梅、二百十日、二百二十日の求め方を紹介する。

社日は、産土神 (うぶすながみ) を祀る日で、春・秋の2回ある。春のものを春社 (しゅんしゃ) 、秋のものを秋社 (しゅうしゃ) と呼ぶ。
春分に最も近い (つちのえ) の日が社日となる。ただし戊と戊のちょうど中間、つまり (みずのと) の日が秋分の日に重なる場合は、春分の瞬間が午前中ならば前の戊の日、午後ならば後の戊の日とする。
秋分の日についても同様にして求める。

入梅 (にゅうばい) は太陽黄経が80度の日である。

立春を第1日目として、八十八夜は88日目、二百十日は210日目、二百二十日は220日目を、それぞれ指す。

これらの雑節を求めるのが getZassetsu メソッドである。カレンダー計算の冒頭で実行する。
上記で説明した雑節の求め方をプログラムに実装した。

解説:一粒万倍日

一粒万倍日 (いちりゅうまんばいび) とは、種籾 (たねもみ) 一粒から何万倍ものお米が穫れるという意味で、古くから吉日のひとつにされてきた。種まき、開店、出資など、何かを始めるのに最適な日とされている。逆に、苦労の種が万倍になって返ってくる事から、借金をしたり、物を借りてはいけない日とされる。
1685年(貞享2年)に渋川春海が編纂した貞享暦から外されたが、近年、宝くじを買うのに良い日と宣伝され、再び脚光を浴びるようになった。

一粒万倍日は、節月と日の干支の組み合わせで決まる。
組み合わせ(計算方式)は2つあり、下表に整理する。
節月 二十四節気 計算方式I 計算方式II
1 立春~啓蟄 丑・午
2 啓蟄~清明 酉・寅
3 清明~立夏 子・卯
4 立夏~芒種 卯・辰
5 芒種~小暑 巳・午
6 小暑~立秋 酉・午
7 立秋~白露 子・未
8 白露~寒露 卯・申
9 寒露~立冬 酉・午
10 立冬~大雪 酉・戌
11 大雪~小寒 亥・子
12 小寒~立春 卯・子

0660: /**
0661:  * その日の節月を求める
0662:  * @param   int $year, $month, $day グレゴリオ暦による年月日
0663:  * @return  int 節月/0:計算失敗
0664: */
0665: function getSetsugetsu($year$month$day) {
0666:     static $table = array(
0667:   1 => 315,
0668:   2 => 345,
0669:   3 =>  15,
0670:   4 =>  45,
0671:   5 =>  75,
0672:   6 => 105,
0673:   7 => 135,
0674:   8 => 165,
0675:   9 => 195,
0676:  10 => 225,
0677:  11 => 255,
0678:  12 => 285,
0679:  13 => 315
0680: );
0681: 
0682:     //太陽黄経
0683:     $ls = $this->longitude_sun($year$month$day, 24, 0, 0);
0684: 
0685:     //節月
0686:     foreach ($table as $key=>$val) {
0687:         if ($key == 2) {
0688:             if (($ls >= $table[2]|| ($ls < $table[3]))    return $key;
0689:         } else {
0690:             if (($ls >= $table[$key]) && ($ls < $table[$key + 1]))    return $key;
0691:         }
0692:     }
0693:     return 0;
0694: }

ユーザー関数 getSetsugetsu は、西暦年月日を引数として、その日の節月を求める。既存の暦に合うよう太陽黄経の小数点以下を切り上げるようにしているが、切り上げに関する根拠がないのでご留意いただきたい。

2155: /**
2156:  * 一粒万倍日かどうか
2157:  * @param   int $year, $month, $day グレゴリオ暦による年月日
2158:  * @param   int $method 計算方式(1または2)(省略可能:デフォルトは1)
2159:  * @return  bool TRUE:一粒万倍日/FALSE:ではない
2160: */
2161: function ichiryumanbai($year$month$day$method=1) {
2162:     //計算方式I
2163:     $table[1] = array(
2164:  1 => array('', ''),
2165:  2 => array('', ''),
2166:  3 => array('', ''),
2167:  4 => array('', ''),
2168:  5 => array('', ''),
2169:  6 => array('', ''),
2170:  7 => array('', ''),
2171:  8 => array('', ''),
2172:  9 => array('', ''),
2173: 10 => array('', ''),
2174: 11 => array('', ''),
2175: 12 => array('', ''),
2176: );
2177:     //計算方式II
2178:     $table[2] = array(
2179:  1 => array(''),
2180:  2 => array(''),
2181:  3 => array(''),
2182:  4 => array(''),
2183:  5 => array(''),
2184:  6 => array(''),
2185:  7 => array(''),
2186:  8 => array(''),
2187:  9 => array(''),
2188: 10 => array(''),
2189: 11 => array(''),
2190: 12 => array(''),
2191: );
2192: 
2193:     //計算方式のチェック
2194:     if (($method != 1) && ($method != 2)) return FALSE;
2195: 
2196:     //節月を求める
2197:     $setsu = $this->getSetsugetsu($year$month$day);
2198:     //干支の右1文字取得
2199:     $eto = mb_substr($this->eto_day($year$month$day), 1, 1);
2200: 
2201:     return in_array($eto$table[$method][$setsu]);
2202: }

ユーザー関数 ichiryumanbai は、西暦年月日を引数として、getSetsugetsuにより節月を計算し、eto_day で求めたその日の干支に合致するかどうかを調べ、一粒万倍日かどうかを判定する。

解説:カレンダー作成

0179: /**
0180:  * 1ヶ月分のカレンダーを作成
0181:  * @param   object $pcl  pahooCalendarオブジェクト
0182:  * @param   int $start 週の開始曜日(0:日曜日, 1:月曜日...6:土曜日)
0183:  * @param   int $year  西暦年
0184:  * @param   int $month 月
0185:  * @return  string HTMLコンテンツ/FALSE:エラー
0186: */
0187: function makeCalendar($pcl$start$year$month) {
0188:     //雑節テーブル
0189:     $table_za = array('春社', '秋社', '八十八夜', '入梅', '二百十日', '二百二十日');
0190: 
0191:     //月のエラーチェック
0192:     if ($month < 1 && $month > 12)      return FALSE;
0193: 
0194:     $eto_year  = $pcl->eto_year($year);
0195:     $eto_month = $pcl->eto_month($year$month);
0196: 
0197:     //彼岸
0198:     $higan = $pcl->getHigan($year);
0199:     //雑節
0200:     $zassetsu = $pcl->getZassetsu($year);
0201: 
0202: $html =<<< EOT
0203: <table class="calendar">
0204: <tr>
0205: <th colspan="7"><span class="large">{$year}年({$eto_year}) {$month}月({$eto_month})</span></th>
0206: </tr>
0207: <tr>
0208: 
0209: EOT;
0210: 
0211:     //曜日の行
0212:     for ($i = 0; $i < 7; $i++) {
0213:         $n  = ($start + $i) % 7;
0214:         if ($n == 6)        $class = 'blue';
0215:         else if ($n == 0)   $class = 'red';
0216:         else                $class = 'black';
0217:         $str = $pcl->__getWeekString($n);
0218:         $html .= "<th><span class=\"{$class}\">{$str}</span></th>";
0219:     }
0220:     $html .= "</tr>\n";
0221: 
0222:     //カレンダー本体
0223:     $wn1 = $pcl->getWeekNumber($year$month, 1); //月の最初の曜日
0224:     $dim = $pcl->getDaysInMonth($year$month);       //月の最後の日
0225:     $cnt = 0;
0226:     $flag = FALSE;
0227:     $n = $start;
0228:     $day = 1;
0229:     while (1) {
0230:         if ($cnt % 7 == 0)      $html .= "<tr>\n";
0231:         //曜日の色
0232:         if ($n % 7 == 6)        $class = 'blue';
0233:         else if ($n % 7 == 0)   $class = 'red';
0234:         else                    $class = 'black';
0235:         if ($n % 7 == $wn1)     $flag = TRUE;
0236:         //表示開始
0237:         if ($flag) {
0238:             $ss = '';
0239:             //祝日
0240:             $holiday = $pcl->getHoliday($year$month$day);
0241:             if ($holiday != '') {
0242:                 $ss .= "<span class=\"small red\">{$holiday}<br /></span>\n";
0243:             }
0244:             //二十四節気
0245:             $solarterm = $pcl->getSolarTerm($year$month$day);
0246:             //土用
0247:             list($ss1$ss2) = $pcl->isDoyo($year$month$day);
0248:             if ($ss2 == '明け') {
0249:                 $ss2 = '';
0250:             } else if (($ss1 != '') && ($ss2 == '')) {
0251:                 $ss2 = '';
0252:             }
0253:             if ($ss1 != '' && $ss2 != '') {
0254:                 if ($solarterm != '') {
0255:                     $solarterm .= '<br />土用の' .  $ss2;
0256:                 } else {
0257:                     $solarterm = '土用の' . $ss2;
0258:                 }
0259:             }
0260:             if ($solarterm != '') {
0261:                 $ss .= "<span class=\"small\">{$solarterm}<br /></span>\n";
0262:             }
0263:             //七十二候
0264:             $solarterm72 = $pcl->getSolarTerm72($year$month$day);
0265:             if ($solarterm72 != '') {
0266:                 $ss .= "<span class=\"small\">{$solarterm72}<br /></span>\n";
0267:             }
0268:             //彼岸
0269:             if (($higan[0]['in']['month'] == $month) && ($higan[0]['in']['day'] == $day)) {
0270:                 $solarterm = '彼岸入り';
0271:                 $ss .= "<span class=\"small\">彼岸入り<br /></span>\n";
0272:             } else if (($higan[0]['out']['month'] == $month) && ($higan[0]['out']['day'] == $day)) {
0273:                 $solarterm = '彼岸明け';
0274:                 $ss .= "<span class=\"small\">彼岸明け<br /></span>\n";
0275:             } else if (($higan[1]['in']['month'] == $month) && ($higan[1]['in']['day'] == $day)) {
0276:                 $solarterm = '彼岸入り';
0277:                 $ss .= "<span class=\"small\">彼岸入り<br /></span>\n";
0278:             } else if (($higan[1]['out']['month'] == $month) && ($higan[1]['out']['day'] == $day)) {
0279:                 $solarterm = '彼岸明け';
0280:                 $ss .= "<span class=\"small\">彼岸明け<br /></span>\n";
0281:             }
0282:             //雑節
0283:             foreach ($table_za as $label) {
0284:                 if (($zassetsu[$label]['month'] == $month) && ($zassetsu[$label]['day'] == $day)) {
0285:                     $solarterm = $label;
0286:                     $ss .= "<span class=\"small\">{$label}<br /></span>\n";
0287:                 }
0288:             }
0289:             //一粒万倍日
0290:             $manbai = $pcl->ichiryumanbai($year$month$day, 1) ? '一粒万倍日' : '';
0291:             if ($manbai != '') {
0292:                 $ss .= "<span class=\"small\">{$manbai}<br /></span>\n";
0293:             }
0294:             //旧暦
0295:             list($oldmonth$oldday$oldleap) = $pcl->Gregorian2Lunar($year$month$day);
0296:             //旧暦テーブルの再作成
0297:             if ($pcl->error && ($pcl->resolve2033 > 0)) {
0298:                 $pcl->error  = FALSE;
0299:                 $pcl->errmsg = '';
0300:                 $yy = ($month <= 2) ? $year - 1 : $year;
0301:                 $pcl->makeLunarCalendar($yyRESOLVE2033);
0302:                 list($oldmonth$oldday$oldleap) = $pcl->Gregorian2Lunar($year$month$day);
0303:             }
0304:             //旧暦作成
0305:             if (! $pcl->error) {
0306:                 $oldleap = $oldleap ? '' : '';
0307:                 $rokuyou = $pcl->rokuyou($oldmonth$oldday);
0308:                 $oldcal = sprintf('%s%d月%d日<br />(%s)', $oldleap$oldmonth$oldday$rokuyou);
0309:             //旧暦作成不可(2033年問題)
0310:             } else {
0311:                 $oldcal = '';
0312:             }
0313:             //日の干支
0314:             $eto_day = $pcl->eto_day($year$month$day);
0315: 
0316:             //表示
0317: $html .=<<< EOT
0318: <td>
0319: <span class="large {$class}">{$day}</span>
0320: <div class="narrow">
0321: <span class="small red">{$holiday}</span><br />
0322: <span class="small blue">{$manbai}</span><br />
0323: <span class="small">{$solarterm}</span><br />
0324: <span class="small">{$solarterm72}</span><br />
0325: <span class="small">{$eto_day}</span><br />
0326: <span class="small">{$oldcal}</span>
0327: </div>
0328: </td>
0329: 
0330: EOT;
0331:             $day++;
0332:             if ($day > $dim)    break;
0333:         } else {
0334:             $html .= "<td>&nbsp;</td>";
0335:         }
0336:         $cnt++;
0337:         $n++;
0338:         if ($cnt % 7 == 0)  $html .= "</tr>\n";
0339:     }
0340:     //最後の日以降の処理
0341:     $cnt++;
0342:     while ($cnt % 7 != 0) {
0343:         $html .= "<td>&nbsp;</td>";
0344:         $cnt++;
0345:         if ($cnt % 7 == 0)  $html .= "</tr>\n";
0346:     }
0347: 
0348:     $html .= "</table>\n";
0349: 
0350:     return $html;
0351: }

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

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

解説:メインプログラム

0491: // メイン・プログラム =======================================================
0492: //パラメータ
0493: //西暦年
0494: $errmsg = '';
0495: $year  = getValidNumber('year', $errmsgdate('Y'), TRUEYEAR_MINYEAR_MAX);
0496: if ($errmsg != '') {
0497:     $year = date('Y');
0498:     $errmsg = '';
0499: }
0500: //月
0501: $month = getValidNumber('month', $errmsgdate('n'), TRUE, 1, 12);
0502: if ($errmsg != '') {
0503:     $month = date('n');
0504:     $errmsg = '';
0505: }
0506: //週の開始曜日
0507: $start = getValidNumber('start', $errmsg, 0, TRUE, 0, 6);
0508: if ($errmsg != '') {
0509:     $start = 0;
0510:     $errmsg = '';
0511: }
0512: //カレンダー作成期間(ヶ月)
0513: $period = getValidNumber('period', $errmsgPERIOD_DEFTRUEPERIOD_MINPERIOD_MAX);
0514: if ($errmsg != '') {
0515:     $period = 3;
0516:     $errmsg = '';
0517: }
0518: //表示モード
0519: $mode = getValidNumber('mode', $errmsg, 0, TRUE, 0, 1);
0520: if ($errmsg != '') {
0521:     $mode = 0;
0522:     $errmsg = '';
0523: }
0524: $msg = '';
0525: 
0526: //pahooCalendarクラス
0527: $pcl = new pahooCalendar();
0528: $pcl->setLanguage('jp');
0529: 
0530: $HtmlBody = makeCommonBody($mode$pcl$year$month$start$period$msg);
0531: 
0532: // 表示処理
0533: echo $HtmlHeader;
0534: echo $HtmlBody;
0535: echo $HtmlFooter;

メインプログラムのパラメータ取得については、「PHPセキュリティ対策:数値入力とバリデーション」をご覧いただきたい。
コマンドラインまたはURL変数で、カレンダー作成開始する西暦年(year)、月(month)、週の開始曜日(start;0:日曜日, 1:月曜日...6:土曜日)、作成期間(period、ヶ月)、表示モード(mode;0:全部表示,1:カレンダーのみ)を指定できる。万年カレンダーと言いながら、太陽や月の位置計算の精度の関係で、計算可能な範囲を100年間に絞っている😓
なお、入力可能な西暦年の範囲は2000以上2099以下の整数、月の範囲は1以上12以下の整数、週の開始曜日は0以上6以下の整数、カレンダー作成期間は1以上36以下の整数、表示モードは0以上1以下の整数に、それぞれ制約した。この範囲を超えたり、数字以外の入力があった場合はエラー処理は行わず、強制的に現在年月、週の開始曜日を0(日曜日)、週の開始曜日を0,表示モードを0にセットする。

質疑応答

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

活用例

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

参考サイト

一粒万倍日を調べる〈タイプ2〉:みんなの知識 ちょっと便利帳

参考書籍

表紙 知れば知るほど面白い暦の謎
著者 片山 真人
出版社 三笠書房
サイズ 文庫
発売日 2022年02月17日頃
価格 858円(税込)
ISBN 9784837987635
1週間はなぜ「7日」なのか?「曜日」はどのように生まれたか?国立天文台「暦の専門家」が教える!
 
(この項おわり)
header