.NET Core プロジェクトによる元号（和暦）表示の注意点

# この記事は

　数ヶ月前に散々騒がれていた新元号とはまた別の話です。

　具体的には、元号を **アルファベット一文字** で表記する際の注意点になります。

# 前提条件

　環境は下記の通りになります。

- Windows 10 1809 Build 17763.475
- CentOS 7.6.1810
- どちらも .NET Core 3.0.100-preview5-011568

# 元号表示に利用するコード

　基本的な部分は一般的な元号表記と同様に `System.Globalization` 名前空間の `CultureInfo` と `JapaneseCalendar` を使用しています。

```csharp
var cultureInfo = new CultureInfo("ja-JP", false);
var dateTimeFormat = cultureInfo.DateTimeFormat;

dateTimeFormat.Calendar = new JapaneseCalendar();

var eraSymbols = new Dictionary<int, char>();

for (var eraSymbol = 'A'; eraSymbol <= 'Z'; eraSymbol++)
{
    var eraIndex = dateTimeFormat.GetEra(eraSymbol.ToString());

    if (eraIndex > 0)
    {
        eraSymbols.Add(eraIndex, eraSymbol);
    }
}

if (eraSymbols.Any())
{
    foreach (var pair in eraSymbols)
    {
        Console.WriteLine($"eraIndex = {pair.Key}, eraSymbol = {pair.Value}");
    }
}
else
{
    Console.WriteLine($"{nameof(eraSymbols)} is empty.");
}
```

　このコードをWindows上で実行すると、下記のような出力結果になります。

```
eraIndex = 4, eraSymbol = H
eraIndex = 1, eraSymbol = M
eraIndex = 5, eraSymbol = R
eraIndex = 3, eraSymbol = S
eraIndex = 2, eraSymbol = T
```

　念のためにもっとわかりやすく表現すると、こういうことです。

|`eraIndex`|`eraSymbol`|元号|
|:-:|:-:|:-:|
|1|M|明治|
|2|T|大正|
|3|S|昭和|
|4|H|平成|
|5|R|令和|

 この `eraIndex` は、例えば上記のコードの続きに `dateTimeFormat.GetEraName(eraIndex)` というようにすれば漢字表記で元号を返します。また、 `dateTimeFormat.GetAbbreviatedEraName(eraIndex)` とすれば漢字表記の先頭一文字目を返します。

```csharp
var eraIndex = 1;
dateTimeFormat.GetEraName(eraIndex); // => 明治
dateTimeFormat.GetAbbreviatedEraName(eraIndex) // => 明
```

　これにより、 `eraIndex` をもとに漢字二文字の正式名称、漢字一文字の略称、アルファベット一文字の略称を利用できるわけなのです。

　ここまでがこの記事の導入になります。

# 問題点

　若干いまさらなお話ですが、.NET Coreというのはクロスプラットフォームでの動作になるので、同様の処理をLinuxでも行えるはずなのです。当然、Windows環境で作成した上記のコードもLinuxで実行することができます。

　ということで、CentOS上で上記コードを実行しました。

```
$ dotnet run
eraSymbols is empty.
```

　上記コードでDictionaryが空っぽだった際のコンソール出力処理があることもあり予定調和的ですが、CentOS上では `eraIndex` を取得することができませんでした。

　では一体、それはなぜなのでしょうか？

# そもそも元号情報はどこから来るのか

　簡潔に言うと元号情報は、Windowsで実行すればレジストリから、macOSやLinuxの環境ではlibicuから取得します。そのため、改元の対応としてWindows Updateによるレジストリの更新やlibicuの更新のみで元号を追加することができます。

　ちなみに、レジストリからもlibicuからも取得できなかった場合は、.NET Coreのソースにハードコーディングされている情報を引っ張ります。

　このあたりは下記ページが参考になります。

- [.NET Coreにおける新元号対応について - 銀の光と碧い空](https://tech.tanaka733.net/entry/2018/03/a-new-era-for-dotnet-core)

　そういった情報を踏まえると、WindowsとLinuxで動作が異なるのは「レジストリとlibicuで元号情報が異なっているのではないか？」という考えにいたります。

# 実際の処理内容

　調査するにあたって、実際に `System.Globalization.DateTimeFormatInfo.GetEraName` の[ソースコード](https://github.com/dotnet/coreclr/blob/v3.0.0-preview6.19303.73/src/System.Private.CoreLib/shared/System/Globalization/DateTimeFormatInfo.cs#L517)を見てみましょう。

　簡単にすると、以下の手順で処理しています。

1. `EraNames` に引数がないか探す。
2. `AbbreviatedEraNames` に引数がないか探す。
3. `AbbreviatedEnglishEraNames` に引数がないか探す。

　これらはすべてstring型の配列でそれぞれ指数が同じ元号を示すようになっているため、引数が見つかった段階でそのインデックス ＋ 1の値を返しています。なお、ソースコードを見ればわかりますが、正しくは上記3つの配列（プロパティ）ではなく異なる配列を探しています。ですがこの場では命名としてこちらのほうが意味がわかりやすいことと、**そもそも中身が同じなので、**このようにしています。

　さて、処理内容を見てみると、どうやらここで探している配列にレジストリやlibicuから取得した元号情報があるのではないかと予測することができますね。

　では実際にレジストリに登録されている元号情報を見てみると、下記のようになっています。

```:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese\Eras
明治_明_Meiji_M
大正_大_Taisho_T
昭和_昭_Showa_S
平成_平_Heisei_H
令和_令_Reiwa_R
```

　これを見たときに気になることがありませんか？

　`System.Globalization.DateTimeFormatInfo.GetEraName` で探している対象の配列は3つなのに、レジストリを見ると4つのパターンがあるということについて、気になりますよね？

　処理の中で3パターンの表記の中から、引数の表記パターンが見つかったときにそのインデックスが返ってきます。つまり、4パターンの表記があるレジストリの持つ値のうち、引数で渡すとインデックスが返ってこない仲間はずれがいるのでは……？

　下記のコードをWindowsとCentOSでそれぞれ実行して確認してみましょう。

```csharp
Console.WriteLine($"平成 = {dateTimeFormat.GetEra("平成")}");
Console.WriteLine($"平 = {dateTimeFormat.GetEra("平")}");
Console.WriteLine($"Heisei = {dateTimeFormat.GetEra("Heisei")}");
Console.WriteLine($"H = {dateTimeFormat.GetEra("H")}");
```

```:Windows
平成 = 4
平 = 4
Heisei = -1
H = 4
```

```:CentOS
平成 = 4
平 = 4
Heisei = 4
H = -1
```

　……やっぱり仲間はずれがいましたし、さらに環境によっては仲間はずれにされるのが異なっているようですね。

　探す対象になっている3つの配列はそれぞれ直訳すると **「元号」「省略された元号」「省略された英語の元号」** となりますし、レジストリから取得した際の出力結果のほうが意味として正しいように感じます。

# まとめ

- .NET Core はクロスプラットフォームだけど、なんでも同じように動くわけではない。（少なくとも現在は）
- この記事の内容に関しては改元の話で盛り上がるよりも前から遭遇していたし解決もしたが、いつかまとめようと思っていたら改元の話題が始まって終わっていた。

　Qiita初投稿なのでなにか書き方が間違っていたりした場合は教えてください。もちろん、技術的に間違っている場合もどんどんご意見をください。