PHPでテキスト中の和暦・西暦年号を統一する(その3)

(1/1)
「PHPでテキスト中の和暦・西暦年号を統一する(その2)」では、飛鳥時代の元号「大化」(645年)まで遡って、西暦の年と元号の年を相互変換できるようにした。ただし、月・日まで正確に変換するには、明治より前の日本で使われていた太陰太陽暦(いわゆる旧暦)の知識が必要になり面倒なので、そのときは棚上げにしていた。

今回、AIエージェント「Claude Code」に調べてもらったところ、旧暦と西暦を相互変換するための対照表データが公開されていることが分かった。そこで、この対照表データを使い、月日まで正確に西暦⇔和暦(旧暦)変換ができるPHPプログラムをつくることにする。あわせて、閏月(うるうづき)にも対応させ、さらに明治6年以降の近現代(明治・大正・昭和・平成・令和)についても、変換できるようにする。

なお、日本が太陰太陽暦からグレゴリオ暦(現在の暦)に切り替えたのは明治6年(1873年)1月1日である。それより前(大化元年~明治5年)は太陰太陽暦の対照表を使って月日を変換し、それ以降(明治6年~令和)は現在と同じ暦なので、従来からある元号変換テーブルだけで変換する。

目次

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

PHPでテキスト中の和暦・西暦年号を統一する(その3)
「西暦(和暦)」を選ぶと、テキスト中の年号を主表記(もう一方の表記)に変換する。たとえば「天正十五年四月一日」であれば「1587年5月8日(天正15年4月1日)」のように、月日まで太陰太陽暦を計算したうえで変換する。

サンプル・プログラムのダウンロード

圧縮ファイルの内容
nengo3.phpサンプル・プログラム本体
pahooNormalizeText.phpテキスト正規化クラス pahooNormalizeText。
テキスト正規化クラスの使い方は「PHPで日本語テキストを正規化」を参照ください。include_pathが通ったディレクトリに配置してください。
qreki_data.json旧暦(太陰太陽暦)対照表データ(JSON形式)。出典・ライセンスは後述。pahooNormalizeText.phpと同じフォルダに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
nengo3.php 更新履歴
バージョン 更新日 内容
1.0.0 2026/09/23 初版
pahooNormalizeText.php 更新履歴
バージョン 更新日 内容
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を格納するようにした
pahooInputData.php 更新履歴
バージョン 更新日 内容
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');

本プログラムは、クラス・ファイル "pahooInputData.php","pahooNormalizeText.php" を利用する。PHPのクラスについては、「PHPでクラスを使ってテキストの読みやすさを調べる」を参照のこと。

クラス・ファイルの読み込みは組み込み関数  require_once  を用いて行う。パス名は絶対値で指定するか、事前に "php.ini" の include_path で指定されるパスにクラス・ファイルを配置する。

新たに追加した旧暦データ・ファイル "qreki_data.json" は、include_path とは関係なく、クラス・ファイル "pahooNormalizeText.php" 自身と同じフォルダに置くこと。プログラム側の実行時カレント・フォルダに関わらず、組み込み関数  dirname  と [__FILE__:php_language] を使ってクラス・ファイル自身の場所を求め、そこからの相対パスでデータ・ファイルを読み込むようにしている。

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。

解説:旧暦(太陰太陽暦)データ

明治より前の日本では、月の満ち欠けを基準にしつつ、季節とのズレを閏月(うるうづき)で補正する「太陰太陽暦」(いわゆる旧暦)が使われていた。1か月の日数は29日(小の月)または30日(大の月)で、年によって決まっており、単純な計算式では求められない。

そこで、旧暦と西暦(大化元年~明治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か月になる年がある。
qreki_data.json:年レコードの構造
キー 内  容
元日その年の旧暦1月1日にあたる西暦(または[ユリウス暦:blue])の年月日("[年/月/日:blue]" 形式)
大小その年の各月が[大の月:blue](30日)か[小の月:blue](29日)かを、[1:blue]/[0:blue] で先頭の月から順に並べた配列
閏閏月がある年だけ存在するキー。何番目の月のあとに閏月が入るかを示す数値
明治3年(1870年)のレコード(実例)
元日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: }

旧暦と西暦を月日まで正確に変換するには、日付同士の前後関係や日数差を単純な引き算で求められる「ユリウス通日」(JDN。紀元前4713年1月1日を起点とする通し日数)に変換すると都合がよい。ユーザー関数 ad2jdn は西暦の年月日をユリウス通日に、jdn2ad はその逆を求める。「PHPで過去の元号を分析する」でも、期間計算のためにユリウス日を利用した。

西暦1582年10月15日、グレゴリオ暦(現在の暦)が導入され、それより前はユリウス暦が使われていた。両者は計算式が異なるため、ad2jdn と jdn2ad は、この日付を境にグレゴリオ暦用/ユリウス暦用の計算式を切り替えている。

解説:西暦を旧暦に変換

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: }

ユーザー関数 ad2qreki は、西暦の年月日を旧暦(年・月・日・閏月かどうか・元号)に変換する。まず ad2jdn でその日のユリウス通日を求め、前述の "qreki_data.json" から対象年(旧暦の元日は1~2月にあるため、前年のレコードに該当することもある)を探し出す。その年の 大小 配列を先頭の月から順に積算し、ユリウス通日がどの月に収まるかを突き止める。閏月かどうかの判定はユーザー関数 qreki_month_label が受け持つ。

戻り値の 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: }

逆に、ユーザー関数 qreki2ad は旧暦を西暦に変換する。旧暦の年は単独の数値ではなく、元号と元号年(例:天正, 15)の組み合わせで指定する仕様にした。単独の西暦年をそのまま使いたいときは、元号に空文字 '' を指定すればよい。

処理としては、まず era2ad を使って元号+元号年を西暦年に変換し、その西暦年の "qreki_data.json" レコードを探す。大小 配列を先頭の月から順に積算してユリウス通日を求め、jdn2ad で西暦の年月日に変換する。指定した月・日・閏月の組み合わせが実在しない場合は FALSE を返す。

解説:近現代(明治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: }

前述のとおり、"qreki_data.json" がカバーするのは明治5年12月2日までである。明治6年1月1日以降(明治・大正・昭和・平成・令和)は現在と同じグレゴリオ暦なので、旧暦データを調べる必要はなく、「その2」で作った ad2era/era2ad だけで変換できる。

そこで、この境界を自動的に判断してくれるユーザー関数 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: }

ユーザー関数 wareki は、任意の文字列中の西暦を和暦へ変換する。正規表現でテキスト中の年号表記にパターンマッチさせ、組み込み関数  preg_replace_callback  を使ってユーザー関数 seireki2wareki に置換させる仕組みは「その2」と同じである。

今回、閏月にも対応させるため、正規表現に「閏」の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: }

ユーザー関数 seireki は、任意の文字列中の和暦を西暦へ変換する。年号を取り出すパターンは前述の wareki と同じで、正規表現で捉えた「閏」の有無をユーザー関数 wareki2seireki の中で wareki2ad に渡している。

年号の前に付いている文字列が 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: }

ユーザー関数 mixture は、西暦・和暦のどちらの表記でも「主表記(もう一方の表記)」の形に統一する。年号の前に付いている文字列が元号であれば、元のテキストを和暦側として西暦を計算し、元号でなければ、元のテキストを西暦側として和暦(旧暦)を計算する。どちらの場合も変換は1回だけ行い、西暦→和暦→西暦のように往復させることはしない。往復変換にすると、月・日が省略された年号(たとえば「1586年3月」)で、元号をまたぐ際に月がずれてしまうことがあったためである。

月・日は、元のテキストで指定されていた精度のぶんだけ付加する。たとえば年だけの表記なら年だけを、年月までの表記なら年月までを変換後の文字列に含める。西暦・和暦が一致する場合(変換のしようがない場合)は、括弧を付けずにそのまま返す。

参考サイト

参考書籍

"qreki_data.json" のもととなった旧暦対照表データは、下記書籍(大化元年~明治5年の期間)および国立天文台暦計算室の資料(明治6年以降)をもとに作成されたものである。
表紙 日本暦日原典 第五版
著者 内田正男
出版社 雄山閣
サイズ 事・辞典
発売日 2023年11月
価格 19,800円(税込)
ISBN 9784639029519
日本の暦日計算(旧暦「太陰太陽暦」から太陽暦への換算)のための画期的大著。明治以来長らく使われてきた『三正綜覧』の誤りを正した本書の出現によって暦日計算は正確かつ容易になり、現在の様々な暦日データ・ベースの多くが、旧暦と新暦の間の換算や干支・閏月・大小などの参考資料として本書を基礎としている。簡単に換算できる本書の利用方法も付記し、あらゆる歴史的仕事にたずさわる誰もが活用できる必携の書が30年ぶりについに復刊。
 
(この項おわり)
header