目次
サンプル・プログラムの実行例
サンプル・プログラムのダウンロード
| nengo3.php | サンプル・プログラム本体 |
| pahooNormalizeText.php | テキスト正規化クラス pahooNormalizeText。 テキスト正規化クラスの使い方は「PHPで日本語テキストを正規化」を参照ください。include_pathが通ったディレクトリに配置してください。 |
| qreki_data.json | 旧暦(太陰太陽暦)対照表データ(JSON形式)。出典・ライセンスは後述。pahooNormalizeText.phpと同じフォルダに配置すること。 |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.0.0 | 2026/09/23 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 4.4 | 2026/09/23 | ad2wareki(), wareki2ad() 追加(明治6年以降・大正・昭和・平成・令和にも対応; |
| 4.3 | 2026/09/23 | qreki2ad():旧暦年の指定を元号+元号年($gengo, $gengo_year)に変更 |
| 4.2 | 2026/09/23 | ad2qreki():戻り値に元号('era')を追加;qreki_load():データファイルをクラス自身のフォルダ基準で読むよう修正 |
| 4.1 | 2026/09/23 | ad2qreki(), qreki2ad() 追加(西暦⇔旧暦[太陰太陽暦]の相互変換;対応範囲645/2/2~1872/12/31) |
| 4.0 | 2021/11/03 | getRuby_Yahoo():subwordを格納するようにした |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.0.1 | 2025/08/11 | getParam() bug-fix |
| 2.0.0 | 2025/08/11 | pahooLoadEnv() 追加 |
| 1.9.0 | 2025/07/26 | getParam() 引数に$trim追加 |
| 1.8.1 | 2025/03/15 | validRegexPattern() debug |
| 1.8.0 | 2024/11/12 | validRegexPattern() 追加 |
解説:準備
nengo3.php
27: // データ入力に関わる関数群:include_pathに配置すること
28: require_once('pahooInputData.php');
29: // PHPバージョン・チェック
30: exitIfLessVersion(MINUMUM_VERSION);
31:
32: // 参考サイト
33: define('REFERENCE', 'https://www.pahoo.org/e-soul/webtech/php03/php03-27-01.shtm');
34:
35: //プログラム・タイトル
36: define('TITLE', 'テキスト中の和暦・西暦年号を統一(その3)');
37:
38: // リファラチェック+リリースフラグの設定
39: if (isset($_SERVER['HTTP_HOST']) && ($_SERVER['HTTP_HOST'] == 'localhost')) {
40: define('FLAG_RELEASE', FALSE);
41: define('REFER_ON', '');
42: ini_set('display_errors', 1);
43: ini_set('error_reporting', E_ALL);
44: } else {
45: // リリース・フラグ(公開時にはTRUEにすること)
46: define('FLAG_RELEASE', TRUE);
47: // リファラ・チェック(直リン防止用;空文字ならチェックしない)
48: define('REFER_ON', 'www.pahoo.org');
49: }
50:
51: // 各種定数(START) ===========================================================
52: // 表示幅(ピクセル)
53: define('WIDTH', 600);
54:
55: // 例文
56: define('DEF_SOUR', "慶応3年1月9日、明治天皇が即位する。慶応四年一月三日、鳥羽・伏見の戦いが起きる。改元は1868年10月23日に行われた。明治2年5月18日、五稜郭が開放され、戊辰戦争は終わる。");
57:
58: // 変換関数の選択肢
59: $SelectFuncs = array(
60: //関数名 タイトル ラジオボタンcheked
61: 'seireki' => array('title'=>'西暦に統一', 'checked'=>''),
62: 'wareki' => array('title'=>'和暦に統一', 'checked'=>''),
63: 'mixture' => array('title'=>'西暦(和暦)', 'checked'=>'')
64: );
65:
66: // 各種定数(END) ===============================================================
67:
68: // pahooNormalizeText クラス;各自の環境に合わせて変更すること
69: require_once('pahooNormalizeText.php');
クラス・ファイルの読み込みは組み込み関数 require_once を用いて行う。パス名は絶対値で指定するか、事前に "php.ini" の include_path で指定されるパスにクラス・ファイルを配置する。
新たに追加した旧暦データ・ファイル "qreki_data.json" は、include_path とは関係なく、クラス・ファイル "pahooNormalizeText.php" 自身と同じフォルダに置くこと。プログラム側の実行時カレント・フォルダに関わらず、組み込み関数 dirname と [__FILE__:php_language] を使ってクラス・ファイル自身の場所を求め、そこからの相対パスでデータ・ファイルを読み込むようにしている。
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
解説:旧暦(太陰太陽暦)データ
そこで、旧暦と西暦(大化元年~明治5年)を年ごとに対応させた対照表データ "qreki_data.json" を用意した。出典・ライセンスについては後述の「参考書籍」「参考サイト」を参照のこと。
対応範囲は、西暦645年2月2日(大化元年1月1日)~西暦1872年12月31日(明治5年12月2日)である。日本がグレゴリオ暦(現在の暦)に切り替えた明治6年1月1日(1873年1月1日)以降は、旧暦データを参照する必要がないため、対応範囲に含めていない。
"qreki_data.json" は、キー "旧暦表" の下に、年ごとのレコードを配列として格納したJSON形式である。各レコードの構造を下表に示す。
たとえば、明治3年(1870年)のレコードは次のようになっている。
大小 配列が13個あるのは、10番目の月のあとに閏10月が挟まるためで、実際の月の並びは「1月, 2月, ……, 10月, 閏10月, 11月, 12月」の13か月になる。このように、旧暦では数年に1度、1年が13か月になる年がある。
| キー | 内 容 |
|---|---|
| 元日 | その年の旧暦1月1日にあたる西暦(または[ユリウス暦:blue])の年月日("[年/月/日:blue]" 形式) |
| 大小 | その年の各月が[大の月:blue](30日)か[小の月:blue](29日)かを、[1:blue]/[0:blue] で先頭の月から順に並べた配列 |
| 閏 | 閏月がある年だけ存在するキー。何番目の月のあとに閏月が入るかを示す数値 |
| 元日 | 1870/2/1 |
|---|---|
| 閏 | 10 |
| 大小 | [0,1,1,0,1,0,1,0,1,0,0,1,0](13個) |
解説:ユリウス通日への変換
pahooNormalizeText.php
1230: /**
1231: * 西暦の年月日をユリウス通日(JDN)に変換する
1232: * 1582年10月15日以降はグレゴリオ暦,それ以前はユリウス暦として扱う
1233: * @param int $year 西暦年
1234: * @param int $month 月(1~12)
1235: * @param int $day 日
1236: * @return int ユリウス通日
1237: */
1238: function ad2jdn($year, $month, $day) {
1239: $a = (int)((14 - $month) / 12);
1240: $yy = $year + 4800 - $a;
1241: $mm = $month + 12 * $a - 3;
1242:
1243: $is_gregorian = ($year > 1582)
1244: || ($year == 1582 && $month > 10)
1245: || ($year == 1582 && $month == 10 && $day >= 15);
1246:
1247: if ($is_gregorian) {
1248: return $day + (int)((153 * $mm + 2) / 5) + 365 * $yy + (int)($yy / 4) - (int)($yy / 100) + (int)($yy / 400) - 32045;
1249: } else {
1250: return $day + (int)((153 * $mm + 2) / 5) + 365 * $yy + (int)($yy / 4) - 32083;
1251: }
1252: }
pahooNormalizeText.php
1254: /**
1255: * ユリウス通日(JDN)を西暦の年月日に変換する
1256: * @param int $jdn ユリウス通日
1257: * @return array array('year'=>年, 'month'=>月, 'day'=>日)
1258: */
1259: function jdn2ad($jdn) {
1260: if ($jdn >= $this->GREGORIAN_CUTOVER_JDN) {
1261: //グレゴリオ暦
1262: $a = $jdn + 32044;
1263: $b = (int)((4 * $a + 3) / 146097);
1264: $c = $a - (int)(146097 * $b / 4);
1265: } else {
1266: //ユリウス暦
1267: $b = 0;
1268: $c = $jdn + 32082;
1269: }
1270: $d = (int)((4 * $c + 3) / 1461);
1271: $e = $c - (int)(1461 * $d / 4);
1272: $m = (int)((5 * $e + 2) / 153);
1273:
1274: $day = $e - (int)((153 * $m + 2) / 5) + 1;
1275: $month = $m + 3 - 12 * (int)($m / 10);
1276: $year = 100 * $b + $d - 4800 + (int)($m / 10);
1277:
1278: return array('year'=>$year, 'month'=>$month, 'day'=>$day);
1279: }
解説:西暦を旧暦に変換
pahooNormalizeText.php
1406: /**
1407: * 西暦を旧暦(太陰太陽暦)に変換する
1408: * 対応範囲:西暦645年2月2日(大化元年1月1日)~西暦1872年12月31日(明治5年12月2日)
1409: * @param int $year 西暦年
1410: * @param int $month 月(1~12)
1411: * @param int $day 日
1412: * @return array 旧暦の年月日 array('year'=>旧暦年, 'month'=>月, 'leap'=>閏月ならTRUE, 'day'=>日)
1413: * /FALSE:対応範囲外,またはデータ読み込みエラー
1414: */
1415: function ad2qreki($year, $month, $day) {
1416: if (! $this->qreki_load()) return FALSE;
1417:
1418: $jdn = $this->ad2jdn($year, $month, $day);
1419: if ($jdn < $this->QREKI_MIN_JDN || $jdn > $this->QREKI_MAX_JDN) {
1420: $this->error = TRUE;
1421: $this->errmsg = '対応範囲外の日付.';
1422: return FALSE;
1423: }
1424:
1425: $qy = $this->qreki_year_of_jdn($jdn);
1426: if ($qy === FALSE) {
1427: $this->error = TRUE;
1428: $this->errmsg = '対応範囲外の日付.';
1429: return FALSE;
1430: }
1431:
1432: $jdn0 = $this->qreki_table[$qy]['jdn'];
1433: $rec = $this->qreki_table[$qy]['rec'];
1434: $offset = $jdn - $jdn0;
1435: $cur = 0;
1436: foreach ($rec['大小'] as $i=>$size) {
1437: $length = ($size == 1) ? 30 : 29;
1438: if ($offset < $cur + $length) {
1439: list($qm, $qleap) = $this->qreki_month_label($rec, $i);
1440: return array(
1441: 'year' => $qy,
1442: 'month'=> $qm,
1443: 'leap' => $qleap,
1444: 'day' => $offset - $cur + 1,
1445: 'era' => $this->qreki_gengo($qy, $year, $month, $day)
1446: );
1447: }
1448: $cur += $length;
1449: }
1450:
1451: $this->error = TRUE;
1452: $this->errmsg = '旧暦への変換に失敗.';
1453: return FALSE;
1454: }
戻り値の era には、その日の元号(例:天正15年)も格納しており、ad2era をもとにしたユーザー関数 ad2gengo で求めている。
解説:旧暦を西暦に変換
pahooNormalizeText.php
1472: /**
1473: * 旧暦(太陰太陽暦)を西暦に変換する
1474: * 対応範囲:旧暦645年1月1日~明治5年12月2日
1475: * 旧暦年は,era2ad() と同様に元号+元号年で指定する(例:"明治", 1 → 1868年)
1476: * 元号を使わず西暦年をそのまま指定したい場合は,$gengo に ''(空文字)を指定する
1477: * @param string $gengo 元号(例:"明治");西暦年をそのまま使う場合は ''
1478: * @param int $gengo_year 元号年(元年は1)/$gengoが''の場合は西暦年
1479: * @param int $qmonth 月(1~12)
1480: * @param int $qday 日
1481: * @param bool $leap 閏月ならTRUE(省略時FALSE)
1482: * @return array 西暦の年月日 array('year'=>年, 'month'=>月, 'day'=>日)
1483: * /FALSE:対応範囲外,該当する年月日が存在しない,元号が不明,またはデータ読み込みエラー
1484: */
1485: function qreki2ad($gengo, $gengo_year, $qmonth, $qday, $leap=FALSE) {
1486: if (! $this->qreki_load()) return FALSE;
1487:
1488: //元号+元号年を,旧暦年(qreki_data.jsonのキー)に変換
1489: //元号年は「西暦1月1日」ではなく「旧暦の元日」を境に繰り上がるため,
1490: //era2ad() の西暦年ベースの単純な引き算ではなく,改元日自体が属する旧暦年を基準に数える
1491: if ($gengo == '') {
1492: //元号を使わず西暦年をそのまま指定
1493: $qyear = (int)$gengo_year;
1494: } else {
1495: $start_date = $this->gengo_start_date($gengo);
1496: if ($start_date === FALSE) {
1497: $this->error = TRUE;
1498: $this->errmsg = '元号「' . $gengo . '」が不明.';
1499: return FALSE;
1500: }
1501: $start_jdn = $this->ad2jdn((int)substr($start_date, 0, 4), (int)substr($start_date, 4, 2), (int)substr($start_date, 6, 2));
1502: $start_qy = $this->qreki_year_of_jdn($start_jdn);
1503: if ($start_qy === FALSE) {
1504: $this->error = TRUE;
1505: $this->errmsg = '元号「' . $gengo . '」の対応範囲外.';
1506: return FALSE;
1507: }
1508: $qyear = $start_qy + (int)$gengo_year - 1;
1509: }
1510:
1511: if (! isset($this->qreki_table[$qyear])) {
1512: $this->error = TRUE;
1513: $this->errmsg = '対応範囲外の旧暦年.';
1514: return FALSE;
1515: }
1516:
1517: $jdn0 = $this->qreki_table[$qyear]['jdn'];
1518: $rec = $this->qreki_table[$qyear]['rec'];
1519: $cur = 0;
1520: foreach ($rec['大小'] as $i=>$size) {
1521: $length = ($size == 1) ? 30 : 29;
1522: list($label, $is_leap) = $this->qreki_month_label($rec, $i);
1523: if ($label == $qmonth && $is_leap == $leap) {
1524: if ($qday < 1 || $qday > $length) {
1525: $this->error = TRUE;
1526: $this->errmsg = '存在しない旧暦の日付.';
1527: return FALSE;
1528: }
1529: $jdn = $jdn0 + $cur + ($qday - 1);
1530: if ($jdn < $this->QREKI_MIN_JDN || $jdn > $this->QREKI_MAX_JDN) {
1531: //理論上の日付だが史実には存在しない(明治5年12月3日以降など)
1532: $this->error = TRUE;
1533: $this->errmsg = '対応範囲外の旧暦日付.';
1534: return FALSE;
1535: }
1536: return $this->jdn2ad($jdn);
1537: }
1538: $cur += $length;
1539: }
1540:
1541: $this->error = TRUE;
1542: $this->errmsg = '存在しない旧暦の月.';
1543: return FALSE;
1544: }
解説:近現代(明治6年以降)にも対応させる
pahooNormalizeText.php
1546: /**
1547: * 西暦の年月日から,和暦(元号)表記の年月日を求める
1548: * 明治5年12月2日以前:太陰太陽暦(旧暦)としてad2qreki()で変換する(閏月に対応)
1549: * 明治6年1月1日以降 :改暦後の暦(西暦と同じ)として,元号だけをad2era()で求める
1550: * (明治・大正・昭和・平成・令和のいずれにも対応;この期間に閏月は存在しない)
1551: * @param int $year 西暦年
1552: * @param int $month 月(1~12)
1553: * @param int $day 日
1554: * @return array 和暦の年月日 array('era'=>元号+元号年, 'month'=>月, 'day'=>日, 'leap'=>閏月ならTRUE)
1555: * /FALSE:対応範囲外(大化元年より前),またはデータ読み込みエラー
1556: */
1557: function ad2wareki($year, $month, $day) {
1558: if (! $this->qreki_load()) return FALSE;
1559:
1560: $jdn = $this->ad2jdn($year, $month, $day);
1561: if ($jdn >= $this->QREKI_MIN_JDN && $jdn <= $this->QREKI_MAX_JDN) {
1562: $q = $this->ad2qreki($year, $month, $day);
1563: if ($q === FALSE) return FALSE;
1564: return array('era'=>$q['era'], 'month'=>$q['month'], 'day'=>$q['day'], 'leap'=>$q['leap']);
1565: }
1566: if ($jdn < $this->QREKI_MIN_JDN) {
1567: //大化元年(西暦645年2月2日)より前は非対応
1568: $this->error = TRUE;
1569: $this->errmsg = '対応範囲外の日付.';
1570: return FALSE;
1571: }
1572:
1573: //明治6年1月1日以降:改暦後の暦(西暦と同じ)として元号だけを求める
1574: $era_str = $this->ad2gengo($year, $month, $day);
1575: if ($era_str == '' || $era_str === FALSE) {
1576: $this->error = TRUE;
1577: $this->errmsg = '元号への変換に失敗.';
1578: return FALSE;
1579: }
1580: return array('era'=>$era_str, 'month'=>(int)$month, 'day'=>(int)$day, 'leap'=>FALSE);
1581: }
pahooNormalizeText.php
1583: /**
1584: * 元号+元号年の和暦から,西暦の年月日を求める
1585: * 明治5年以前:太陰太陽暦(旧暦)としてqreki2ad()で変換する(閏月に対応)
1586: * 明治6年以降:改暦後の暦(西暦と同じ)として,年月日をそのまま西暦とみなす
1587: * (明治・大正・昭和・平成・令和のいずれにも対応;この期間に閏月は存在しない)
1588: * @param string $gengo 元号(例:"明治")
1589: * @param int $gengo_year 元号年(元年は1)
1590: * @param int $month 月(1~12)
1591: * @param int $day 日
1592: * @param bool $leap 閏月ならTRUE(省略時FALSE)
1593: * @return array 西暦の年月日 array('year'=>年, 'month'=>月, 'day'=>日)
1594: * /FALSE:対応範囲外,該当する年月日が存在しない,元号が不明,またはデータ読み込みエラー
1595: */
1596: function wareki2ad($gengo, $gengo_year, $month, $day, $leap=FALSE) {
1597: $str = $this->era2ad($gengo, $gengo_year);
1598: if (! preg_match('/^(-?[0-9]+)年/u', $str, $matches)) {
1599: $this->error = TRUE;
1600: $this->errmsg = '元号「' . $gengo . '」が不明.';
1601: return FALSE;
1602: }
1603: $ad_year = (int)$matches[1];
1604:
1605: if ($ad_year < 1873) {
1606: //明治5年12月2日以前:太陰太陽暦(旧暦)として変換する
1607: return $this->qreki2ad($gengo, $gengo_year, $month, $day, $leap);
1608: }
1609:
1610: //明治6年1月1日以降:改暦後の暦(西暦と同じ);この期間に閏月は存在しない
1611: if ($leap) {
1612: $this->error = TRUE;
1613: $this->errmsg = '明治6年以降に閏月は存在しない.';
1614: return FALSE;
1615: }
1616: if (! checkdate((int)$month, (int)$day, $ad_year)) {
1617: $this->error = TRUE;
1618: $this->errmsg = '存在しない日付.';
1619: return FALSE;
1620: }
1621: return array('year'=>$ad_year, 'month'=>(int)$month, 'day'=>(int)$day);
1622: }
そこで、この境界を自動的に判断してくれるユーザー関数 ad2wareki/wareki2ad を用意した。明治5年12月2日以前なら ad2qreki/qreki2ad を、それ以降なら ad2era/era2ad を呼び出すよう振り分ける。なお、閏月が存在するのは太陰太陽暦だけなので、明治6年以降について閏月ありの変換を求められた場合は FALSE を返すようにしている。
解説:西暦を和暦へ変換(閏月対応)
nengo3.php
258: /**
259: * 和暦に統一
260: * @param string $sour オリジナル・テキスト
261: * @return string 変換後テキスト
262: */
263: function wareki($sour) {
264: $pat = '/([^0-9〇一二三四五六七八九十百千万あ-ん、-〟!-¥]{0,4})\s*([0-9元〇一二三四五六七八九十百千万]+)年\s*(閏)?([0-9〇一二三四五六七八九十]*)月?([0-9〇一二三四五六七八九十]*)日?/msui';
265:
266: return preg_replace_callback($pat, 'seireki2wareki', $sour);
267: }
nengo3.php
189: /**
190: * 西暦→和暦変換(漢数字対応・閏月対応)
191: * @param array $arr[1] プレフィックス
192: * [2] 年
193: * [3] 閏(「閏」または空)
194: * [4] 月
195: * [5] 日
196: * @return string 和暦
197: */
198: function seireki2wareki($arr) {
199: // インスタンス生成
200: $pnt = new pahooNormalizeText();
201:
202: // プレフィックス
203: $prefix = isset($arr[1]) ? trim($arr[1]) : '';
204: if (isset($arr[2])) {
205: $year = ($arr[2] == '元') ? 1 : $pnt->kan2num($arr[2], 0);
206: } else {
207: $year = 0;
208: }
209: // 既に和暦表記の場合,入力に「閏」が付いているかどうか
210: $in_leap = isset($arr[3]) && $arr[3] != '';
211:
212: // 日が省略されている場合には1を代入
213: $day = (isset($arr[5]) && $arr[5] != '') ?
214: $pnt->kan2num($arr[5], 0) : 1;
215: // 月が省略されている場合には1を代入
216: $month = (isset($arr[4]) && $arr[4] != '') ?
217: $pnt->kan2num($arr[4], 0) : 1;
218:
219: // すでに元号(和暦)表記の場合は,漢数字を正規化するだけでよい
220: // (西暦年としてad2wareki()に渡すと,全く別の年として誤変換されてしまう)
221: if ($prefix != '' && $pnt->isera($prefix)) {
222: $era_str = $prefix . (($year == 1) ? '元' : $year) . '年';
223: $leap_str = $in_leap ? '閏' : '';
224: // 西暦表記の場合は,旧暦(元号)に変換する
225: // 明治5年12月2日以前は太陰太陽暦,明治6年以降は改暦後の暦として変換する
226: } else {
227: $arr2 = $pnt->ad2wareki($year, $month, $day);
228: if ($arr2 === FALSE) {
229: // 対応範囲外などで変換できない場合は,元の文字列のまま返す
230: $pnt = NULL;
231: return $arr[0];
232: }
233: $era_str = $arr2['era'];
234: $month = $arr2['month']; //旧暦の月日に置き換える
235: $day = $arr2['day'];
236: $leap_str = $arr2['leap'] ? '閏' : '';
237: }
238:
239: // 年月日が揃っている場合
240: if (isset($arr[5]) && $arr[5] != '') {
241: $era = sprintf('%s%s%d月%d日', $era_str, $leap_str, $month, $day);
242: // 日が省略されている場合
243: } else if (isset($arr[4]) && $arr[4] != '') {
244: $era = sprintf('%s%s%d月', $era_str, $leap_str, $month);
245: // 月が省略されている場合
246: } else if (isset($arr[2]) && $arr[2] != '') {
247: $era = sprintf('%s', $era_str);
248: // エラー時
249: } else {
250: $era = FALSE;
251: }
252:
253: // インスタンス解放
254: $pnt = NULL;
255: return $era;
256: }
今回、閏月にも対応させるため、正規表現に「閏」の1文字を捉えるグループを追加した。年の直後、月の直前にある「閏」の有無を読み取り、既に和暦表記であればそのまま出力に反映し、西暦表記から変換する場合は ad2wareki の戻り値 leap を見て「閏」を補うようにしている。
漢数字を数値に変換する処理は、「PHPで漢数字を半角数字に変換する(整数版)」で作成したユーザー関数 kan2num をそのまま利用している。
解説:和暦を西暦へ変換
nengo3.php
335: /**
336: * 西暦に統一
337: * @param string $sour オリジナル・テキスト
338: * @return string 変換後テキスト
339: */
340: function seireki($sour) {
341: $pat = '/([^0-9〇一二三四五六七八九十百千万あ-ん、-〟!-¥]{0,4})\s*([0-9〇元一二三四五六七八九十百千万]+)年\s*(閏)?([0-9〇一二三四五六七八九十]*)月?([0-9〇一二三四五六七八九十]*)日?/msui';
342:
343: return preg_replace_callback($pat, 'wareki2seireki', $sour);
344: }
nengo3.php
269: /**
270: * 和暦→西暦変換(漢数字対応・閏月対応)
271: * @param array $arr 和暦年月日(漢数字可能)
272: * [1]プレフィックス(元号) [2]年 [3]閏(「閏」または空) [4]月 [5]日
273: * @return string 西暦
274: */
275: function wareki2seireki($arr) {
276: // インスタンス生成
277: $pnt = new pahooNormalizeText();
278:
279: // 元号
280: $prefix = isset($arr[1]) ? trim($arr[1]) : '';
281: // 年号
282: if (isset($arr[2])) {
283: if ($arr[2] == '元') $arr[2] = 1;
284: }
285: $year = isset($arr[2]) && ($arr[2] != '') ? $pnt->kan2num($arr[2], 0) : 0;
286: $leap = isset($arr[3]) && $arr[3] != '';
287: $month = isset($arr[4]) && ($arr[4] != '') ? $pnt->kan2num($arr[4], 0) : 1;
288: $day = isset($arr[5]) && ($arr[5] != '') ? $pnt->kan2num($arr[5], 0) : 1;
289:
290: // プレフィックスが元号でない場合
291: if ($prefix == '' || ! $pnt->isera($prefix)) {
292: // 元号を伴わない数字だけの「年」は,既に西暦表記なので変換不要
293: //(漢数字は正規化する)
294: if ($prefix == '') {
295: if (isset($arr[5]) && $arr[5] != '') {
296: $dest = sprintf('%d年%d月%d日', $year, $month, $day);
297: } else if (isset($arr[4]) && $arr[4] != '') {
298: $dest = sprintf('%d年%d月', $year, $month);
299: } else {
300: $dest = sprintf('%d年', $year);
301: }
302: // 元号と判断できない文字列が前置している場合は,誤変換を避けて元のまま返す
303: } else {
304: $dest = $arr[0];
305: }
306: $pnt = NULL;
307: return $dest;
308: }
309:
310: // 元号(和暦)表記の場合:西暦に変換する
311: // 明治5年以前は太陰太陽暦(閏月対応),明治6年以降は改暦後の暦として変換する
312: $ad = $pnt->wareki2ad($prefix, $year, $month, $day, $leap);
313: if ($ad === FALSE) {
314: // 対応範囲外などで変換できない場合は,元の文字列のまま返す
315: $pnt = NULL;
316: return $arr[0];
317: }
318:
319: // 年月日が揃っている場合
320: if (isset($arr[5]) && $arr[5] != '') {
321: $dest = sprintf('%d年%d月%d日', $ad['year'], $ad['month'], $ad['day']);
322: // 日が省略されている場合
323: } else if (isset($arr[4]) && $arr[4] != '') {
324: $dest = sprintf('%d年%d月', $ad['year'], $ad['month']);
325: // 月が省略されている場合
326: } else {
327: $dest = sprintf('%d年', $ad['year']);
328: }
329:
330: // インスタンス解放
331: $pnt = NULL;
332: return $dest;
333: }
年号の前に付いている文字列が isera で元号と判断できない場合は、既に西暦表記とみなし、漢数字の正規化だけを行って変換をスキップする。こうすることで、元々西暦で書かれていた年号を誤って旧暦の年として再解釈してしまう不具合を避けている。
解説:西暦・和暦混合変換
nengo3.php
407: /**
408: * 西暦(和暦)混合変換
409: * @param string $sour オリジナル・テキスト
410: * @param int $mode 変換範囲
411: * @return string 変換後テキスト
412: */
413: function mixture($sour) {
414: $pat = '/([^0-9〇一二三四五六七八九十百千万あ-ん、-〟!-¥]{0,4})\s*([0-9元〇一二三四五六七八九十百千万]+)年\s*(閏)?([0-9〇一二三四五六七八九十]*)月?([0-9〇一二三四五六七八九十]*)日?/msui';
415:
416: return preg_replace_callback($pat, 'seireki2mix', $sour);
417: }
nengo3.php
346: /**
347: * 西暦・和暦のどちらの表記でも,「主表記(もう一方の表記)」に変換する
348: * 元号(和暦)表記の場合 → 西暦を計算して「西暦(和暦)」にする
349: * 西暦表記の場合 → 元号を計算して「西暦(和暦)」にする
350: * 月・日は,元のテキストで指定されていた精度のぶんだけ付加する
351: * 明治5年12月2日以前は太陰太陽暦(閏月対応),明治6年以降は改暦後の暦として扱う
352: * @param array $arr [1]プレフィックス(元号),[2]年,[3]閏(「閏」または空),[4]月,[5]日
353: * @return string 西暦(和暦)
354: */
355: function seireki2mix($arr) {
356: // インスタンス生成
357: $pnt = new pahooNormalizeText();
358:
359: $prefix = isset($arr[1]) ? trim($arr[1]) : '';
360: $year = isset($arr[2]) ? (($arr[2] == '元') ? 1 : $pnt->kan2num($arr[2], 0)) : 0;
361: $in_leap = isset($arr[3]) && $arr[3] != '';
362: $has_month = isset($arr[4]) && $arr[4] != '';
363: $has_day = isset($arr[5]) && $arr[5] != '';
364: $month = $has_month ? $pnt->kan2num($arr[4], 0) : 1;
365: $day = $has_day ? $pnt->kan2num($arr[5], 0) : 1;
366:
367: // 元号(和暦)表記の場合:元のテキストを和暦側とし,西暦を計算する
368: if ($prefix != '' && $pnt->isera($prefix)) {
369: $leap_str = $in_leap ? '閏' : '';
370: $wareki = $prefix . (($year == 1) ? '元' : $year) . '年';
371: if ($has_month) $wareki .= $leap_str . $month . '月';
372: if ($has_day) $wareki .= $day . '日';
373:
374: $ad = $pnt->wareki2ad($prefix, $year, $month, $day, $in_leap);
375: if ($ad === FALSE) {
376: $pnt = NULL;
377: return $arr[0]; //変換できない場合は元の文字列のまま
378: }
379:
380: if ($has_day) $seireki = sprintf('%d年%d月%d日', $ad['year'], $ad['month'], $ad['day']);
381: else if ($has_month) $seireki = sprintf('%d年%d月', $ad['year'], $ad['month']);
382: else $seireki = sprintf('%d年', $ad['year']);
383:
384: // 西暦表記の場合:元のテキストを西暦側とし,元号(旧暦)を計算する
385: } else {
386: $seireki = $prefix . $year . '年';
387: if ($has_month) $seireki .= $month . '月';
388: if ($has_day) $seireki .= $day . '日';
389:
390: $q = $pnt->ad2wareki($year, $month, $day);
391: if ($q === FALSE) {
392: $pnt = NULL;
393: return $arr[0]; //対応範囲外などで変換できない場合は元の文字列のまま
394: }
395: $leap_str = $q['leap'] ? '閏' : '';
396:
397: if ($has_day) $wareki = sprintf('%s%s%d月%d日', $q['era'], $leap_str, $q['month'], $q['day']);
398: else if ($has_month) $wareki = sprintf('%s%s%d月', $q['era'], $leap_str, $q['month']);
399: else $wareki = sprintf('%s', $q['era']);
400: }
401:
402: // インスタンス解放
403: $pnt = NULL;
404: return ($seireki != $wareki) ? $seireki . '(' . $wareki . ')' : $seireki;
405: }
月・日は、元のテキストで指定されていた精度のぶんだけ付加する。たとえば年だけの表記なら年だけを、年月までの表記なら年月までを変換後の文字列に含める。西暦・和暦が一致する場合(変換のしようがない場合)は、括弧を付けずにそのまま返す。
参考サイト
- Qreki_nako:本記事で利用した旧暦対照表データの公開元(CC0ライセンス)
- 暦計算室:国立天文台
- PHPでテキスト中の和暦・西暦年号を統一する(その2):ぱふぅ家のホームページ
- PHPでテキスト中の和暦・西暦年号を統一する:ぱふぅ家のホームページ
- PHPで過去の元号を分析する:ぱふぅ家のホームページ
- 太陰太陽暦:Wikipedia
- ユリウス通日:Wikipedia
参考書籍
|
|
日本暦日原典 第五版 | ||
| 著者 | 内田正男 | ||
| 出版社 | 雄山閣 | ||
| サイズ | 事・辞典 | ||
| 発売日 | 2023年11月 | ||
| 価格 | 19,800円(税込) | ||
| ISBN | 9784639029519 | ||
| 日本の暦日計算(旧暦「太陰太陽暦」から太陽暦への換算)のための画期的大著。明治以来長らく使われてきた『三正綜覧』の誤りを正した本書の出現によって暦日計算は正確かつ容易になり、現在の様々な暦日データ・ベースの多くが、旧暦と新暦の間の換算や干支・閏月・大小などの参考資料として本書を基礎としている。簡単に換算できる本書の利用方法も付記し、あらゆる歴史的仕事にたずさわる誰もが活用できる必携の書が30年ぶりについに復刊。 | |||


今回、AIエージェント「Claude Code」に調べてもらったところ、旧暦と西暦を相互変換するための対照表データが公開されていることが分かった。そこで、この対照表データを使い、月日まで正確に西暦⇔和暦(旧暦)変換ができるPHPプログラムをつくることにする。あわせて、閏月(うるうづき)にも対応させ、さらに明治6年以降の近現代(明治・大正・昭和・平成・令和)についても、変換できるようにする。
なお、日本が太陰太陽暦からグレゴリオ暦(現在の暦)に切り替えたのは明治6年(1873年)1月1日である。それより前(大化元年~明治5年)は太陰太陽暦の対照表を使って月日を変換し、それ以降(明治6年~令和)は現在と同じ暦なので、従来からある元号変換テーブルだけで変換する。