【PHP】西暦から和暦・和暦から西暦を算出する

[西暦から干支・干支から西暦を算出する](https://qiita.com/Kokusui/items/79da562e4e979aea1825 "西暦から干支・干支から西暦を算出する")に続き暦ネタ。
#### 西暦から和暦を算出
```seireki2wareki.php
function seireki2wareki(string $datestr = NULL) {
 //datestrで与えられた年月日(の属する暦年)の和暦年を返す ただし1968年～2100年の範囲
 if (!$datestr) {$datestr = date("Y-m-d");} //datestrが空欄の場合は今日の和暦
 else {
  $datestr = mb_convert_kana($datestr, 'n'); //全角数字は半角に直す
  if (is_numeric($datestr = str_replace('年', '', $datestr))) {
     //datestr(「年」がついていたら取る)が数値文字列の場合(＝年のみで月日が入ってない場合)
   if (intval($datestr) >= 1868 && intval($datestr) <= 2100) {
    //1868～2999の範囲であればその年の1月1日の和暦
    $datestr .= '-1-1';
   } else {return NULL;} //数値しか入っていないが範囲外だったらNULLを返す
  } else {$datestr = str_replace(['年', '月', '日'], ['/', '/', ''], $datestr);} //日本語入力
 }
 if ((date_parse($datestr)['warning_count'] + date_parse($datestr)['error_count']) === 0) {
  $input_date = date(strtotime($datestr)); //日付書式のチェック
  $y = date_parse($datestr)['year'];
  if ($y >= 1873) {
   if ($y <= 1911) {return '明治' . ($y - 1867) . '年';}
   else if ($input_date <= strtotime('1912-7-29')) {return '明治45年';}
   else if ($y == 1912) {return '大正元年';}
   else if ($y <= 1925) {return '大正' . ($y - 1911) . '年';}
   else if ($input_date <= strtotime('1926-12-25')) {return '大正15年';}
   else if ($y == 1926) {return '昭和元年';}
   else if ($y <= 1988) {return '昭和' . ($y - 1925) . '年';}
   else if ($input_date <= strtotime('1989-1-7')) {return '昭和64年';}
   else if ($y == 1989) {return '平成元年';}
   else if ($y <= 2018) {return '平成' . ($y - 1988) . '年';}
   else if ($input_date <= strtotime('2019-4-30')) {return '平成31年';}
   else if ($y == 2019) {return '令和元年';}
   else {return '令和' . ($y - 2018) . '年';}
  } else {
   //1872年以前は旧暦を使用していたため日によって西暦とのズレがある
   if ($input_date >= strtotime('1872-2-9')) {
    return '明治5年';
   } else if ($input_date >= strtotime('1871-2-19')) {
    return '明治4年';
   } else if ($input_date >= strtotime('1870-2-1')) {
    return '明治3年';
   } else if ($input_date >= strtotime('1869-2-11')) {
    return '明治2年';
   } else if ($input_date >= strtotime('1868-10-23')) {
   //1868年10月23日に改元したため
    return '明治元年';
   }
  }
 }
 return NULL;
}
```
##### 西暦→和暦の解説
引数は日付(ぽい)文字列あるいは西暦年を受け付ける(空欄の場合は今日が属する和暦)。1968～2100年に対応しているが、和暦は西暦年の途中で切り替わるため、年だけでなく日付込で計算するようにしている(年だけ入れた場合は該当年の1月1日の和暦を返す)。さらに1872年以前は旧暦を使用していたため、こちらも和暦と西暦がズレる。
この切り替わり処理のためスマートとは程遠いスクリプトになってしまった。

さて、月日を省略した場合の処理についてはちょっとした罠がある。
```php
echo date("Y-m-d", strtotime('1990')); //1990-06-13 #日付は今日だが年は正しい
echo date("Y-m-d", strtotime('2012')); //2024-06-13 #なぜか今日の年月日
```
最初はこの理由がわからなかったがあれこれ調べてみると次が判明。
```php
var_dump(date_parse('2012'));
/*
array(12) {
  ["year"]=>
  bool(false)
  ["month"]=>
  bool(false)
  ["day"]=>
  bool(false)
  ["hour"]=>
  int(20)
  ["minute"]=>
  int(12)
  ["second"]=>
  int(0)
  ["fraction"]=>
  bool(false)
  ["warning_count"]=>
  int(0)
  ["warnings"]=>
  array(0) {
  }
  ["error_count"]=>
  int(0)
  ["errors"]=>
  array(0) {
  }
  ["is_localtime"]=>
  bool(false)
}*/
```
つまり```'2012'```が「20時12分」と解釈されてしまった結果[^1]であった。なので```'-01-01'```を付け加えて4桁の数字が年だよ、と解釈させている。

日付としてみなされない値や日付としてありえない値(2023/2/29とか2000/13/21とか)は```date_parse()```関数で判別して弾くようにしているが、```date_parse()```関数の挙動には次のような特徴がある。
```php
var_dump(date_parse('2023/02/29'));
// 日にちの数値は日付文字列の範囲内だが実際の日付として存在しない場合はWarning扱い
// '2024/04/31'なども同様
/*
array(12) {
  ["year"]=>
  int(2023)
  ["month"]=>
  int(2)
  ["day"]=>
  int(29)
  ["hour"]=>
  bool(false)
  ["minute"]=>
  bool(false)
  ["second"]=>
  bool(false)
  ["fraction"]=>
  bool(false)
  ["warning_count"]=>
  int(1)
  ["warnings"]=>
  array(1) {
    [11]=>
    string(27) "The parsed date was invalid"
  }
  ["error_count"]=>
  int(0)
  ["errors"]=>
  array(0) {
  }
  ["is_localtime"]=>
  bool(false)
}*/
```
```php
var_dump(date_parse('2024/05/32'));
// 日付文字列としてあり得ない日付を入れた場合はerror扱い
// '2023/13/01'とか'2024-Jon-20'などもほぼ同じ
/*
array(12) {
  ["year"]=>
  int(2024)
  ["month"]=>
  int(5)
  ["day"]=>
  int(3)
  ["hour"]=>
  bool(false)
  ["minute"]=>
  bool(false)
  ["second"]=>
  bool(false)
  ["fraction"]=>
  bool(false)
  ["warning_count"]=>
  int(0)
  ["warnings"]=>
  array(0) {
  }
  ["error_count"]=>
  int(1)
  ["errors"]=>
  array(1) {
    [9]=>
    string(20) "Unexpected character"
  }
  ["is_localtime"]=>
  bool(false)
}*/
```
したがって、日付として正しい文字列かどうかを判定する処理がこれ。
```php
if((date_parse($datestr)['warning_count'] + date_parse($datestr)['error_count']) === 0)
```
あとは力技で日付から年を出している。

#### 和暦から西暦を算出
```wareki2seireki.php
function wareki2seireki(string $datestr) {
 //datestrに入れた和暦年(明治～令和)を西暦にして返す
 //入力形式は　明治45年　(元号＋数字＋年)　ただし「元年」はOK
 //存在しない元号を入れた場合はNULLを返す
 //存在しない年はそのまま出力(例:平成40年 -> 2028)
 //明治4年以前は旧暦使用により西暦とのズレがあるため除外
 $wareki = [
  '明治' => 1867,
  '大正' => 1911,
  '昭和' => 1925,
  '平成' => 1988,
  '令和' => 2018
 ];
 $datestr = str_replace('元年', '1年', mb_convert_kana($datestr, 'n'));
 //「元年」を「1年」に、全角数字を半角に直した
 $gengou = mb_substr($datestr, 0, 2); //元号だけ抽出
 $nen = str_replace('年', '', mb_substr($datestr, 2)); //計算のため「年」を取る
 if (in_array($gengou, array_keys($wareki)) && ($gengou != '明治' || $nen > 4)){
  return $wareki[$gengou] + intval($nen);
 }
 return NULL;
}
```
##### 和暦→西暦の解説
こちらは和暦が途中で切り替わっても「昭和64年」と「平成元年」が同じ1989年なので処理がだいぶ簡単。
ただ明治4年以前の旧暦処理は~~やはり面倒くさい~~月日まで入れさせる必要があるので処理をスキップするようにした。```($gengou != '明治' || $nen > 4)```の部分が該当するが「『元号が明治でかつ年が4以下の場合』でない場合は処理をする」を「『元号が明治でない』または『年が4より大きい』場合は処理をする」に変換した結果[^2]。
開き直って「入力年の1月1日の西暦/和暦を出す」みたいな仕様にしてしまえば、用途はともかく江戸時代以前にも対応できるスクリプトができるかもしれない[^3]。

なお元号としてはありえない年でもそのまま出力するようにしているため
```php
echo wareki2seireki('昭和100年'); //2025
```
「おお、来年は昭和100年なのだな!」ということがわかったりして面白い。

[^1]:4桁が時刻としてあり得る数値(```'1038'```とか```'2359'```とか)の場合は時刻(10:38や23:59)として解釈され、それ以外(```'1099'```や```'2501'```)だと年として処理される。
[^2]:覚えててよかった[ド・モルガンの法則](https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%BB%E3%83%A2%E3%83%AB%E3%82%AC%E3%83%B3%E3%81%AE%E6%B3%95%E5%89%87)である。
[^3]:4文字の元号も存在するので```$gengou = mb_substr($datestr, 0, 2);```の部分がそのままでは使えない。