Featured image of post Kotlinで和暦を使う

Kotlinで和暦を使う

帳票などで、たまに和暦を処理する必要な時がありますね。例えば元号を表記するとか、和暦の年度を表記するなどの場合があるかと思います。Kotlin(JVM)の場合、西暦だとJavaのAPIのDateLocalDateなどの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()という関数にTextStyleLocaleを指定して文字列を取得することができます。前者は文字の出力型を指定する列挙型定数で、後者は言語の指定と思ってください。

TextStyleの場合、以下のような値があります。他の言語だと指定したものによって出力がかなり変わってくるかもしれませんが、日本語の場合はFULLNARROWだけで十分ではないかと思います。

定数出力例
FULL昭和
FULL_STANDALONE昭和
NARROWS
NARROW_STANDALONES
SHORT昭和
SHORT_STANDALONE昭和

Localeの場合、Locale.JAPANLocale.JAPANESEのどちらを指定しても結果は同じです。ただ、実装としては以下のようになるのでなるべくLocale.JAPANを使った方が良さそうです。

Locale作られるBaseLocaleの設定
JAPANlanguage = ja, region = JP
JAPANESElanguage = 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以前のバージョンを使うなどでLocalDateJapaneseDateが使えなく、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
TAISHO0
SHOWA1
HEISEI2
REIWA3

2021年から2022年の3月の場合は令和3年なので、JapaneseEra.REIWA.valueの値が年度だと勘違いされやすいかなと思います。実際の年度の情報はJapaneseDateの方にあるので注意しましょう。

年度を数字で表示する

JapaneseEraは元号を得るために使う列挙型定数のクラスなので、これ自体はJapaneseDateの日付情報を持っていません。なので参照できる情報は、あくまでも元となるJapaneseDateが属した元号の情報のみです。

なので数値としての年度は、列挙型のChronoFieldJapaneseDate.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

これはJapaneseDateLocalDateと違って、直接yearをgetterで取得できないからです。実際オブジェクトの中を覗いてみると、LocalDateは年月日をintとshortのフィールドとして保持していることに対して、JapaneseDateLocalDateとint型のyearOfEraを持っていて、get(ChronoField.YEAR_OF_ERA)を通じてはじめてyearOfEraを取得できることになります。getterを用意していないのはおそらくLocalDateyearOfEraという二つの概念があるからなのではないかと思います。もちろん、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バージョン別の改元(新元号)対応まとめという良い記事があったので、興味のある方はご一読ください。

では、また!

Built with Hugo
Theme Stack designed by Jimmy