和暦年から西暦年に変換

元号が令和に代わるとき、システム改修を行った際に考慮したメモです。

## 要件

和暦の元号と年から西暦年へ変換したいことはよくあるかと思います。
例えば、自分の誕生日を年月日バラバラで入力する項目などですね。

本来、元号が変わる際は月日も考慮する必要があります。
例えば、西暦2019年は二つの元号を考慮する必要がありますが、月日まで指定されると、元号は一つに絞られます。

+ 平成31年 4月 30日 ⇔ 2019年 4月 30日
+ 令和元年 5月 1日 ⇔ 2019年 5月 1日

ただ、上記からわかる通り、和暦年だけからでも西暦年に変換することは可能です。  

≪今回の要件前提≫

+ 和暦の元号と年のみから西暦年を取得したい。
+ 和暦の元号は、近代の**明治以降**のみ考慮する。
+ 和暦の元号の末年以上の年を入力されても、エラーとせず西暦に変換したい。  
  例えば、平成32年は存在しないが2020年として取得したい。
+ タプルを事前に用意せず、システムのデータを利用する。  
  今回私が対応した案件は Windows アプリ（C#）なので、Windows（.NET Framework）のシステムデータを使い、和暦年から西暦年へ変換する。

## 結論を先に

C# で和暦から西暦へ変換する際には、年だけでなく月日のデータが必要です。
（他の言語でも、基本は年月日のすべてが必要と思います。）  

変換に利用する仮の月日は、**12月31日** を指定する方法がスマートです。

```C#

using System.Globalization;

string era;   // 元号の文字列
int eraYear;  // 和暦の年

CultureInfo culture = new CultureInfo("ja-JP", true);
culture.DateTimeFormat.Calendar = new JapaneseCalendar();

string warekiDate = era + eraYear.ToString() + "/12/31";
DateTime date = DateTime.Parse(warekiDate, culture.DateTimeFormat);

int year = date.Year;   // 西暦の年
```


## なぜ12月31日を指定するのか

分かりやすさで言えば、1月1日でもよい気がします。
これには２つ理由があります。

### 【明治対応のため】
元号は1月1日など固定された日から変わるものではなく、ある日を境に突然変わるものです。
当然、明治元年も1月1日からではありません。
（当時は1月1日からみたいな内容もありましたが、話がずれるため Wikipedia なんかで調べてもらえればと思います。）  

明治元年は現在の暦上で、**1868年1月25日**から始まります。（Wikipedia 調べ）
また .NET Framewrok 上では、**1868年9月8日**からのみ明治の日付データとして受け入れ可能となります。

上記のプログラムで1月1日を指定していた場合、明治元年がエラーで変換できなくなります。

### 【改元年に元号が確実に変更されている日であるため】
そもそもの、元号と年について考えてみます。

* 新元号に変わる日は、だれにもわからない。
* 新元号元年(新元号1年)から新元号2年に変わる日は、**新元号元年の翌年の1月1日**である。

上記をもとに、新しい元号に変わったケースをいろいろ考えてみます。
テストケースは、年末年始及び過去の改元日です。

|改元日| 年始 | 旧元号末日 | 新元号初日 | **年末** |
:----:|:----:|:----------:|:----------:|:----------:
|1月 1日|新元号年 1月 1日|*(前年の 12月 31日)*|新元号年 1月 1日|**新元号年 12月 31日**|
|1月 8日|旧元号年 1月 1日|旧元号年 1月 7日|新元号年 1月 8日|**新元号年 12月 31日**|
|1月 25日|旧元号年 1月 1日|旧元号年 1月 24日|新元号年 1月 25日|**新元号年 12月 31日**|
|5月 1日|旧元号年 1月 1日|旧元号年 4月 30日|新元号年 5月 1日|**新元号年 12月 31日**|
|7月 30日|旧元号年 1月 1日|旧元号年 7月 29日|新元号年 7月 30日|**新元号年 12月 31日**|
|12月 25日|旧元号年 1月 1日|旧元号年 12月 24日|新元号年 12月 25日|**新元号年 12月 31日**|
|12月 31日|旧元号年 1月 1日|旧元号年 12月 30日|新元号年 12月 31日|**新元号年 12月 31日**|

つまり、元号が変わった年の12月31日は必ず新元号であることが確実なのです。
逆を言うとそれ以外の日は、旧元号の日付であるか、新元号の日付であるかは、その改元が訪れるまで分からないということになります。
その都度、改修やテストが必要となることは避けておきたいですよね。

新元号の元年を西暦の年に直したい場合、12月31日で変換するとエラーとならずに確実なのです。

---
## 考えられる追加要件

今回の案件は上記までの対応でOKだったため、追加の考慮は必要なかったのですがメモとして記載しておきます。

### 元号の末年以上の年を許容しない
例えば、平成32年は存在しないのでエラーとしたい、もしくは正しい和暦に変換したい等の「あるある」要件です。  
方法としては、以下の2つが考えられます。

≪前提≫

+ 和暦を年だけで判定する場合は、改元前の末年と改元後の元年は同じ年である
+ 和暦を年月日で判定する場合は、月日に応じて改元前と改元後の元号と和暦年を考慮する

#### 和暦年⇔西暦年の再変換を利用
和暦年→西暦年→和暦年と再変換させるやり方です。
1. 上記のロジックを使用して和暦年→西暦年に変換
2. 再度、西暦年から和暦年に変換
3. 上記の和暦が等しいか判定
4. （月日を考慮しない場合）再変換後の和暦が元年だった場合、改元前の末年と等しいか判定する処理が追加で必要

利点

+ OSやライブラリの仕様に準ずることができるため、後々手を入れる必要がない。（と思いたい。）
+ 西暦から和暦に再変換した値を再利用できたりなんかするといいなぁ。

欠点

+ サポート切れのOSなんかを使用されている顧客に対応しづらい。
+ 演算コストがかかるので、大量のデータ処理には不向き。

#### 元号と西暦のタプルを用意
***個人的にはおススメしない方法です。***
元号が始まった日を、元号と組み合わせてマスターデータのように保持させます。
例えば、『令和』であれば、『2019-05-01』をタプルとして保持します。

1. 和暦元年の年月日データを事前に準備
    * {明治, 1868/1/25}
    * {大正, 1912/7/30}
    * {昭和, 1926/12/25}
    * {平成, 1989/1/8}
    * {令和, 2019/5/1}
2. 上記のロジックを使用して和暦年→西暦年に変換
3. 入力値とタプルの日付を比較
4. （月日を考慮しない場合）再変換後の和暦が元年だった場合、改元前の末年と等しいか判定する処理が追加で必要

利点

+ OSやライブラリの仕様に非依存のため、移植がかんたん。
+ OSやライブライ非依存のため、サポート終了後も対応可能。

欠点

+ 元号が増えるたびに、追加作業や適用作業が必要。
+ 作り方によっては、作者しか仕様を把握していない。
+ **つまり、自分で責任もってメンテしなければならない。（大問題）**

---

## 最後に

これは、新人教育の良い例題だなーと思いつつ書きました。
考慮漏れ等指摘があれば、よろしくお願いします！

## 参考ページ
* Wikipedia 元号一覧\_(日本) - https://ja.wikipedia.org/wiki/%E5%85%83%E5%8F%B7%E4%B8%80%E8%A6%A7_(%E6%97%A5%E6%9C%AC)
* Microsoft Docs - https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.japanesecalendar?view=netframework-4.8