mike-neckのブログ

Java or Groovy or Swift or Golang

Java8のDate and Time API - LocalDateTimeとZonedDateTimeの相互変換

こんにちわ、みけです。

ここ最近さわってるJava8 Date and Time APIですが、

今日はLocalDateTimeZonedDateTimeの相互変換についてです。

LocalDateTimeZonedDateTimeの変換

ZonedDateTimeからLocalDateTimeへはゾーン情報を取り除くだけなので、

変換の種類は#toLocalDateTimeに限られています。

逆に、LocalDateTimeからZonedDateTimeへの変換は、

新しいゾーン情報、変換前のゾーン、夏時間などの考慮するべきことがあり、

それぞれに対応するメソッドが存在しています。


@RunWith(Enclosed.class)
public class DateTimeTest {

    private static final DateTimeFormatter withoutZone = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");

    private static final DateTimeFormatter withZone = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss Z");

    private static final ZoneId ASIA_TOKYO = ZoneId.of("Asia/Tokyo");

    private static final ZoneId GMT = ZoneId.of("GMT");

    private static final String DATE_TIME = "2014/10/24 14:09:20 +0900";

    //ログをSystem.out.printlnする
    private synchronized static void log(Queue<Output> outputs) {/*省略*/}

    //ログ用のクラス(クラス名、出力、メソッド名を出力)
    private static class Output {/*省略*/}

    public static class ZonedDateTimeとLocalDateTimeの相互変換 {

        private static final String CLASS = ZonedDateTimeとLocalDateTimeの相互変換.class.getSimpleName();

        private static final Queue<Output> QUEUE = new ConcurrentLinkedQueue<>();

        @Rule
        public TestName testName = new TestName();

        @Test
        public void zonedDateTimeからLocalDateTimeに変換する() {
            ZonedDateTime parsed = ZonedDateTime.parse(DATE_TIME, withZone);
            push(withZone.format(parsed));
            LocalDateTime converted = parsed.toLocalDateTime();
            push(withoutZone.format(converted) + "      ");
        }

        @Test(expected = DateTimeException.class)
        public void localDateTimeからゾーン無しでZonedDateTimeに変換できない() {
            LocalDateTime parsed = LocalDateTime.parse(DATE_TIME, withZone);
            push(withoutZone.format(parsed));
            ZonedDateTime.from(parsed);
        }

        @Test
        public void localDateTimeからゾーン指定してZonedDateTimeに変換する() {
            LocalDateTime parsed = LocalDateTime.parse(DATE_TIME, withZone);
            push(withoutZone.format(parsed) + "      ");
            ZonedDateTime converted = ZonedDateTime.of(parsed, ASIA_TOKYO);
            push(withZone.format(converted));
        }

        @Test
        public void localDateTimeから異なるゾーンで変換したZonedDateTimeは異なる時刻になる() {
            LocalDateTime parsed = LocalDateTime.parse(DATE_TIME, withZone);
            push(withoutZone.format(parsed) + "      ");
            ZonedDateTime asiaTokyo = ZonedDateTime.of(parsed, ASIA_TOKYO);
            ZonedDateTime gmt = ZonedDateTime.of(parsed, GMT);
            push(withZone.format(asiaTokyo));
            push(withZone.format(gmt));
            assertThat(gmt.isAfter(asiaTokyo), is(true));
        }

        @Test
        public void localDateTimeからゾーンとオフセット指定してZonedDateTimeに変換する() {
            LocalDateTime parsed = LocalDateTime.parse(DATE_TIME, withZone);
            push(withoutZone.format(parsed) + "      ");
            ZonedDateTime hawaii = ZonedDateTime.ofInstant(parsed,
                    //変換元のゾーン
                    ZoneOffset.ofHours(-10),
                    //変換後のゾーン
                    ZoneId.of("US/Hawaii"));
            push(withZone.format(hawaii));
        }

        @Test
        public void 東京の時刻_Offsetが9時間_をGMTに変換したZonedDateTimeを取得する() {
            LocalDateTime parsed = LocalDateTime.parse(DATE_TIME, withZone);
            push(withoutZone.format(parsed) + "      ");
            ZonedDateTime converted = ZonedDateTime.ofInstant(parsed,
                    ZoneOffset.ofHours(9), GMT);
            ZonedDateTime gmt = ZonedDateTime.of(parsed, GMT);
            push(withZone.format(converted));
            assertThat(converted.isBefore(gmt), is(true));
        }

        @Test
        public void 夏時間を柔軟に考慮するofLocal() {
            //Yew York時間
            ZoneId newYork = ZoneId.of("EST5EDT");
            //EST5EDTで夏時間終了1秒前
            String end = "2014/11/02 01:59:59";
            LocalDateTime parsed = LocalDateTime.parse(end, withoutZone);
            push(withoutZone.format(parsed) + "      ");
            //UTC-4(夏時間)で優先して変換
            ZonedDateTime utcMinus4 = ZonedDateTime.ofLocal(parsed,
                    newYork,
                    ZoneOffset.ofHours(-4));
            push(withZone.format(utcMinus4));
            //UTC-5(冬時間)で優先して変換
            ZonedDateTime utcMinus5 = ZonedDateTime.ofLocal(parsed,
                    newYork,
                    ZoneOffset.ofHours(-5));
            push(withZone.format(utcMinus5));
            assertThat(utcMinus4.isBefore(utcMinus5), is(true));
        }

        @Test
        public void 夏時間を柔軟に考慮するofLocal夏時間終了後() {
            //Yew York時間
            ZoneId newYork = ZoneId.of("EST5EDT");
            //EST5EDTで夏時間終了1秒前
            String end = "2014/11/02 02:00:00";
            LocalDateTime parsed = LocalDateTime.parse(end, withoutZone);
            push(withoutZone.format(parsed) + "      ");
            //UTC-4(夏時間)で優先して変換
            ZonedDateTime utcMinus4 = ZonedDateTime.ofLocal(parsed,
                    newYork,
                    ZoneOffset.ofHours(-4));
            push(withZone.format(utcMinus4));
            //UTC-5(冬時間)で優先して変換
            ZonedDateTime utcMinus5 = ZonedDateTime.ofLocal(parsed,
                    newYork,
                    ZoneOffset.ofHours(-5));
            push(withZone.format(utcMinus5));
        }

        @Test
        public void 夏時間を厳密に判定する_夏時間終了1秒前_minus4() {
            //YEW YORK時間
            ZoneId newYork = ZoneId.of("EST5EDT");
            //EST5EDTで夏時間終了1秒前
            String end = "2014/11/02 01:59:59";
            LocalDateTime parsed = LocalDateTime.parse(end, withoutZone);
            push(withoutZone.format(parsed) + "      ");
            ZonedDateTime converted = ZonedDateTime.ofStrict(parsed,
                    ZoneOffset.ofHours(-4), newYork);
            push(withZone.format(converted));
        }

        @Test(expected = DateTimeException.class)
        public void 夏時間を厳密に判定する_夏時間終了時_minus4でエラー() {
            //YEW YORK時間
            ZoneId newYork = ZoneId.of("EST5EDT");
            //EST5EDTで夏時間終了1秒前
            String end = "2014/11/02 02:00:00";
            LocalDateTime parsed = LocalDateTime.parse(end, withoutZone);
            push(withoutZone.format(parsed) + "      ");
            //EST5EDTで夏時間終了時は2時から3時に変わるので変換エラー(minus5ならパスする)
            ZonedDateTime.ofStrict(parsed, ZoneOffset.ofHours(-4), newYork);
        }

        private void push(String message) {
            QUEUE.offer(new Output(message, CLASS, testName));
        }

        @AfterClass
        public static void outputMessages() {
            log(QUEUE);
        }
    }
}

出力結果

2014/10/24 14:09:20 +0900 <- zonedDateTimeからLocalDateTimeに変換する
2014/10/24 14:09:20       <- zonedDateTimeからLocalDateTimeに変換する
2014/10/24 14:09:20 <- localDateTimeからゾーン無しでZonedDateTimeに変換できない
2014/10/24 14:09:20       <- localDateTimeからゾーン指定してZonedDateTimeに変換する
2014/10/24 14:09:20 +0900 <- localDateTimeからゾーン指定してZonedDateTimeに変換する
2014/10/24 14:09:20       <- localDateTimeからゾーンとオフセット指定してZonedDateTimeに変換する
2014/10/24 14:09:20 -1000 <- localDateTimeからゾーンとオフセット指定してZonedDateTimeに変換する
2014/10/24 14:09:20       <- 東京の時刻_Offsetが9時間_をGMTに変換したZonedDateTimeを取得する
2014/10/24 05:09:20 +0000 <- 東京の時刻_Offsetが9時間_をGMTに変換したZonedDateTimeを取得する
2014/10/24 14:09:20       <- localDateTimeから異なるゾーンで変換したZonedDateTimeは異なる時刻になる
2014/10/24 14:09:20 +0900 <- localDateTimeから異なるゾーンで変換したZonedDateTimeは異なる時刻になる
2014/10/24 14:09:20 +0000 <- localDateTimeから異なるゾーンで変換したZonedDateTimeは異なる時刻になる
2014/11/02 01:59:59       <- 夏時間を柔軟に考慮するofLocal
2014/11/02 01:59:59 -0400 <- 夏時間を柔軟に考慮するofLocal
2014/11/02 01:59:59 -0500 <- 夏時間を柔軟に考慮するofLocal
2014/11/02 02:00:00       <- 夏時間を柔軟に考慮するofLocal夏時間終了後
2014/11/02 02:00:00 -0500 <- 夏時間を柔軟に考慮するofLocal夏時間終了後
2014/11/02 02:00:00 -0500 <- 夏時間を柔軟に考慮するofLocal夏時間終了後
2014/11/02 01:59:59       <- 夏時間を厳密に判定する_夏時間終了1秒前_minus4
2014/11/02 01:59:59 -0400 <- 夏時間を厳密に判定する_夏時間終了1秒前_minus4
2014/11/02 02:00:00       <- 夏時間を厳密に判定する_夏時間終了時_minus4でエラー

最初のテストzonedDateTimeからLocalDateTimeに変換する

ZonedDateTime#toLocalDateTimeを使って、

ZonedDateTimeからLocalDateTimeに変換します。

変換は単純にゾーン情報がなくなるだけです。


2つ目のテストlocalDateTimeからゾーン無しでZonedDateTimeに変換できないでは

ZonedDateTime#from(TemporalAccessor)を使って、

LocalDateTimeからZonedDateTimeに変換しようとしますが、

LocalDateTimeにはゾーン情報はないので、

DateTimeExceptionが発生します。


3つ目のテストlocalDateTimeからゾーン指定してZonedDateTimeに変換するでは、

ZonedDateTime#of(TemporalAccessor, ZoneId)を使い、

変換するゾーン情報を付与するので、例外は発生しません。


4つ目のテストlocalDateTimeから異なるゾーンで変換したZonedDateTimeは異なる時刻になる

では、ZonedDateTime#of(TemporalAccessor, ZoneId)の、

ZoneIdが違えば、得られるZonedDateTimeの時刻も

同時刻にならないことを確認しています。

例では東京とGMTとの2014/10/24 14:09:20に変換されるので、

東京の14:09:20の方が先に訪れる時間であることを確認しています。


5つ目のテストlocalDateTimeからゾーンとオフセット指定してZonedDateTimeに変換する

では、ZonedDateTime#ofInstant(LocalDateTime, ZoneOffset, ZoneId)

変換しています。

最初の引数が日時、第二引数が変換前のゾーン、第三引数が変換後のゾーンです。

この例ではUTC-10(ハワイ)からハワイの時間に変換しているので、

日時は変わっていません。


6つ目のテストは、5つ目のテストの第二引数と第三引数が異なるゾーンを

さすように変更したテストです。

東京の時刻_Offsetが9時間_をGMTに変換したZonedDateTimeを取得する

というテスト名からわかるように、

東京(UTC-9)からGMTに変換しています。

結果、9時間遅い時刻が返されています。


7つ目のテスト夏時間を柔軟に考慮するofLocalは夏時間に関わる変換です。

ニューヨークなど一部の国地域では、夏時間が採用されていて、

夏時間から通常時間(冬時間)に切り替わるタイミングによっては、

変換できる時間に複数の候補が出来る場合があります。

その場合にZoneOffsetで与えられたゾーンを優先して、

時間を変更するのが

ZonedDateTime#ofLocal(LocalDateTime, ZoneId, ZoneOffset)です。

例ではニューヨークの夏時間を採用しています。

ニューヨークの夏時間は2014/11/02 02:00:00にて

冬時間2014/11/02 03:00:00になります。


一方夏時間を厳密に判定するメソッドもあります。

ZonedDateTime#ofStrict(LocalDateTime, ZoneOffset, ZoneId)です。

夏時間を厳密に判定する_夏時間終了1秒前_minus4では、

夏時間の終了1秒前のLocalDateTime(2014/11/02 01:59:59)を

オフセット-4のニューヨーク時刻で変換します。


夏時間を厳密に判定する_夏時間終了時_minus4でエラー

夏時間が終了する時刻(2014/11/02 02:00:00)を

オフセット-4のニューヨーク時刻で変換します。

ただし、ニューヨーク時刻で2014/11/02 02:00:00は存在せず、

2014/11/02 03:00:00になるので、

この例ではDateTimeExceptionが発生します。


7つ目以降のテストは1951年以降夏時間がない日本では

特に使うことはないかもしれませんが、

今後夏時間を採用する可能性がないとも言い切れないので、

念のため、こんなのがあるよ程度に覚えておいた方がよいかもしれません。


ただ、なんか引数の順番とか型とかがアレでアレですね。


以上

次回はDateLocalDateTime/ZonedDateTimeへの相互変換です。