概要
Java 1.8で採用(ただしデフォルトでは無効)され、Java 9でデフォルトで有効になったCLDRロケール・データについて調べた内容です。
CLDRについては参考に挙げたサイトで確認できますが、簡単に説明するとUnicode Consortiumで進められているプロジェクトで、世界中の異なるロケール(日付のフォーマット、通貨名、国名、曜日の並び、数値のフォーマットなど)をデータベース化しています。
このデータはXML形式のLDML(Locale Data Markup Language)で管理、公開されており、Javaもこのデータを完全ではありませんが取り入れています。
この記事を書いた動機は、ヌーラボのアカウント基盤を Java 9 にマイグレーションして起きた問題と解決法という記事を読み、下記に引用した箇所でどのようなコードだと影響を受けるのか気になったためです。
日付や通貨のフォーマットが変わった
Java 8 と 9 とで実行時の動作が変わったことにより自動テストが失敗しました。日付のフォーマットが国際化されています。これはJava 9より国際化拡張機能の初期値が Unicode Consortium が定義した国際化の事実上の標準である CLDR (共通ロケール・データ・リポジトリ) に変更された (JEP 252) ことが原因です。
環境
- Windows 10 Professional
- Oracle JDK 1.8.0_172
- OpenJDK 9.0.4
- OpenJDK 10.0.1
参考
Javaバージョン間での挙動の違いについて
Java 8(Oracle JDK) / Java 9(OpenJDK) / Java 10(OpenJDK)でのバージョン間の挙動の違いを簡単に調べました。
Locale.toLanguageTag
このロケールを表す、整形式の IETF BCP 47 言語タグを返します。
Locale.getDefault().toLanguageTag(); // → (1)
new Locale("ja", "JP").toLanguageTag(); // → (2)
new Locale("ja", "JP", "JP").toLanguageTag(); // → (3)
出力結果
pattern | 1.8.0 | 9.0.4 | 10.0.1 |
---|---|---|---|
1 | ja-JP |
ja-JP |
ja-JP |
2 | ja-JP |
ja-JP |
ja-JP |
3 | ja-JP-u-ca-japanese-x-lvariant-JP |
ja-JP-u-ca-japanese-x-lvariant-JP |
ja-JP-u-ca-japanese-x-lvariant-JP |
特例ロケールのLocale("ja", "JP", "JP")について
Locale (Java SE 10 & JDK 10)
互換性を維持するため、2つの非準拠ロケールが特例として扱われます。 これらはja_JP_JPおよびth_TH_THです。Javaでは、日本で使用されている日本語とともに日本の皇暦を表すためにja_JP_JPを使用してきました。 これは現在ではUnicodeロケール拡張を使用して、Unicodeロケール・キーca (カレンダ)とタイプjapaneseを指定することによって表されます。 Localeコンストラクタが引数"ja", "JP", "JP"で呼び出されると、拡張u-ca-japaneseが自動的に追加されます。
上記に引用したJavaDocに記載されている”u-ca-japanese”は、Unicodeロケール拡張を表す"u"と、ロケールのデフォルトの動作(この例ではカレンダー)をオーバーライドするキーワード(キーとタイプのペア)を表す"ca-japanese"の組み合わせです。
+----- U extension
| +--- Keyword (Key & Type)
| |
- -----------
u-ca-japanese
^^ ^^^^^^^^
| |
| +--- Type (japanese = Japanese Imperial calendar)
+------ Key (ca = Calendar algorithm)
Javaがサポートするキーの種類は、Java 9では次の2つがサポート(JEP 314: Additional Unicode Language-Tag Extensions)され
- ca (Calendar algorithm)
- bcp47/calendar.xml
- nu (Numbering system)
- bcp47/number.xml
Java 10で次の4つが追加されています。
- cu (Currency type)
- bcp47/currency.xml
- fw (First day of week)
- bcp47/calendar.xml
- rg (Region Override)
- tz (Time zone)
- bcp47/timezone.xml
ロケールの通貨タイプをオーバーライドする
Locale.forLanguageTagメソッドがありますが、Locale.Builderが推奨されているので、このBuilderを使用してカスタマイズしたロケールを生成します。
以下は日本ロケールに通貨タイプをUSドルでオーバーライドする例です。
Locale locale = new Locale.Builder()
.setLocale(Locale.getDefault())
.setUnicodeLocaleKeyword("cu", "USD")
.build();
System.out.println(locale.toLanguageTag());
// → ja-JP-u-cu-usd
Currency currency = Currency.getInstance(locale);
System.out.println(currency.getCurrencyCode());
// → USD
System.out.println(currency.getDisplayName());
// → 米ドル
System.out.println(currency.getSymbol());
// → $
double money = 123456789.12345;
NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
formatter.setMinimumFractionDigits(3);
System.out.println(formatter.format(money));
// → $123,456,789.123
カレンダーの週の初めの曜日をオーバーライドする
下記に引用した通りCalendarクラスのJavaDocに記載があるように”u-fw-xxx”というUnicode拡張キーワードを指定すると週の初めの曜日を任意の曜日でオーバーライドできます。
Calendar (Java SE 10 & JDK 10)
Calendar週の最初の日および最初の週の最小日数(1から7)という2つのパラメータを使用して、ロケール固有の週7日が定義されます。 これらの番号は、Calendarが構築されたときのロケール・リソース・データまたはロケール自体から取得されます。 指定されたロケールに"fw"および/または"rg" 「Unicode拡張」が含まれている場合、その週の最初の曜日はそれらの拡張子に従って取得されます。
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getCalendarType());
// → gregory
System.out.println(calendar.getFirstDayOfWeek());
// → 1 (Calendar.SUNDAY)
Locale locale = new Locale.Builder()
.setLocale(Locale.getDefault())
.setUnicodeLocaleKeyword("fw", "mon")
.build();
System.out.println(locale.toLanguageTag());
// → ja-JP-u-fw-mon
Calendar calendar = Calendar.getInstance(locale);
System.out.println(calendar.getCalendarType());
// → gregory
System.out.println(calendar.getFirstDayOfWeek());
// → 2 (Calendar.MONDAY)
DateとDateFormat
Unicodeロケール拡張(u-ca-japaneseの追加)
Locale locale = new Locale("ja", "JP", "JP");
Date now = new Date();
DateFormat.getDateInstance(DateFormat.FULL, locale).format(now); // → (1)
DateFormat.getDateInstance(DateFormat.LONG, locale).format(now); // → (2)
DateFormat.getDateInstance(DateFormat.MEDIUM, locale).format(now); // → (3)
DateFormat.getDateInstance(DateFormat.SHORT, locale).format(now); // → (4)
デフォルトのロケール
Date now = new Date();
DateFormat.getDateInstance(DateFormat.FULL).format(now); // → (5)
DateFormat.getDateInstance(DateFormat.LONG).format(now); // → (6)
DateFormat.getDateInstance(DateFormat.MEDIUM).format(now); // → (7)
DateFormat.getDateInstance(DateFormat.SHORT).format(now); // → (8)
出力結果
pattern | 1.8.0 | 9.0.4 | 10.0.1 | 1.8と9の差異 |
---|---|---|---|---|
1 | 平成30年6月25日 | 平成30年6月25日 | 平成30年6月25日 | |
2 | H30.06.25 | 平成30.06.25 | 平成30.06.25 | 有 |
3 | H30.06.25 | 平成30.06.25 | 平成30.06.25 | 有 |
4 | H30.06.25 | 平成30.06.25 | 平成30.06.25 | 有 |
5 | 2018年6月25日 | 2018年6月25日月曜日 | 2018年6月25日月曜日 | 有 |
6 | 2018/06/25 | 2018年6月25日 | 2018年6月25日 | 有 |
7 | 2018/06/25 | 2018/06/25 | 2018/06/25 | |
8 | 18/06/25 | 2018/06/25 | 2018/06/25 | 有 |
CalendarとDateFormat
Unicodeロケール拡張(u-ca-japaneseの追加)
Locale locale = new Locale("ja", "JP", "JP");
Calendar now = Calendar.getInstance(locale);
DateFormat.getDateInstance(DateFormat.FULL, locale).format(now.getTime()); // → (1)
DateFormat.getDateInstance(DateFormat.LONG, locale).format(now.getTime()); // → (2)
DateFormat.getDateInstance(DateFormat.MEDIUM, locale).format(now.getTime()); // → (3)
DateFormat.getDateInstance(DateFormat.SHORT, locale).format(now.getTime()); // → (4)
デフォルトのロケール
Calendar now = Calendar.getInstance();
DateFormat.getDateInstance(DateFormat.FULL).format(now.getTime()); // → (5)
DateFormat.getDateInstance(DateFormat.LONG).format(now.getTime()); // → (6)
DateFormat.getDateInstance(DateFormat.MEDIUM).format(now.getTime()); // → (7)
DateFormat.getDateInstance(DateFormat.SHORT).format(now.getTime()); // → (8)
出力結果
pattern | 1.8.0 | 9.0.4 | 10.0.1 | 1.8と9の差異 |
---|---|---|---|---|
1 | 平成30年6月25日 | 平成30年6月25日 | 平成30年6月25日 | |
2 | H30.06.25 | 平成30.06.25 | 平成30.06.25 | 有 |
3 | H30.06.25 | 平成30.06.25 | 平成30.06.25 | 有 |
4 | H30.06.25 | 平成30.06.25 | 平成30.06.25 | 有 |
5 | 2018年6月25日 | 2018年6月25日月曜日 | 2018年6月25日月曜日 | 有 |
6 | 2018/06/25 | 2018年6月25日 | 2018年6月25日 | 有 |
7 | 2018/06/25 | 2018/06/25 | 2018/06/25 | |
8 | 18/06/25 | 2018/06/25 | 2018/06/25 | 有 |
LocalDateTimeとDateTimeFormatter
Unicodeロケール拡張(u-ca-japaneseの追加)
Locale locale = new Locale("ja", "JP", "JP");
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale).format(now); // → (1)
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(locale).format(now); // → (2)
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale).format(now); // → (3)
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale).format(now); // → (4)
デフォルトのロケール
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(now); // → (5)
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).format(now); // → (6)
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).format(now); // → (7)
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(now); // → (8)
出力結果
pattern | 1.8.0 | 9.0.4 | 10.0.1 | 1.8と9の差異 |
---|---|---|---|---|
1 | 2018年6月25日 | 2018年6月25日月曜日 | 2018年6月25日月曜日 | 有 |
2 | 2018/06/25 | 2018年6月25日 | 2018年6月25日 | 有 |
3 | 2018/06/25 | 2018/06/25 | 2018/06/25 | |
4 | 18/06/25 | 2018/06/25 | 2018/06/25 | 有 |
5 | 2018年6月25日 | 2018年6月25日月曜日 | 2018年6月25日月曜日 | 有 |
6 | 2018/06/25 | 2018年6月25日 | 2018年6月25日 | 有 |
7 | 2018/06/25 | 2018/06/25 | 2018/06/25 | |
8 | 18/06/25 | 2018/06/25 | 2018/06/25 | 有 |
DateTimeFormatter.localizedBy
localizedByメソッドはJava 10より導入されたメソッドです。ロケールにUnicode拡張が含まれている場合、ロケールがオーバーライドされます。(ロケールインスタンスへの副作用はありません)
Locale locale = new Locale("ja", "JP", "JP");
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).localizedBy(locale).format(now); // → (1)
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).localizedBy(locale).format(now); // → (2)
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).localizedBy(locale).format(now); // → (3)
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).localizedBy(locale).format(now); // → (4)
出力結果
pattern | 1.8.0 | 9.0.4 | 10.0.1 | 1.8と9の差異 |
---|---|---|---|---|
1 | - | - | 平成30年6月27日水曜日 | - |
2 | - | - | 平成30年6月27日 | - |
3 | - | - | 平成30年6月27日 | - |
4 | - | - | H30/6/27 | - |
ZonedDateTimeとDateTimeFormatter
Unicodeロケール拡張(u-ca-japaneseの追加)
Locale locale = new Locale("ja", "JP", "JP");
ZonedDateTime now = ZonedDateTime.now();
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.FULL).withLocale(locale).format(now); // → (1)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.LONG).withLocale(locale).format(now); // → (2)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM).withLocale(locale).format(now); // → (3)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT).withLocale(locale).format(now); // → (4)
デフォルトのロケール
ZonedDateTime now = ZonedDateTime.now();
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.FULL).format(now); // → (5)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.LONG).format(now); // → (6)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM).format(now); // → (7)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT).format(now); // → (8)
出力結果
pattern | 1.8.0 | 9.0.4 | 10.0.1 | 1.8と9の差異 |
---|---|---|---|---|
1 | 2018年6月25日 22時20分24秒 JST | 2018年6月25日月曜日 22時22分28秒 日本標準時 | 2018年6月25日月曜日 22時24分57秒 日本標準時 | 有 |
2 | 2018/06/25 22:20:24 JST | 2018年6月25日 22:22:28 JST | 2018年6月25日 22:24:57 JST | 有 |
3 | 2018/06/25 22:20:24 | 2018/06/25 22:22:28 | 2018/06/25 22:24:57 | |
4 | 18/06/25 22:20 | 2018/06/25 22:22 | 2018/06/25 22:24 | 有 |
5 | 2018年6月25日 22時20分24秒 JST | 2018年6月25日月曜日 22時22分28秒 日本標準時 | 2018年6月25日月曜日 22時24分57秒 日本標準時 | 有 |
6 | 2018/06/25 22:20:24 JST | 2018年6月25日 22:22:28 JST | 2018年6月25日 22:24:57 JST | 有 |
7 | 2018/06/25 22:20:24 | 2018/06/25 22:22:28 | 2018/06/25 22:24:57 | |
8 | 18/06/25 22:20 | 2018/06/25 22:22 | 2018/06/25 22:24 | 有 |
任意のパターンを指定する
任意のパターンをする場合はバージョン間に差異はありませんでした。
Locale locale = new Locale("ja", "JP", "JP");
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter.ofPattern("G yyyy-MM-dd (E) a HH:mm:ss.SSS", locale).format(now); // → (1)
DateTimeFormatter.ofPattern("G yyyy-MM-dd (E) a HH:mm:ss.SSS").format(now); // → (2)
DateTimeFormatter.ofPattern("G yy-MM-dd (E) a HH:mm:ss.SSS").localizedBy(locale).format(now); // → (3)
Locale locale = new Locale("ja", "JP", "JP");
ZonedDateTime now = ZonedDateTime.now();
DateTimeFormatter.ofPattern("G yyyy-MM-dd (E) a HH:mm:ss.SSS zzz", locale).format(now); // → (4)
DateTimeFormatter.ofPattern("G yyyy-MM-dd (E) a HH:mm:ss.SSS zzz").format(now); // → (5)
DateTimeFormatter.ofPattern("G yy-MM-dd (E) a HH:mm:ss.SSS zzz").localizedBy(locale).format(now); // → (6)
出力結果
pattern | 1.8.0 | 9.0.4 | 10.0.1 | 1.8と9の差異 |
---|---|---|---|---|
1 | 西暦 2018-06-26 (火) 午前 00:01:45.267 | 西暦 2018-06-26 (火) 午前 00:02:50.751 | 西暦 2018-06-26 (火) 午前 00:03:56.584 | |
2 | 西暦 2018-06-26 (火) 午前 00:01:45.267 | 西暦 2018-06-26 (火) 午前 00:02:50.751 | 西暦 2018-06-26 (火) 午前 00:03:56.584 | |
3 | - | - | 平成 30-06-26 (火) 午前 00:26:03.888 | - |
4 | 西暦 2018-06-26 (火) 午前 00:01:45.269 JST | 西暦 2018-06-26 (火) 午前 00:02:50.767 JST | 西暦 2018-06-26 (火) 午前 00:03:56.601 JST | |
5 | 西暦 2018-06-26 (火) 午前 00:01:45.269 JST | 西暦 2018-06-26 (火) 午前 00:02:50.767 JST | 西暦 2018-06-26 (火) 午前 00:03:56.601 JST | |
6 | - | - | 平成 30-06-26 (火) 午前 00:26:03.898 JST | - |
補足
Unicode Technical Standard #35
UNICODE LOCALE DATA MARKUP LANGUAGE (LDML)
国際化の拡張機能の変遷
CLDR関連については記載はありません。
CLDRとは関係ありませんが和歴のサポートがJava SE 6で行われています。
和暦のサポート
2005年(グレゴリオ暦)を平成 17年とするような和暦の計数をサポートするため、新しいCalendar実装が追加されています。この和暦のインスタンスは、Locale("ja", "JP", "JP")を指定すれば、Calendar.getInstanceファクトリで作成することができます。java.text.SimpleDateFormatクラスは、グレゴリオ暦以外のカレンダ固有の年号および日付形式をサポートしています。
Locale locale = new Locale("ja", "JP", "JP");
Calendar calendar = Calendar.getInstance(locale);
System.out.println(calendar.getClass().getCanonicalName());
// → java.util.JapaneseImperialCalendar
System.out.println(new SimpleDateFormat("Gyy年MM月dd日 (E)", locale).format(calendar.getTime()));
// → 平成30年06月25日 (月)
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getClass().getCanonicalName());
// → java.util.GregorianCalendar
System.out.println(new SimpleDateFormat("Gyyyy年MM月dd日 (E)").format(calendar.getTime()));
// → 西暦2018年06月25日 (月)
ロケールクラスが BCP47 と UTR35 をサポート
Locale クラスは、BCP 47 (IETF BCP 47「Tags for Identifying Languages」) と相互に交換できる識別子を実装するように更新され、ロケールデータ交換用の LDML (UTS#35「Unicode Locale Data Markup Language」) の BCP 47 互換拡張をサポートしています。
Unicode CLDRデータの採用とjava.locale.providersシステム・プロパティ
Unicode Consortiumは、「もっとも大規模かつ広範な標準ロケール・データ・リポジトリによって世界の言語をサポートする」ために、共通ロケール・データ・リポジトリ(CLDR)プロジェクトをリリースしました。CLDRはロケール・データの事実上の標準となりつつあります。
CLDRのXMLベースのロケール・データはJDK 8リリースに組み込まれていますが、デフォルトでは無効になっています。
デフォルト
デフォルトの動作は次の設定と同等です。
java.locale.providers=JRE,SPI
デフォルトで有効になっているCLDRロケール・データ
最初にJDK 8に追加されたUnicode Common Locale Data Repository (CLDR)のXMLベースのロケール・データが、JDK 9でのデフォルトのロケール・データです。以前のリリースでは、デフォルトはJREでした。
デフォルト
このプロパティを設定しない場合、デフォルトの動作は次の設定と同等です。
java.locale.providers=CLDR,COMPAT,SPI
- COMPAT(旧称JRE)
追加のUnicode言語タグ拡張
Java SE 9では、-ca (カレンダ)および-nu (数値)拡張のみがサポートされます。
Java SE 10では、関連するJDKクラスで次の追加の拡張のサポートが追加されています。
- -cu (通貨タイプ)
- -fw (週の最初の日)
- -rg (リージョン・オーバーライド)
- -tz (タイム・ゾーン)
Java 9で対応された問題
java.time: DateTimeFormatter containing "DD" fails on 3-digit day-of-year value
対象日付の年の通算日数が100日以上の場合にパターン文字列に"DD"を指定すると例外がおきるバグは、Java 9で解決しています。
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter.ofPattern("D").format(now);
// → 177
DateTimeFormatter.ofPattern("DD").format(now); // ← Java 1.8では例外
// → 177
DateTimeFormatter.ofPattern("DDD").format(now);
// → 177
DateTimeFormatter won't parse dates with custom format "yyyyMMddHHmmssSSS"
Java 1.8ではパターン文字列に"yyyyMMddHHmmssSSS"を指定すると例外がおきるバグは、Java 9で解決しています。
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(now);
// → 20180625222024147
java.time.format.FormatStyle.LONG or FULL causes unchecked exception
LocalDateTimeをDateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)でフォーマットすると例外が発生しますが、これは仕様なので問題ではありません。
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).format(now);
// → Exception in thread "main" java.time.DateTimeException: Unable to extract ZoneId from temporal 2018-06-26T01:00:54.557262700
ZonedDateTimeの場合は問題なくフォーマットできます。
ZonedDateTime now = ZonedDateTime.now();
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.FULL).format(now);
// → 2018年6月25日月曜日 22時24分57秒 日本標準時
その他パターン文字列についての対応(追加というよりCLDRの仕様に準拠するような対応)
DateTimeFormatter pattern letters 'A','n','N'
Add date-time patterns 'v' and 'vvvv'
DateTimeFormatter pattern letter 'g'
Incorrect documentation for DateTimeFormatter letter 'k'
Java 11で対応される(た)課題
気になったものをピックアップしているので、ここに挙げたものがすべてではありません。
Japanese new era implementation
Release Note: Japanese New Era Implementation
2019年5月1日行われる改元の対応です。現時点では暫定として元号に"NewEra"が表示されるということです。
Release Note: Update locale data to Unicode CLDR v33
CLDR data to Version 33のアップグレードされます。