Java
C++
ICU
和暦
令和

【最終版】新元号に対応するために ICU を用いて和暦の動作確認をする


更新内容


  • 2019/5/7 Node.js に関して追記

  • 2019/5/7 和暦ロケールを言語タグで指定、ソースコードの一部APIの使い方に追記(動作に変更はなし)

  • 2019/4/26 Mac Homebrew にて ICU 64.2 記述を更新

  • 2019/4/18 ICU 64.2 について追記。仮元号の指定が不要になりました

  • 2019/4/1 新元号が令和に決まりましたので,関連するところを更新

  • 2019/4/1 ICU 64.1 について追記


はじめに

新元号が「令和(れいわ)」に決定し、改元の施行日まであと2週間を切りました(2019年4月18日時点)。

本記事では、クロスプラットフォームの国際化ライブラリである ICU (International Components for Unicode) を組み込んでいるアプリケーションを対象に、新元号に対応するために必要なアクションを解説します。

本記事の内容は、こちらに書かれている内容をもとにしています。


ICU の状況

ICU は 2019年4月18日(日本時間)にリリースされたバージョン 64.2 から、新元号「令和」に対応するためのリソースが組み込まれました。

それ以前の 2018年10月15日にリリースされたバージョン 63 (63.1※) からすでに、新元号に対応するためのリソースが「仮元号」という形で組み込まれましたが、仮元号の指定は不要になりました。また、2019年3月27日リリースのバージョン 64 (64.1) からは、1年に加えて「元年」も入出力ともに表記として扱えるようになりました。現時点でのICUの新元号対応は、具体的には、以下の2点になります


注※ 63.0 といったゼロスタートではなく、最初のリリースが 63.1 です。



  1. 2019年5月1日からの元号として「令和」が追加された (64.2より)

  2. 「1年」「元年」どちらの表記も扱えるようになった (64.1より)


  3. 2019年5月1日からの元号として「QQ」(全角のアルファベット「キュー」2つ)という仮元号を示すプレースホルダーが追加された (63.1から64.1まで)


  4. プレースホルダーを利用するには、JVM引数や環境変数で ICU_ENABLE_TENTATIVE_ERA=true を指定する (63.1から64.1まで)

v63.1 より古いバージョンの ICU とリンクしているアプリケーションは、ライブラリを v64.2 に置き換えるだけで動作確認ができるようになります。しかし、「令和」を表示したり、入力で受け付けるためには、すでにアプリケーションが「平成」や「昭和」といった和暦形式を入力および出力でサポートしている場合に限ります。したがって、 現時点で西暦でしか日付を扱っていない場合は、ライブラリを置き換えても新元号に即対応できるわけではありません

動作をまとめると以下のようになります。

和暦での入力
63.1 以下または ICU_ENABLE_TENTATIVE_ERA=false
63.1 以上かつ ICU_ENABLE_TENTATIVE_ERA=true
64.2 以上(環境変数不要)

平成31年5月1日
平成31年5月1日
QQ1年5月1日 (63.1) / QQ元年5月1日 (64.1)
令和元年5月1日

QQ1年5月1日
パースエラー
QQ1年5月1日 (63.1) / QQ元年5月1日 (64.1)
パースエラー

QQ元年5月1日
パースエラー
パースエラー (63.1) / QQ元年5月1日 (64.1)
パースエラー

令和元年5月1日
パースエラー
パースエラー (63.1) / パースエラー (64.1)
令和元年5月1日

この結果は java.util.Calendar.setLenient(true) および com.ibm.icu.util.Calendar.setCalendarLenient(true) を前提としています(どちらもデフォルト値なのでとくに指定する必要はありません)。

「平成31年5月1日」は現実には存在しない日付となりますが、これを厳密にパースエラーにするかどうかはアプリケーションの仕様に依存します。

setLenient() および setCalendarLenient() の挙動については内容が多岐にわたるため、本記事では扱いません。

また、新元号「令和」対応についてのもうひとつのトピックとして、「㍼」 「㍻」 などのように元号を1文字で表す合字の取扱いもありますが、こちらも本記事では扱いません。

ICU v64.1 から「元年」表記に対応しました。ただし、v64.1 リリースは、新元号「令和」の公表前だったため、まだ「令和」には対応していませんので注意してください。v64.2 以上とリンクしてください。

タイムラインをまとめると次のようなります。

時期
 ICU Version
新元号対応

2018年10月
63.1
「QQ」をICU_ENABLE_TENTATIVE_ERA=true で有効に

2019年3月
64.1
「QQ」をICU_ENABLE_TENTATIVE_ERA=true で有効に, 「1年=元年」対応

2019年4月
64.2
「令和」を追加(ICU_ENABLE_TENTATIVE_ERA は不要)


過去のICUバージョンへの令和対応

こちらのプルリクエスト(ICU-20536)によると、v64.2 以外にも、過去のバージョンについての令和の元号が追加されているようです。以下のバージョン番号が確認できました。

4.8.2 50.2 51.3 52.2 53.2 54.2 55.2 56.2 57.2 58.3 59.2 60.3 61.2 62.2 63.2

これらのバージョンは、v64.2 と比べて、以下の新元号対応が省略されています


  • 「元年」対応がない

  • Unicode v12.1 で追加される令和の合字への対応がない(古いICUはベースにしている Unicode も古いため)

  • 合字のCollation (㍾ < ㍽ < ㍼ < ㍻ < (令和の合字) という並び順) の対応がない


いますぐできること

それでは、ICU version 64.2 (と過去の令和対応ICUバージョン) をアプリケーションにリンクして動作確認をする方法を説明します。


ICU4C を用いた C++ ソースコードの例

ICU (ICU4C) には C と C++ それぞれの API が用意されていますが、ここでは C++ の例で説明します。


sample.cpp

#include <stdio.h>

#include <iostream>
#include "unicode/datefmt.h"
#include "unicode/dtfmtsym.h"
#include "unicode/gregocal.h"
#include "unicode/timezone.h"
#include "unicode/unistr.h"
#include "unicode/ustring.h"
#include "unicode/dtptngen.h"
#include "unicode/dtitvfmt.h"

using namespace icu;

// Windows で文字化けしないための簡易的なユーティリティー
void myprintf(std::string format, UnicodeString ustr) {
char abuf[0x100];
ustr.extract(0, sizeof(abuf), abuf);
printf(format.c_str(), abuf);
}

int main( int argc, char **argv )
{
UErrorCode status = U_ZERO_ERROR;
// 西暦ベースのロケール
Locale loc_jp1 = Locale::getJapanese();
// 和暦ベースのロケール LanguageTag での指定
Locale loc_jp2 = Locale("ja-u-ca-japanese");

// (A) パターン生成クラス
DateTimePatternGenerator *g_jp1 = DateTimePatternGenerator::createInstance(loc_jp1, status);
status = U_ZERO_ERROR;
DateTimePatternGenerator *g_jp2 = DateTimePatternGenerator::createInstance(loc_jp2, status);
if (U_FAILURE(status)) {
return 1;
}

// (B) ロケールごとに適切な日付フォーマットのパターンを取得
status = U_ZERO_ERROR;
UnicodeString up_jp1 = g_jp1->getBestPattern(UnicodeString("yyyyMMMd"), status);
myprintf("pattern jp1: %s\n", up_jp1);
status = U_ZERO_ERROR;
UnicodeString up_jp2 = g_jp2->getBestPattern(UnicodeString("yyyyMMMd"), status);
myprintf("pattern jp2: %s\n", up_jp2);

// (C) フォーマットのパターンを用いて生成する
status = U_ZERO_ERROR;
SimpleDateFormat *df_jp1 = new SimpleDateFormat(up_jp1, loc_jp1, status);
status = U_ZERO_ERROR;
SimpleDateFormat *df_jp2 = new SimpleDateFormat(up_jp2, loc_jp2, status);

// (A),(B),(C)をまとめてDateFormatをつくる書き方はこちら
//DateFormat *df_jp1 = DateFormat::createInstanceForSkeleton("yMMMd", loc_jp1, status);
//DateFormat *df_jp2 = DateFormat::createInstanceForSkeleton("yMMMd", loc_jp2, status);

UnicodeString uin = UnicodeString(argv[1]);
myprintf("input:%s\n", uin);
status = U_ZERO_ERROR;
// まずは西暦としてパース
UDate inDate = df_jp1->parse(uin, status);
if (U_FAILURE(status)) {
std::cout << "Parse error (" << u_errorName(status) << ") try another." << std::endl;
status = U_ZERO_ERROR;
// 次に和暦としてパース
inDate = df_jp2->parse(uin, status);
if (U_FAILURE(status)) {
std::cout << "Parse error (" << u_errorName(status) << ") again." << std::endl;
return 1;
}
}
// 入力された日付を西暦・和暦それぞれで出力
UnicodeString ud_jp1;
df_jp1->format(inDate, ud_jp1);
myprintf("output jp1:%s\n", ud_jp1);
UnicodeString ud_jp2;
df_jp2->format(inDate, ud_jp2);
myprintf("output jp2:%s\n", ud_jp2);

return 0;
}



macOS での実行

macOS には ICU が組み込まれていますが、OS の ICU を直接更新することなくテストするために、 Homebrew を活用して、ICU 64.2 をインストールします。(2019年4月19日公開)

$ brew install icu4c

上記サンプルコードは次のようにしてコンパイルできます。環境変数の部分はコンパイラへの引数として指定することもできます。

brew によるインストール先が例と異なる場合は適宜置き換えてください。

$ export C_INCLUDE_PATH=/usr/local/opt/icu4c/include

$ export CPLUS_INCLUDE_PATH=/usr/local/opt/icu4c/include
$ export LIBRARY_PATH=/usr/local/opt/icu4c/lib
$ export LD_LIBRARY_PATH=/usr/local/opt/icu4c/lib
$ clang++ -licuio -licui18n -licutu -licuuc -licudata -std=c++1z sample.cpp

以下のようにして実行できます。第1引数には入力の日付を西暦や和暦で指定します。

実行例は v64.2 をもとにしています。v63.2 以下で実行した結果は、「元年」を「1年」に置き換えてください。


(A) 平成のままで入力して、西暦と令和で出力

setLenient() の設定によりますが、令和の期間の日付を平成で入力しても処理できます。

df_jp1 は西暦での入力を期待しているので Parse error が1度出ます。

$ ./a.out 平成31年5月1日

pattern jp1: yyyy年M月d日
pattern jp2: Gy年M月d日
input:平成31年5月1日
Parse error (U_ILLEGAL_ARGUMENT_ERROR) try another.
output jp1:2019年5月1日
output jp2:令和元年5月1日


(B) 平成のままで入力して、西暦と平成で出力

令和対応により、上記のサンプルコードを用いて「令和元年5月1日」以降の日付を強制的に平成で出力することはできなくなりました。


(C) 西暦で入力して、西暦と令和で出力

Parse error は出ません。

$ ./a.out 2019年5月1日

pattern jp1: yyyy年M月d日
pattern jp2: Gy年M月d日
input:2019年5月1日
output jp1:2019年5月1日
output jp2:令和元年5月1日


(D) 西暦で入力して、西暦と平成で出力

令和対応により、上記のサンプルコードを用いて「令和元年5月1日」以降の日付を強制的に平成で出力することはできなくなりました。


(E) 令和で入力して、西暦と令和で出力

df_jp1 は西暦での入力を期待しているので Parse error が1度出ます。

$ ./a.out 令和元年5月1日

pattern jp1: yyyy年M月d日
pattern jp2: Gy年M月d日
input:令和元年5月1日
Parse error (U_ILLEGAL_ARGUMENT_ERROR) try another.
output jp1:2019年5月1日
output jp2:令和元年5月1日

「1年」で入力しても、「元年」で出力されます。

$ ./a.out 令和1年5月1日

pattern jp1: yyyy年M月d日
pattern jp2: Gy年M月d日
input:令和1年5月1日
Parse error (U_ILLEGAL_ARGUMENT_ERROR) try another.
output jp1:2019年5月1日
output jp2:令和元年5月1日


(F) 令和で入力して、西暦と平成で出力

setLenient() の設定によりますが、平成の期間の日付を令和で入力しても処理できます。この場合は、平成で出力されます。

$ ./a.out 令和元年4月1日

pattern jp1: yyyy年M月d日
pattern jp2: Gy年M月d日
input:令和元年4月1日
Parse error (U_ILLEGAL_ARGUMENT_ERROR) try another.
output jp1:2019年5月1日
output jp2:平成31年4月1日

$ ./a.out 令和1年4月1日

pattern jp1: yyyy年M月d日
pattern jp2: Gy年M月d日
input:令和1年4月1日
Parse error (U_ILLEGAL_ARGUMENT_ERROR) try another.
output jp1:2019年5月1日
output jp2:平成31年4月1日


Linux での実行

Ubuntu 18.04 (64bit) で確認しました。

ICU のサイトから、 icu4c-64_2-Ubuntu-18.04-x64.tgz をダウンロードして、展開します。展開したディレクトリをここでは $ICUPATH とします。

あとは macOS の場合とほぼ同じようにできます。

$ export C_INCLUDE_PATH=$ICUPATH/include

$ export CPLUS_INCLUDE_PATH=$ICUPATH/include
$ export LIBRARY_PATH=$ICUPATH/lib
$ export LD_LIBRARY_PATH=$ICUPATH/lib
$ clang++ -licuio -licui18n -licutu -licuuc -licudata -std=c++1z sample.cpp

または

$ g++ sample.cpp -licuio -licui18n -licutu -licuuc -licudata

実行方法は macOS の場合と同じです。


Windows (VC++) での実行

Visual Studio に Visual C++ > Windows デスクトップ > Windows コンソールアプリケーション のプロジェクトを作成します。ここでは JapaneseNewEraICU というソリューション名で説明します。

ICU のサイトから、 icu4c-64_2-Win64-MSVC2017.zip をダウンロードして、展開します。展開したフォルダをここでは %ICUPATH% とします。

ソリューションのプロパティで以下の値を設定します。

項目
 設定する値

構成プロパティ > VC++ ディレクトリ

インクルード ディレクトリ

%ICUPATH%\include;$(IncludePath)

ライブラリ ディレクトリ

%ICUPATH%\lib64;$(LibraryPath)

構成プロパティ > リンカー > 入力

追加の依存ファイル

icuio.lib;icuin.lib;icutu.lib;icuuc.lib;%(AdditionalDependencies)

この設定でコンパイルすると、「コンピューターに icuin64.dll がないため、プログラムを開始できません。この問題を解決するには、プログラムを再インストールしてみてください。」 というエラーが出ます。これは、ダウンロードした ICU の DLL が環境変数 %PATH% で見つからないことによるエラーです。

実行するときに、別途コマンドプロンプトを開いて、

C:\> set PATH=%ICUPATH%\bin64;%PATH%

を実行したあとに

C:\> .\JapaneseNewEraICU.exe 平成32年2月16日

などのように実行できます。


ICU4J を用いた Java ソースコードの例

ICU4J を Maven リポジトリの依存関係に追加します。


pom.xml

<!-- https://mvnrepository.com/artifact/com.ibm.icu/icu4j -->

<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>64.2</version>
</dependency>

C++ と同様のコード例はこちらになります。


test/Sample.java

package test;

import java.text.ParseException;
import java.util.Date;
import java.util.Locale;

import com.ibm.icu.text.DateTimePatternGenerator;
import com.ibm.icu.text.SimpleDateFormat;

public class Sample {

public static void main(String[] args) {
Locale loc_jp1 = Locale.JAPANESE;
Locale loc_jp2 = Locale.forLanguageTag("ja-u-ca-japanese");

//(A)
DateTimePatternGenerator g_jp1 = DateTimePatternGenerator.getInstance(loc_jp1);
DateTimePatternGenerator g_jp2 = DateTimePatternGenerator.getInstance(loc_jp2);
//(B)
String p_jp1 = g_jp1.getBestPattern("yyyyMMMd");
System.out.format("pattern jp1: %s\n",p_jp1);
String p_jp2 = g_jp2.getBestPattern("yyyyMMMd");
System.out.format("pattern jp2: %s\n",p_jp2);
//(C)
SimpleDateFormat df_jp1 = new SimpleDateFormat(p_jp1, loc_jp1);
SimpleDateFormat df_jp2 = new SimpleDateFormat(p_jp2, loc_jp2);

//(A),(B),(C)をまとめてDateFormatをつくる書き方はこちら
//import com.ibm.icu.text.DateFormat; と併せて
//DateFormat df_jp1 = DateFormat.getInstanceForSkeleton("yMMMd", loc_jp1);
//DateFormat df_jp2 = DateFormat.getInstanceForSkeleton("yMMMd", loc_jp2);

String input = args[0];
System.out.format("input: %s\n", input);
Date idate = null;
try {
idate = df_jp1.parse(input);
} catch (ParseException e) {
e.printStackTrace();
}
if (idate==null) {
try {
idate = df_jp2.parse(input);
} catch (ParseException e) {
e.printStackTrace();
}
}

String out_jp1 = df_jp1.format(idate);
System.out.format("output jp1: %s\n", out_jp1);
String out_jp2 = df_jp2.format(idate);
System.out.format("output jp2: %s\n", out_jp2);
}
}


IDE上での実行のほか、コマンドから直接実行する場合は以下のようになります。

$ java -cp ~/.m2/repository/com/ibm/icu/icu4j/64.2/icu4j-64.2.jar:target/classes test.Sample 令和1年5月10日


(おまけ) Node.js での対応

2019年4月29日にリリースされた Node.js v12.1 より、 ICU v64.2 とリンクするようになりましたので、特別な指定なく元号として令和を使用できるようになりました。

ICU for NodeJS といったライブラリは存在しないので、通常の Date オブジェクトで令和の期間の日付を表示する例は以下のようになります。


test/sample.js

var date = new Date(Date.UTC(2019, 11, 20, 3, 0, 0));

var options = {
era: 'short',
year: 'numeric',
month: 'narrow',
day: 'numeric',
weekday: 'narrow'
};
console.log(date.toLocaleString('ja-u-ca-japanese',options));

$ node test/sample.js

令和元年12月20日(日)


新元号が決定したらできること

上の「ICUの状況」で説明したように、「令和」が埋め込まれた ICU のリリース v64.2 がリリースされました。

これまで、ICU 63.1/64.1 またはそれ以降を用いて仮元号の「QQ」での表示や入力を十分にテストしておけば、正式リリース後に ICU v64.2 に置き換えることで新元号への対応が完了します。

注意点としましては、和暦に対応したロケール設定が挙げられます。サンプルのソースコードで jp2 として使用したロケール ja_JP@calendar=japanese が和暦に対応したロケールになります。このほか ja_JP_TRADITIONAL または ja_JP_JP といった形式でも動作します。

これらの値が環境変数 LANG 等でユーザーまたはシステムによって設定されることはまれですので、アプリケーションが独自に和暦で動作するかどうかの判定ロジックを用意することが期待されます。


さいごに

本記事は 2019年4月18日現在の情報をもとに記述されていますが、状況が変わりしだい随時内容を更新していく予定です。

2019年5月の改元までの準備期間に、本記事が対応作業への手助けになれば幸いです。