2018年4月26日木曜日

[JavaSE8 Goldへの道] その14 Date/Time APIその2 日時の書式設定とタイムゾーンの扱い

JavaSE8 Goldへの道(Upgrade to Java SE 8 Programmer 1Z0-810 試験対策)12回目です。

一連の記事は「JavaSE8Gold」ラベルを付けていきます。

今回はDate/Time APIでの書式設定と、タイムゾーンを表すクラスの扱い方を解説します。


書式設定

Date/Time APIでは従来のjava.text.DateFormat(SimpleDateFormat)に代わり、新たにjava.time.format.DateTimeFormatterが用意されました。
書式設定はこのクラスを使います。

parse,formatメソッドは各日時クラスとDateTimeFormatterの両方に実装されていますが、後者のとくにparseメソッドの方は戻り値の型がTemporalAccessorになっているため、日時クラスの方を使うほうが良いでしょう。
引数としてDateTimeFormatterを渡します。

いくつかのフォーマットは事前定義されています。
定数
BASIC_ISO_DATE'20111203'
ISO_LOCAL_DATE'2011-12-03'
ISO_OFFSET_DATE'2011-12-03+01:00'
ISO_ZONED_DATE_TIME'2011-12-03T10:15:30+01:00[Europe/Paris]'
全ての一覧はAPIドキュメントに載っています。
SimpleDateFormatのように独自に指定したい場合はofPatternメソッドを使って行います。

format

フォーマットの例(こちらの参考サイトのサンプルコードより。以降同様。)
// Current date and time
LocalDateTime dateTime = LocalDateTime.now();

// Format as basic ISO date format
String asBasicIsoDate = dateTime.format(DateTimeFormatter.BASIC_ISO_DATE);
System.out.println("BASIC ISO DATE : " + asBasicIsoDate);

// Format as ISO week date
String asIsoWeekDate = dateTime.format(DateTimeFormatter.ISO_WEEK_DATE);
System.out.println("ISO WEEK DATE : " + asIsoWeekDate);

// Format ISO date time
String asIsoDateTime = dateTime.format(DateTimeFormatter.ISO_DATE_TIME);
System.out.println("ISO DATE TIME : " + asIsoDateTime);

// Use a custom pattern: day / month / year
String asCustomPattern = dateTime.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
System.out.println("Custom pattern : " + asCustomPattern);

// Use short Belarusian date/time formatting
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withLocale(new Locale("be"));
String belarusDateTime = dateTime.format(formatter);
System.out.println("Belarusian locale : " + belarusDateTime);

// Use short US date/time formatting
DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withLocale(new Locale("en-US"));
String usDateTime = dateTime.format(formatter1);
System.out.println("US locale : " + usDateTime);

// FormatStyle.FULL、ロケール日本で出力(ZonedDateTimeを使用)
DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.JAPAN);
String jpDateTime = ZonedDateTime.now().format(formatter2);
System.out.println("JP locale : " + jpDateTime);

実行結果
BASIC ISO DATE : 20180424
ISO WEEK DATE : 2018-W17-2
ISO DATE TIME : 2018-04-24T11:55:29.746
Custom pattern : 24/04/2018
Belarusian locale : 24.4.18 11.55
US locale : 4/24/18 11:55 AM
JP locale : 2018年4月24日 11時55分29秒 JST
最後のFormatStyle.FULLを指定した場合ではLocalDateTimeだとjava.time.DateTimeExceptionになってしまいました。
タイムゾーン情報がないとだめなようです。

まあおそらく通常の用途ではDateTimeFormatter.BASIC_ISO_DATEofPatternメソッドによるフォーマット指定がメインになるでしょう。


parse

parseの方にはDateTimeFormatterを引数に取らないものもあり、DateTimeFormatter.ISO_LOCAL_DATE_TIMEで解析されます。

パースの例
// Parsing date strings
LocalDate fromIsoDate = LocalDate.parse("2015-07-20");
System.out.println("From ISO date : " + fromIsoDate);

LocalDate fromIsoWeekDate = LocalDate.parse("2015-W01-2", DateTimeFormatter.ISO_WEEK_DATE);
System.out.println("From ISO week date : " + fromIsoWeekDate);

LocalDate fromCustomPattern = LocalDate.parse("25.07.2015", DateTimeFormatter.ofPattern("dd.MM.yyyy"));
System.out.println("From custom pattern : " + fromCustomPattern);
実行結果
From ISO date : 2015-07-20
From ISO week date : 2014-12-30
From custom pattern : 2015-07-25
SimpleDateFormatでは「yyyy/MM/dd」に対して「2018/4/5」を渡しても問題なく解析されましたが、Date/Time APIでは桁も厳密になっていて合わないと例外がスローされます。


ResolverStyleについて

多分試験範囲外なのですが、使うときに重要だと思うので触れておきます。

従来のDateFormatにもsetLenientメソッドで「厳密な解析」を指定することができました。
例えば「4/31」など存在しない日付を指定した時、厳密でない解析では「5/1」として解釈されます。

Date/Time APIでもResolverStyleとして定義されています。これには厳密(STRICT)、スマート(SMART)、および非厳密(LENIENT)の3つがあります。デフォルトはスマートです。

スマートが新しいですが、APIドキュメントにはあまり詳しく書いていません。
以下のひしだまさんのページによれば、絶対にありえない値(32日とか13月とか)は例外、範囲内だけど存在しない場合は切り詰め(4/31→4/30)という感じのようです。
で、厳密な場合に書式「yyyy」の動作が変わります。これは暦(ERA)に対する年なので、暦が指定されない場合例外になってしまいます。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
        .withResolverStyle(ResolverStyle.STRICT);

LocalDate ldate = LocalDate.parse("2018/04/24", formatter);

Exception in thread "main" java.time.format.DateTimeParseException: Text '2018/04/24' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=4, YearOfEra=2018, DayOfMonth=24},ISO of type java.time.format.Parsed
 at java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1920)
 at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1855)
 at java.time.LocalDate.parse(LocalDate.java:400)
 at test.Test3.main(Test3.java:16)
Caused by: java.time.DateTimeException: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=4, YearOfEra=2018, DayOfMonth=24},ISO of type java.time.format.Parsed
 at java.time.LocalDate.from(LocalDate.java:368)
 at java.time.format.Parsed.query(Parsed.java:226)
 at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
 ... 2 more
これを回避するには、
  • 暦として書式に「G」を追加し、「西暦」を指定する
  • 書式に「uuuu」を指定する
があります。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd G")
        .withResolverStyle(ResolverStyle.STRICT);

LocalDate ldate = LocalDate.parse("2018/04/24 西暦", formatter);
日本語がアレな場合はロケールを英語(Locale.ENGLISH)に変えて「AD」でもいけます。

もう一つの書式「uuuu」は暦無しの年を表し、これなら暦なしでも大丈夫です。こちらの方が面倒がないでしょう。


タイムゾーンの扱い

タイムゾーンはjava.time.ZoneIdクラスで扱います。
従来はjava.util.TimeZoneでした。

前回出てきたZoneOffsetZoneIdのサブクラスです。イメージ的には逆のような気もしますが・・・

生成にはZoneId#ofメソッドを使いますが、記述の仕方にはいくつかあります。
  • 「Z」のみでUTCを表す
  • 「+(-)n」または「+(-)n:nn」(n:数値)
  • 「GMT/UTC/UT+(-)n」または「GMT/UTC/UT+(-)n:nn」
  • 地域ベースID(Asia/Tokyoなど)
  • 短い略称(JSTなど)
例を示します。
//Zのみ
ZoneId utc = ZoneId.of("Z");
System.out.println(utc + ",class=" + utc.getClass().getName());

//オフセット数値のみ
ZoneId plus1 = ZoneId.of("+01:00");
System.out.println(plus1 + ",class=" + plus1.getClass().getName());
ZoneId minus3 = ZoneId.of("-3");
System.out.println(minus3 + ",class=" + minus3.getClass().getName());

//GMT+といった書き方
ZoneId gmt1 = ZoneId.of("GMT+01:00");
System.out.println(gmt1 + ",class=" + gmt1.getClass().getName());
ZoneId utc1 = ZoneId.of("UTC+1");
System.out.println(utc1 + ",class=" + utc1.getClass().getName());

//地域ベースID
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
System.out.println(tokyo + ",class=" + tokyo.getClass().getName());

//略称
ZoneId jst = ZoneId.of("JST", ZoneId.SHORT_IDS);
System.out.println(jst + ",class=" + jst.getClass().getName());

//システムデフォルト
ZoneId def= ZoneId.systemDefault();
System.out.println(def + ",class=" + def.getClass().getName());
実行結果
Z,class=java.time.ZoneOffset
+01:00,class=java.time.ZoneOffset
-03:00,class=java.time.ZoneOffset
GMT+01:00,class=java.time.ZoneRegion
UTC+01:00,class=java.time.ZoneRegion
Asia/Tokyo,class=java.time.ZoneRegion
Asia/Tokyo,class=java.time.ZoneRegion
Asia/Tokyo,class=java.time.ZoneRegion
Zもしくは数字だけの場合ZoneOffset、それ以外に接頭辞を持つものと地域ベースIDはZoneRegionという非公開のクラスが返されています。
いずれもZoneIdのサブクラスなので普通に使う分にはあまり関係がないのでしょう。

略称で取得する場合、第二引数にマッピング情報を渡す必要があります。これはZoneId.SHORT_IDSに定義されています。
略称は外部から与えるようになっているくらいなので、できれば使わない方が良いでしょう。

ZoneOffsetの方は生成方法にもう少し種類があって、ofHoursなど数値でオフセット時間を指定して取得する方法があります。

用途としては、前回紹介したZonedDateTimeを生成するときに渡すのが基本の使い方だと思います。


夏時間の扱い

地域ベースIDで取得した場合、サマータイムの考慮が含まれます。これはjava.time.zone.ZoneRulesに保持されており、サマータイムの実施期間はオフセットが遷移するようになっています。
isFixedOffsetメソッドでオフセットが固定かどうかがわかります。
日本はかつてサマータイムを実施していた時期があったことから、東京のタイムゾーンではこのメソッドはtrueを返します。

ちょうど夏時間の開始・終了をまたぐように生成すると、以下のようになります。
//ロサンゼルスの夏時間開始 2016/3/13 14:00
ZoneId zone = ZoneId.of("America/Los_Angeles");
LocalDate date = LocalDate.of(2016, Month.MARCH, 13);
LocalTime time1 = LocalTime.of(1, 00); //13:00
LocalTime time2 = time1.plusHours(1); //14:00

ZonedDateTime zdt1 = ZonedDateTime.of(date, time1, zone);
System.out.println(zdt1);
ZonedDateTime zdt2 = ZonedDateTime.of(date, time2, zone);
System.out.println(zdt2);

long between = ChronoUnit.HOURS.between(zdt1, zdt2);
System.out.println(between);
実行結果
2016-03-13T01:00-08:00[America/Los_Angeles]
2016-03-13T03:00-07:00[America/Los_Angeles]
1
14:00はスキップされ、13時の1時間後は15時となります。その代わりオフセットが-7:00から-8:00になります。

オフセットが代わっているだけなので、時間の差を取ると1となります。

夏時間終了時は同じ時間が2周回ってきます。オフセットが元に戻りますが、差を取るとやはり1時間です。


蛇足 和暦について

これは試験範囲にもなく、今のタイミングで使うべきでもないと思いますが、Date/Time APIではISO暦以外の暦体系も扱えるようになっています。
その中でも和暦については標準で対応しています。
java.time.chrono.JapaneseDateクラスで扱うことができます。
当然次の元号に対応する際にはJVMの更新が必要になるでしょうが、自前で対応する必要がないのは良いですね。


と、い、う、わ、け、で

タイムゾーンについてでした。
自分はタイムゾーンを意識したアプリケーションを書いたことがないのですが、今回勉強したことが役に立つ時が来る・・・かなぁ。
今回も以下のサイトとGoldの通常試験の参考書を参考にしています。
一連の記事は「JavaSE8Gold」ラベルを付けていきます。

それではみなさまよきガジェットライフを(´∀`)ノ


0 件のコメント :
コメントを投稿

▼こちらの記事もどうぞ

▼ブログを気に入っていただけたらRSS登録をお願いします!
▼ブログランキング参加中!応援よろしくお願いします。

スポンサーリンク