帳票などで、たまに和暦を処理する必要な時がありますね。例えば元号を表記するとか、和暦の年度を表記するなどの場合があるかと思います。Kotlin(JVM)の場合、西暦だとJavaのAPIのDateやLocalDateなどのAPIを使うと簡単ですが、和暦が必要となるのはごく一部のケースなので方法がなかなか分かりづらいかと思います。なので、今回はKotlinで和暦を扱う方法について少しまとめてみました。
JapanseEra / JapaneseDate
Javaでは、1.8から和暦で日付を扱えるJapaneseDate及び元号を扱えるJapaneseEraというAPIを提供しています。なのでJapaneseDateのインスタンスを作り、そこからJapaneseEraを取得することで簡単に元号の情報を取得できるようになります。実際の使い方は以下の通りです。
// 現在日付のJapaneseDateを取得
val japaneseDate = JapaneseDate.now()
// JapaneseEraの取得
val japaneseEra = japaneseDate.era
JapaneseDateの場合、LocalDateと同じくChronoLocalDateを継承しているのでインスタンスを作成する方法はそう変わりません。なので、以下のようなこともできます。
// LocalDateをJapaneseDateに変換
val japaneseDateFromLocalDate = JapaneseDate.from(LocalDate.now())
// 特定の日付を指定してJapaneseDate
val japaneseDateFromSpecificDate = JapaneseDate.of(2000, 12, 31)
元号を日本語で表記する
和暦を扱う場合にやりたいことは大きく二つかと思います。一つは、元号を文字列として扱うこと、そしてもう一つは、和暦での年度を数字として扱うことです。まずは、元号を文字列として取得できる方法について説明します。
まず上記で紹介した通り、JapaneseDateのインスタンスを取得した上で、さらにそのオブジェクトが保持しているJapaneseEraを取得する必要があります。その後、JapaneseEra.getDisplayName()という関数にTextStyleとLocaleを指定して文字列を取得することができます。前者は文字の出力型を指定する列挙型定数で、後者は言語の指定と思ってください。
TextStyleの場合、以下のような値があります。他の言語だと指定したものによって出力がかなり変わってくるかもしれませんが、日本語の場合はFULLとNARROWだけで十分ではないかと思います。
| 定数 | 出力例 |
|---|---|
FULL | 昭和 |
FULL_STANDALONE | 昭和 |
NARROW | S |
NARROW_STANDALONE | S |
SHORT | 昭和 |
SHORT_STANDALONE | 昭和 |
Localeの場合、Locale.JAPANやLocale.JAPANESEのどちらを指定しても結果は同じです。ただ、実装としては以下のようになるのでなるべくLocale.JAPANを使った方が良さそうです。
| Locale | 作られるBaseLocaleの設定 |
|---|---|
JAPAN | language = ja, region = JP |
JAPANESE | language = ja |
以下はこれらの定数を渡して元号を文字列として取得する例です。
val today = JapaneseDate.now()
val era = today.era
// 元号を漢字で取得
val eraName = era.getDisplayName(TextStyle.FULL, Locale.JAPAN) // 令和
元号だけでなく、年度までも合わせて表記したい場合もあるかと思います。その場合に使えるものはDateTimeFormatterです。これもJapaneseDateが実質LocalDateと同じくChronoLocalDateを継承しているから可能なことですね。
// 日付を日本語で表記する
val formatter = DateTimeFormatter.ofPattern("Gy年", Locale.JAPAN)
val todayString = formatter.format(JapaneseDate.now()) // 令和3年
もしJava 1.8以前のバージョンを使うなどでLocalDateやJapaneseDateが使えなく、java.util.Dateの方を使うしかない場合は、以下のような方法で年号と年度の取得が可能です。
val format = SimpleDateFormat("Gy年", Locale("Ja", "JP", "JP"))
val year = format.format(Date()) // 令和3年
java.util.Dateを使う場合は、Localeに第3引数のvariantまで指定する必要があるので、既存の列挙型として定義されたものは使えません。
また、Locale.ENGLISHなどに設定すると、JapaenseDateを使っている場合でも取得した結果はAD2021年12月5日になります。
合字で表記する
年号については、Unicodeで合字を取得して使いたい場合もあるかと思います。その場合は、以下のようにUnicodeのMapなどを定義しておいて取得するのが良いかと思います。拡張関数などを定義するのも良いでしょう。
val eraUnicodeMap = mapOf(
JapaneseEra.MEIJI to "\u337e", // ㍾
JapaneseEra.TAISHO to "\u337d", // ㍽
JapaneseEra.SHOWA to "\u337c", // ㍼
JapaneseEra.HEISEI to "\u337b", // ㍻
JapaneseEra.REIWA to "\u32ff" // ㋿
)
val era = JapaneseDate.now().era
// 元号を合字で取得する
val eraUnicode = eraUnicodeMap[era] // ㋿
上記のサンプルではJapaneseEraが列挙型なのでそのままキーとしていますが、JapaneseEraは数値としての情報も持っているのでそちらを使う方法もあるでしょう。それぞれの値に対する数値は以下の通りです。
| JapaneseEra | 数値 |
|---|---|
| MEIJI | -1 |
| TAISHO | 0 |
| SHOWA | 1 |
| HEISEI | 2 |
| REIWA | 3 |
2021年から2022年の3月の場合は令和3年なので、JapaneseEra.REIWA.valueの値が年度だと勘違いされやすいかなと思います。実際の年度の情報はJapaneseDateの方にあるので注意しましょう。
年度を数字で表示する
JapaneseEraは元号を得るために使う列挙型定数のクラスなので、これ自体はJapaneseDateの日付情報を持っていません。なので参照できる情報は、あくまでも元となるJapaneseDateが属した元号の情報のみです。
なので数値としての年度は、列挙型のChronoFieldをJapaneseDate.get()に渡して取得する必要があります。
val today = JapaneseDate.of(2010, 12, 31) // 平成22年
// 年度をIntとして取得する
val year = today.get(ChronoField.YEAR) // 2010
val yearOfHeisei = today.get(ChronoField.YEAR_OF_ERA) // 22
これはJapaneseDateがLocalDateと違って、直接yearをgetterで取得できないからです。実際オブジェクトの中を覗いてみると、LocalDateは年月日をintとshortのフィールドとして保持していることに対して、JapaneseDateはLocalDateとint型のyearOfEraを持っていて、get(ChronoField.YEAR_OF_ERA)を通じてはじめてyearOfEraを取得できることになります。getterを用意していないのはおそらくLocalDateとyearOfEraという二つの概念があるからなのではないかと思います。もちろん、Kotlinなのでこれは簡単に拡張関数を書くことでgetterを作ることはできますね。
また、日付のオブジェクトとしてLocalDateを使っている場合は場合はChronoField.YEAR_OF_ERAを渡しても西暦の年度が返ってくるので、和暦を使うためにJapaneseDateを使っているかどうかをまず確認しましょう。
年度を2桁の文字で表示する
厳密に言って和暦とは関係のないことですが、年度を取得して使う場合、一貫して先端に「0」のついた2桁の文字列として扱いたい場合もあるかと思います。JapaneseDateを通じて年度を取得した場合はInt型になるので、1〜9の間は1桁の数字となるわけですが、これを01〜09に表示したい場合は以下の方法が使えます。
DecimalFormatを利用する
一つは、JavaのAPIであるDecimalFormatを使うことです。小数点の範囲などをわかりやすく指定できるので個人的には好むやり方です。
val today = JapaneseDate.now() // 令和3年
// 数字を表示するためのフォーマットを指定
val decimalFormat = DecimalFormat("00")
val year = decimalFormat.format(today) // 03
String.formatを利用する
もう一つの方法は、Kotlinのスタンダードライブラリの機能であるString.format()を使うことです。性能注視なら、こちらの方法が良いかなと思います。
val today = JapaneseDate.now() // 令和3年
// 数字を表示するためのフォーマットを指定
val year = "%02d".format(today) // 03
番外:kotlinx-datetime
Kotlinには元々日付や時間を扱うAPIがなかったのですが、2020年からkotlinx-datetimeを提供しています。なのでKotlin/JSやKotlin/Nativeなど、JVM上で動かない場合でも日付を扱える公式のAPIができたわけですが、いくつかの懸念があるのでこれを導入するには検討が必要かと思います。
Pre-releaseの段階
kotlinx-datetimeはまだpre-releaseの段階で、2021年10月にv0.3.1がリリースされています。なので色々とバグがあったり、思い通りにならない可能性があります。また、開発途中のものなので仕方ありませんが、現時点で提供している機能もjava.timeのAPIに比べて少なく、簡単に年号の計算などができるわけではありません。今は必要最低限の機能だけを提供していると思って良いでしょう。
マルチプラットフォーム向け
Kotlinのスタンダードライブラリ、及びkotlinxとして提供されるライブラリはマルチプラットホームを考慮した実装となっているため、プラットホームが違っても同じ使い方ができるというメリットがありますが、かえってデメリットになる場合もあります。実際、kotlinx-datetimeのJVMの実装は内部的にjata.timeのAPIに依存しているため、JVMだけを使う場合はあえて導入する必要がないともいえます。
また、プラットフォームごとに実装が違うということはどこかで予期せぬ例外が発生したり、期待した結果にならないケースも発生しえる、ということにもなるかと思います。
java.timeの懸念
JapaneseEraでは明治以前(慶応など)の元号は使えませんが、おそらくその理由は和暦でグレゴリウス暦が使われたのは明治からだったという歴史的な背景があるのではないかと思います。また、JapaneseDateでも明治6年(西暦1873年1月1日)以前の日付を指定すると以下のように例外が発生します。
Exception in thread "main" java.time.DateTimeException: JapaneseDate before Meiji 6 is not supported
at java.base/java.time.chrono.JapaneseDate.<init>(JapaneseDate.java:333)
at java.base/java.time.chrono.JapaneseDate.of(JapaneseDate.java:257)
なので、単純に帳票を作るなどのケースでなく、歴史的な研究のための日付計算ではここで紹介した方法は使えないケースもあるかと思います。
また、JDKのバージョンなどの問題があるためか、JapaneseEra.REIWAの取得ができなく、エラーとなるケースがあるので注意する必要があります。この場合でもvalueの値の取得は問題ないので、少し可読性は低下しながら分岐などの判定に定数をそのまま使うのは避けたほうが良さそうです。(正確な理由はわかりませんが…)
最後に
いかがでしたか。少し興味本位で調べ始めたもののまとめではありますが、本業の方で実際に必要な処理でもあり、これをどうやって拡張関数として落とせるかということも考えられる良い機会となったかなと思っています。
また、JavaのAPIに関してはJavaバージョン別の改元(新元号)対応まとめという良い記事があったので、興味のある方はご一読ください。
では、また!
