JavaのDate and Time APIでサマータイムを確認する

今日は3月の最終日曜日です。

ヨーロッパではサマータイム(daylight saving time)がはじまりましたね。

Java 8の新しいDate and Time APIの使い方を少し確認するために、タイムゾーンが異なる日付データの変換を、サマータイムという題材で試してみました。

イギリスのサマータイム

今回は、イギリスの時刻を見ることにします。

まず、冬時間最終日と夏時間初日のLocalDateTimeを取得します。

		DateTimeFormatter format = DateTimeFormatter
				.ofPattern("yyyy/MMM/d a hh:mm zzzz");

		LocalDateTime winterDay = LocalDateTime
				.of(2014, Month.MARCH, 29, 1, 00);
		LocalDateTime summerDay = LocalDateTime
				.of(2014, Month.MARCH, 30, 1, 00);

3月最終日曜日午前1時〜10月最終日曜日午前1時(UTC基準)

夏時間 - Wikipedia

とのことなので、両日とも午前1時のデータにしました。

次に、タイムゾーンGMTを指定して、ZonedDateTimeに変換します。

		ZoneId gmt0 = ZoneId.of("GMT0");
		ZonedDateTime gmtWinter = ZonedDateTime.of(winterDay, gmt0);
		ZonedDateTime gmtSummer = ZonedDateTime.of(summerDay, gmt0);
		
		System.out.printf("%s (%s)%n", gmtWinter.format(format), gmt0);
		// 2014/3/29 午前 01:00 グリニッジ標準時 (GMT0)
		
		System.out.printf("%s (%s)%n", gmtSummer.format(format), gmt0);
		// 2014/3/30 午前 01:00 グリニッジ標準時 (GMT0)

GMTは夏時間を考慮しないため、日付が1日異なるだけの同時刻(午前1時)のデータが確認できます。

ついでに、タイムゾーンのオフセットも確認しておきます。

		ZoneOffset gmtWinterOffset = gmtWinter.getOffset();
		ZoneOffset gmtSummerOffset = gmtSummer.getOffset();
		assertEquals("GMTでは、冬時間の日と夏時間の日のオフセットは同じ。", 0,
				gmtWinterOffset.compareTo(gmtSummerOffset));

同じタイムゾーンGMT0なので、オフセットももちろん同じですね。

そして、この時刻データを、Europe/LondonタイムゾーンのZonedDateTimeに変換してみます。

		ZoneId euro = ZoneId.of("Europe/London");
		ZonedDateTime euroWinter = gmtWinter.withZoneSameLocal(euro);
		ZonedDateTime euroSummer = gmtSummer.withZoneSameLocal(euro);
		
		System.out.printf("%s (%s)%n", euroWinter.format(format), euro);
		// 2014/3/29 午前 01:00 グリニッジ標準時 (Europe/London)
		
		System.out.printf("%s (%s)%n", euroSummer.format(format), euro);
		// 2014/3/30 午前 02:00 英国夏時間 (Europe/London)

Europe/Londonでは3/30のデータに夏時間が適用され、1時間進んだ午前2時になりました。

タイムゾーンのオフセットも確認しましょう。夏時間は、タイムゾーンのオフセットを変更することで表現されます。

		ZoneOffset euroWinterOffset = euroWinter.getOffset();
		ZoneOffset euroSummerOffset = euroSummer.getOffset();
		assertEquals("Europe/Londonでは、冬時間の日と夏時間の日のオフセットが異なる。", true,
				0 < euroWinterOffset.compareTo(euroSummerOffset));

		assertEquals("Europe/London冬時間適用日のオフセットはゼロ", "Z",
				euroWinterOffset.getId());
		assertEquals("Europe/London夏時間適用日のオフセットは+01:00", "+01:00",
				euroSummerOffset.getId());

夏時間が適用された方の日付データでは、オフセットが1時間プラスされることが確認できました。


日本のサマータイム

ところで、日本でも、第二次大戦後の数年間は、夏時間が導入されていたそうです(最近人から教えていただきました)。なぜ廃止されたかというと、諸説ありますが、ひとつにはサマータイムのせいで労働者の勤務時間が延びたからだとか……なにそれこわい。

Javaの時間系APIではもちろん対応していますので、そちらも確認してみます。

まず、先ほどと同じように、冬時間最終日と夏時間初日のLocalDateTimeを取得します。

		DateTimeFormatter format = DateTimeFormatter
				.ofPattern("yyyy/MMM/d a hh:mm zzzz");
		LocalDateTime winterDay = LocalDateTime.of(1951, Month.MAY, 5, 2, 00);
		LocalDateTime summerDay = LocalDateTime.of(1951, Month.MAY, 6, 2, 00);

ここでは、日本で最後に夏時間が実施された1951年の値を使いました。昭和23年の夏時刻法を改定する形で出された、昭和25年法律第39号の記述を参考にしました。

毎年、五月の第一土曜日の午後十二時から九月の第二土曜日の翌日の午前零時までの間は、すべて中央標準時より一時間進めた時刻(夏時刻)を用いるものとする。

夏時刻法 - Wikipedia

# ちなみに、日本の「午後十二時」は「深夜の十二時(正子)」の意味です。Java英米式を採っているのか、12時間表記で、深夜を「午前十二時」と表記しますよね。

# ちなみに×2、この夏時間は午後十二時から始まるのですが、Javaが使っているtz database(後述)にバグがあるようで、午前二時から始まるように定義されてしまっているため、ここでは午前2時の日付データを作成しました。

次に、先ほどと同様にGMTのZonedDateTimeに変換します。

		ZoneId gmt0 = ZoneId.of("GMT0");
		ZonedDateTime gmtWinter = ZonedDateTime.of(winterDay, gmt0);
		ZonedDateTime gmtSummer = ZonedDateTime.of(summerDay, gmt0);
		
		System.out.printf("%s (%s)%n", gmtWinter.format(format), gmt0);
		// 1951/5/5 午前 02:00 グリニッジ標準時 (GMT0)
		
		System.out.printf("%s (%s)%n", gmtSummer.format(format), gmt0);
		// 1951/5/6 午前 02:00 グリニッジ標準時 (GMT0)

GMTは夏時間を考慮しないため、ここでも日付が1日異なるだけの同時刻(午前2時)のデータが確認できます。

オフセットも同様です。

		ZoneOffset gmtWinterOffset = gmtWinter.getOffset();
		ZoneOffset gmtSummerOffset = gmtSummer.getOffset();
		assertEquals("GMTでは、冬時間の日と夏時間の日のオフセットは同じ。", 0,
				gmtWinterOffset.compareTo(gmtSummerOffset));

そして、タイムゾーンにAsia/Tokyoを指定したZonedDateTimeに変換します。

		ZoneId tokyo = ZoneId.of("Asia/Tokyo");
		ZonedDateTime tokyoWinter = gmtWinter.withZoneSameLocal(tokyo);
		ZonedDateTime tokyoSummer = gmtSummer.withZoneSameLocal(tokyo);
		
		System.out.printf("%s (%s)%n", tokyoWinter.format(format), tokyo);
		// 1951/5/5 午前 02:00 日本標準時 (Asia/Tokyo)
		
		System.out.printf("%s (%s)%n", tokyoSummer.format(format), tokyo);
		// 1951/5/6 午前 03:00 日本夏時間 (Asia/Tokyo)

おぉ! 「日本夏時間」という見慣れない言葉が出てきました。

最後に、オフセットも確認しておきます。

		ZoneOffset tokyoWinterOffset = tokyoWinter.getOffset();
		ZoneOffset tokyoSummerOffset = tokyoSummer.getOffset();
		assertEquals("Asia/Tokyoでは、冬時間の日と夏時間の日のオフセットが異なる。", true,
				0 < tokyoWinterOffset.compareTo(tokyoSummerOffset));

		assertEquals("Asia/Tokyo冬時間適用日のオフセットは+09:00", "+09:00",
				tokyoWinterOffset.getId());
		assertEquals("Asia/Tokyo夏時間適用日のオフセットは+10:00", "+10:00",
				tokyoSummerOffset.getId());

タイムゾーンオフセットが、夏時間適用日には1時間プラスされていることが確認できました。

夏時間対応のJava実装

夏時間対応は、Javaが単独で実装しているわけではなく、内部でtz databaseというIANAの成果物を利用しています。各種のOSや他の言語でも採用されているそうです。

夏時間の実施年や開始と終了の定義(ルール)は、次のファイルに定義されています。(openjdk-7-fcs-src-b147-27_jun_2011で確認)

jdk\make\sun\javazic\tzdata\asia

Rule	Japan	1948	only	-	May	Sun>=1	2:00	1:00	D
Rule	Japan	1948	1951	-	Sep	Sat>=8	2:00	0	S
Rule	Japan	1949	only	-	Apr	Sun>=1	2:00	1:00	D
Rule	Japan	1950	1951	-	May	Sun>=1	2:00	1:00	D

これが先ほど書いた午前2時から開始とされているというバグ(?)ですね。

調べていたら、このバグ報告しようとおっしゃっている人の記事*1に行き着いたので、そのうち直るかもしれません。

日本標準時」「日本夏時間」の文字列リテラルは、次のファイルに定義されています。

jdk\src\windows\lib\tzmappings

package sun.util.resources;

public final class TimeZoneNames_ja extends TimeZoneNamesBundle {

    protected final Object[][] getContents() {
        // ...
        String JST[] = new String[] {"\u65e5\u672c\u6a19\u6e96\u6642", "JST",
                                     "\u65e5\u672c\u590f\u6642\u9593", "JDT"};
        // ...
    }
    // ...
}