2020/04/24 追記
気がついたら、2020/04/24 現在、少なくとも Blazor WebAssembly v.3.2 Preview 5 では、令和対応が済んでいました。
気がついたら、少なくとも Blazor WebAssembly v.3.2 Preview 5 では、令和対応が済んでいました。 pic.twitter.com/jGPw95JKOm
— @jsakamoto (@jsakamoto) April 24, 2020
ということで、以下はあくまでも記録のために、当時の記事をそのまま掲載とします。
先に結論
なかなかに難題だった。
Mono ランタイムの改訂を待つべし。
元ネタ
先日 2019年5月29~30日で開催された日本マイクロソフト主催の技術カンファレンス「de:code 2019」、その2日目にあったセッション「MW51 C# ドキドキ・ライブコーディング対決 @ de:code - ONLY C#!! Blazor Web 開発バトル -」で出されたお題です。
西暦による日付入力を和暦表記に変換して表示する Blazor (Client-side) アプリを作って下さい、そこでちゃんと「令和元年」と表示されるようにしてください、というチャレンジでした。
まずは普通に C#/.NET における和暦表示をやってみる
まずは普通に System.Globalization.CultureInfo
と System.Globalization.JapaneseCalendar
を用いて、DateTime
値を和暦に書式化する実装を試みます。
主旨としてはこんな感じ。
var cultureInfo = new CultureInfo("ja-JP", false);
cultureInfo.DateTimeFormat.Calendar = new JapaneseCalendar();
var date = new DateTime(2019, 6, 6);
date.ToString("gy'年'M月d日", cultureInfo);
セッション中、 「(Client-side) Blazor でも CultureInfo 使えるんだ!?」 という声もあがっておりましたが、はい、使えます。
Blazor は JavaScript へのトランスパイラではありません。
.NET のバイナリをそのままロードして動作する .NET ランタイムがブラウザ内で動作しているものです。
その仕組みから、 System.Globalization.CultureInfo
や System.Globalization.JapaneseCalendar
が使えたりします。
とはいえ、上記主旨のコードを Blazor 上で実行すると...
...がーん
たしかに和暦にはなるのですが、 「令和元年」ではなく「平成31年」 になってしまっています。
ちなみに上記主旨のコードを、Client-side Blazor ではなく、Server-side Blazor で実行すると、ちゃんと「令和元年」で表示されます。
どうしてでしょうか!?
Client-side Blazor で「令和」表示されない理由
Client-side Blazor で「令和」表示されない理由ですが、結論としては、Blazor の .NET 実行環境である Mono ランタイム、その mscorlib 実装にはまだ「令和」が来てないっぽい のです。
Mono の GitHub リポジトリ上のこの辺りに、Mono ランタイムにおける System.Globalization.JapaneseCalendar
の実装コードがあります。
これを見るに、和暦情報の配列を初期化している箇所に「令和」がまだ掲載されていないようなのです (master ブランチしか見ていませんが)。
ちなみに、.NET Core 3.0 Preview5 のランタイムにおける System.Globalization.JapaneseCalendar
の実装コードは、GitHub 上、こちらで確認できます。
これを見ると .NET Core 3.0 Preview 5 実装には「令和」対応が含まれているようであることがわかります。
なお、ランタイムの実装コード中にインラインで「令和」対応が組み込まれていなくても、これら実装コードのソースを見ると、
japaneseEraInfo = GetErasFromRegistry();
というように、レジストリの和暦定義を読み込んで、それを優先して使用するコードになっています。
ですが Client-side Blazor は、いくら .NET 実行環境であるとはいえ、Web ブラウザの WebAssembly エンジン上で動作する仕掛けです。
そのため、さすがにブラウザ内のコードからレジストリを読み取ることは不可能です。
実際、Blazor に読み込まれる mscorlib の実装では、上記 GetErasFromRegistry()
メソッドは、null を即値で返すだけの実装になっていました。
じゃぁどうするか?
先に示した、Mono と .NET Core 各々の System.Globalization.JapaneseCalendar
実装を見比べるに、Mono の実装における和暦定義の配列の初期化コードに、「令和」の定義をちょこっと書き足せば、Mono ランタイムでも「令和」対応が済んでしまいそうです。
ということで、Client-side Blazor が公式リリースする頃には、Cient-side Blazor でも「令和」対応が完了しているかもしれません。
ただ、実際のところ、Mono の mscorlib 実装について .NET Core とのソースコードの統一がどんな感じで進捗するのか自分はよくわかっていませんし、Mono ランタイムに対して「令和」対応が進んでいるのかどうか、そもそも Issue が挙がってたりするのかどうかも現時点では調べていません。
なぜ調べもしていないのかというと、「今すぐ Client-side Blazor で令和表示したいから!」ですw
Mono ランタイムにおける JapaneseCalendar
が令和対応し、かつそのランタイムで Client-side Blazor がビルドされるようになるのを待ちきれない次第です。
次なる作戦は...
「今すぐ Client-side Blazor で令和表示したい」という場合、それでは現時点でどんな方法があるでしょうか?
つまるところ、DateTime
値を和暦表記に書式化する仕事を担っているのはどうやら JapaneseCalendar
クラスです。
ということは、JapaneseCalendar
と同じように、Calendar
クラスから派生したオレオレ Calendar クラスを独自実装して「令和」対応も含めた和暦書式化を行えるようにし、それを CultureInfo
インスタンスに設定してやればよいのではないでしょうか!
幸い、お手本となるソースコードは GitHub 上で入手し、オープンソースライセンス下で再利用可能です。
ということで、Mono ないしは .NET Core の JapaneseCalendar
のソースコードをちょっと拝借し、前述のとおり、和暦定義の配列初期化コードをちょこっと書き換えただけのオレオレ Calendar クラスを書き上げたら、速攻で令和対応できそうな気がします。
やってみたのだけれど...
ということで早速やってみたのですが...
JapaneseCalendar
の実装、想定していた以上に internal な各種実装と癒着しているようで、芋づる式に、あのクラス、このクラス、をどんどんコピーしなくてはならなくなり、早々にやる気が萎えてしまいましたw
かといって、Mono や .NET Core のソースコードに頼らずにスクラッチでオレオレ Calendar クラスを実装する技術力も根性も自分にはなく... orz
モンキーパッチ
ここまで来て、ついに手を出したのが、リフレクションによるモンキーパッチです。
繰り返しになりますが、JapaneseCalendar
の中でハードコーディングされている和暦定義の配列をいじれば、令和対応可能です。
実際、その和暦定義の配列は、下記のように静的プロパティに格納されているようです。
static internal volatile EraInfo[] japaneseEraInfo;
ただ、この静的プロパティのアクセス制御が、上記のとおり internal 指定されているため、普通には手が出せません。
そこで、.NET のリフレクション機能を使って、禁断の、内部実装をハックする、という手段に出ましたw
和暦定義のクラスも internal なため、リフレクションを駆使しまくっての実装となりますが、どうにかこうにか、下記のようなコードでハックすることができました (下記コードの詳しい解説は省きます)。
var japaneseEraInfoField = typeof(JapaneseCalendar).GetField("japaneseEraInfo", BindingFlags.NonPublic | BindingFlags.Static);
var typeOfEraInfo = typeof(JapaneseCalendar).Assembly.GetType("System.Globalization.EraInfo");
const int GregorianCalendar_MaxYear = 9999;
var eraSets = new object[][] {
new object[] { 5, 2019, 5, 1, 2018, 1, GregorianCalendar_MaxYear - 2018, "\x4ee4\x548c", "\x4ee4", "R" },
new object[] { 4, 1989, 1, 8, 1988, 1, 2019 - 1988, "\x5e73\x6210", "\x5e73", "H" },
new object[] { 3, 1926, 12, 25, 1925, 1, 1989 - 1925, "\x662d\x548c", "\x662d", "S" },
new object[] { 2, 1912, 7, 30, 1911, 1, 1926 - 1911, "\x5927\x6b63", "\x5927", "T" },
new object[] { 1, 1868, 1, 1, 1867, 1, 1912 - 1867, "\x660e\x6cbb", "\x660e", "M" }
};
var eraInfoArray = Array.CreateInstance(typeOfEraInfo, eraSets.Length);
var index = 0;
foreach (var eraSet in eraSets)
{
var args = eraSet;
var eraInfo = Activator.CreateInstance(typeOfEraInfo, BindingFlags.NonPublic | BindingFlags.Instance, null, args, null);
eraInfoArray.SetValue(eraInfo, index++);
}
japaneseEraInfoField.SetValue(null, eraInfoArray);
上記のコードをプログラム開始時に実行しておくことで...
ついに Client-side Blazor でも「令和」表記となりました!
まとめ
上記のリフレクションによるモンキーパッチを、NuGet パッケージにまとめて公開しました。
Toolbelt.Mono.JapaneseEra.Reiwa.MonkeyPatch
詳しい使い方は上記リンク先に記載されているドキュメントなどを参照ください。
ソースコードは GitHub 上に公開してあります。
実際に Client-side Blazor で「令和」表記しているライブデモを、GitHub Pages 上で稼働させています (下記 URL)。
https://jsakamoto.github.io/Toolbelt.Mono.JapaneseEra.Reiwa.MonkeyPatch/
なお、当然のことながら、Mono ランタイムの内部実装が変れば、このモンキーパッチはあっという間にクラッシュします。
本来であれば、
- Mono ランタイムの mscorlib 実装の動向を確認し、令和対応を待つ。待つだけでなく、適宜、令和対応のための Issue や Pull Request を送る。
-
System.Globalization.Calendar
から派生したオレオレ Calendar クラスを実装し、そこで令和を含めた和暦書式化をスクラッチで実装する。
といった手を打つべきです。
自分は気力が尽きたので、いったんこのモンキーパッチでお茶を濁します。
ですがまたいつの日か、まだ Client-side Blazor で令和対応が完了していなければ、上記王道のいずれかに再チャレンジするかもしれません。
...ということで、Client-side Blazor で令和表示、まったくもって10分で終わらず、なかなかに大変でした!