読者です 読者をやめる 読者になる 読者になる

俺の備忘録

個人的な備忘録です。

Calendarクラス(java)のサマータイム切り替わり前後の時刻の挙動を検証してみた

java

はじめに

日本国内だけならあまり問題になることはないが、海外をメインに売っている製品開発に携わっていると、度々タイムゾーンサマータイムの問題に悩まされる。 つい最近もサマータイムの問題に悩まされたため、 javaのCalendarクラスのサマータイム切り替わり前後の時刻の挙動についてメモしておく。

そもそもサマータイムって?

サマータイムとは、夏に時計の時間を1時間進める制度のこと。
なんか明るい時間を有効に使うために生まれた制度らしい。
サマータイムの開始や終了の時期や時刻、進める時間の量は地域等によって異なる。 アメリカのニューヨーク(東部標準時)を例に挙げると、夏時間は毎年3月第2日曜日の02:00から始まり、11月の第1日曜日の午前02:00に終了する。

図にしてみると、以下のように、標準時から夏時間に変わるときは02:00台がなくなるため、これにより時計が1時間早まる。逆に出るときは、01:00台が2回現れ、時計が1時間遅くなる。

f:id:magayengineer:20161218233243p:plain

サマータイムはIT屋さんには悩ましい問題である

普通の日本人なら、時計が1時間早まったり遅くなったりすることに対して「ふーん。」程度にしか思わないだろうが、IT屋さんにとっては非常に悩ましい問題である。
例えば、 - 毎日02:30にあるバッチを定期実行するようにスケジュール登録している場合
=> サマータイム開始日の省略される02:30にはバッチは実行されるの????

  • 毎日01:00にあるバッチを定期実行するようにスケジュール登録している場合 => サマータイムが終了する日は、夏時間の01:00に実行されるの?それとも標準時の01:00に実行されるの?

っと言った感じに、意図した通りにバッチやタスクが実行されるのか、しばしばよくわからなくなってしまうのである。

Calendarクラスのサマータイム切り替わり前後の挙動検証

標準時からサマータイムへの切り替わり前後

省略される時間帯の時刻を時刻にセットしてみる

テストコード

// 東部時間(アメリカ/ニューヨーク)にデフォルトタイムゾーンをセット
TimeZone tz = TimeZone.getTimeZone("America/New_York");
TimeZone.setDefault(tz);

SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm z");
sdFormat.setTimeZone(tz);

Calendar Cal0159 = Calendar.getInstance();
Cal0159.set(2017, 2, 12, 1, 59, 0);
System.out.println("入力:2017/03/12 01:59 => 出力:" + sdFormat.format(Cal0159.getTime()));

Calendar Cal0200 = Calendar.getInstance();
Cal0200.set(2017, 2, 12, 2, 00, 0);
System.out.println("入力:2017/03/12 02:00 => 出力:" + sdFormat.format(Cal0200.getTime()));

Calendar Cal0230 = Calendar.getInstance();
Cal0230.set(2017, 2, 12, 2, 30, 0);
System.out.println("入力:2017/03/12 02:30 => 出力:" + sdFormat.format(Cal0230.getTime()));

Calendar Cal0259 = Calendar.getInstance();
Cal0259.set(2017, 2, 12, 2, 59, 0);
System.out.println("入力:2017/03/12 02:59 => 出力:" + sdFormat.format(Cal0259.getTime()));

Calendar Cal0300 = Calendar.getInstance();
Cal0300.set(2017, 2, 12, 3, 00, 0);
System.out.println("入力:2017/03/12 03:00 => 出力:" + sdFormat.format(Cal0300.getTime()));

実行結果

入力:2017/03/12 01:59 => 出力:2017/03/12 01:59 EST
入力:2017/03/12 02:00 => 出力:2017/03/12 03:00 EDT
入力:2017/03/12 02:30 => 出力:2017/03/12 03:30 EDT
入力:2017/03/12 02:59 => 出力:2017/03/12 03:59 EDT
入力:2017/03/12 03:00 => 出力:2017/03/12 03:00 EDT

省略される時間に設定した場合は+01:00されることがわかる。

時間を加算してみる

テストコード

// 東部時間(アメリカ/ニューヨーク)にデフォルトタイムゾーンをセット
TimeZone tz = TimeZone.getTimeZone("America/New_York");
TimeZone.setDefault(tz);

SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm z");
sdFormat.setTimeZone(tz);

String before = "";

Calendar Cal0130 = Calendar.getInstance();
Cal0130.set(2017, 2, 12, 1, 30, 0);
before = sdFormat.format(Cal0130.getTime());
Cal0130.add(Calendar.HOUR_OF_DAY, 1);
System.out.println(before + " + 1時間 => " + sdFormat.format(Cal0130.getTime()));

Calendar Cal0230Yesterday = Calendar.getInstance();
Cal0230Yesterday.set(2017, 2, 11, 2, 30, 0);
before = sdFormat.format(Cal0230Yesterday.getTime());
Cal0230Yesterday.add(Calendar.DAY_OF_MONTH, 1);
System.out.println(before + " + 1日   => " + sdFormat.format(Cal0230Yesterday.getTime()));


Calendar Cal0300Yesterday = Calendar.getInstance();
Cal0300Yesterday.set(2017, 2, 11, 3, 0, 0);
before = sdFormat.format(Cal0300Yesterday.getTime());
Cal0300Yesterday.add(Calendar.DAY_OF_MONTH, 1);
System.out.println(before + " + 1日   => " + sdFormat.format(Cal0300Yesterday.getTime()));

実行結果

2017/03/12 01:30 EST + 1時間 => 2017/03/12 03:30 EDT
2017/03/11 02:30 EST + 1日   => 2017/03/12 01:30 EST
2017/03/11 03:00 EST + 1日   => 2017/03/12 03:00 EDT

1時間Addしたときは、省略される時間を飛ばして03:30台になったが、 サマータイムに入る前日の02:30から1日足した時は、03:30にはならずに +23時間の01:30になった。どういうことだってばよ。

時間を減算してみる

テストコード

// 東部時間(アメリカ/ニューヨーク)にデフォルトタイムゾーンをセット
TimeZone tz = TimeZone.getTimeZone("America/New_York");
TimeZone.setDefault(tz);

SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm z");
sdFormat.setTimeZone(tz);

String before = "";

Calendar Cal0200 = Calendar.getInstance();
Cal0200.set(2017, 2, 12, 2, 0, 0);
before = sdFormat.format(Cal0200.getTime());
Cal0200.add(Calendar.HOUR_OF_DAY, -1);
System.out.println(before + " - 1時間 => :" + sdFormat.format(Cal0200.getTime()));

Calendar Cal0230 = Calendar.getInstance();
Cal0230.set(2017, 2, 12, 2, 30, 0);
before = sdFormat.format(Cal0230.getTime());
Cal0230.add(Calendar.HOUR_OF_DAY, -1);
System.out.println(before + " - 1時間 => :" + sdFormat.format(Cal0230.getTime()));

Calendar Cal0300 = Calendar.getInstance();
Cal0300.set(2017, 2, 12, 3, 00, 0);
before = sdFormat.format(Cal0300.getTime());
Cal0300.add(Calendar.HOUR_OF_DAY, -1);
System.out.println(before + " - 1時間   => :" + sdFormat.format(Cal0300.getTime()));


Calendar Cal0200Tomo = Calendar.getInstance();
Cal0200Tomo.set(2017, 2, 13, 2, 0, 0);
before = sdFormat.format(Cal0200Tomo.getTime());
Cal0200Tomo.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(before + " - 1日 => " + sdFormat.format(Cal0200Tomo.getTime()));

Calendar Cal0230Tomo  = Calendar.getInstance();
Cal0230Tomo.set(2017, 2, 13, 2, 30, 0);
before = sdFormat.format(Cal0230Tomo.getTime());
Cal0230Tomo.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(before + " - 1日 => " + sdFormat.format(Cal0230Tomo.getTime()));

Calendar Cal0300Tomo  = Calendar.getInstance();
Cal0300Tomo.set(2017, 2, 13, 3, 00, 0);
before = sdFormat.format(Cal0300Tomo.getTime());
Cal0300Tomo.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(before + " - 1日   => " + sdFormat.format(Cal0300Tomo.getTime()));

実行結果

2017/03/12 03:00 EDT - 1時間 => :2017/03/12 01:00 EST
2017/03/12 03:30 EDT - 1時間 => :2017/03/12 01:30 EST
2017/03/12 03:00 EDT - 1時間   => :2017/03/12 01:00 EST
2017/03/13 02:00 EDT - 1日 => 2017/03/12 03:00 EDT
2017/03/13 02:30 EDT - 1日 => 2017/03/12 03:30 EDT
2017/03/13 03:00 EDT - 1日   => 2017/03/12 03:00 EDT

やはり1時間引いた時は、省略される02:00台が飛ばされた形の結果になり、 1日引いたときは、-23時間されている。

サマータイムから標準時への切り替わり前後

重複する時間帯前後に時刻をセットしてみる

テストコード

// 東部時間(アメリカ/ニューヨーク)にデフォルトタイムゾーンをセット
TimeZone tz = TimeZone.getTimeZone("America/New_York");
TimeZone.setDefault(tz);

SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm z");
sdFormat.setTimeZone(tz);

Calendar Cal0030 = Calendar.getInstance();
Cal0030.set(2017, 10, 5, 0, 30, 0);
System.out.println("入力:2017/11/5 00:30 => 出力:" + sdFormat.format(Cal0030.getTime()));

Calendar Cal0100 = Calendar.getInstance();
Cal0100.set(2017, 10, 5, 1, 00, 0);
System.out.println("入力:2017/11/5 01:00 => 出力:" + sdFormat.format(Cal0100.getTime()));

Calendar Cal0130 = Calendar.getInstance();
Cal0130.set(2017, 10, 5, 1, 30, 0);
System.out.println("入力:2017/11/5 01:30 => 出力:" + sdFormat.format(Cal0130.getTime()));

Calendar Cal0200 = Calendar.getInstance();
Cal0200.set(2017, 10, 5, 2, 00, 0);
System.out.println("入力:2017/11/5 02:00 => 出力:" + sdFormat.format(Cal0200.getTime()));

実行結果

入力:2017/11/5 00:30 => 出力:2017/11/05 00:30 EDT
入力:2017/11/5 01:00 => 出力:2017/11/05 01:00 EST
入力:2017/11/5 01:30 => 出力:2017/11/05 01:30 EST
入力:2017/11/5 02:00 => 出力:2017/11/05 02:00 EST

重複する01:00台をセットした場合は、夏時間の01:00にはならずに 標準時の01:00台になるようだ。

CalendarにAddしてみる

テストコード

// 東部時間(アメリカ/ニューヨーク)にデフォルトタイムゾーンをセット
TimeZone tz = TimeZone.getTimeZone("America/New_York");
TimeZone.setDefault(tz);

SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm z");
sdFormat.setTimeZone(tz);

String before = "";

Calendar Cal0030 = Calendar.getInstance();
Cal0030.set(2017, 10, 5, 0, 30, 0);
before = sdFormat.format(Cal0030.getTime());
Cal0030.add(Calendar.HOUR_OF_DAY, 1);
System.out.println(before + " + 1時間 => " + sdFormat.format(Cal0030.getTime()));

Calendar Cal0030_2 = Calendar.getInstance();
Cal0030_2.set(2017, 10, 5, 0, 30, 0);
before = sdFormat.format(Cal0030_2.getTime());
Cal0030_2.add(Calendar.HOUR_OF_DAY, 2);
System.out.println(before + " + 2時間 => " + sdFormat.format(Cal0030_2.getTime()));

Calendar Cal0130Yesterday = Calendar.getInstance();
Cal0130Yesterday.set(2017, 10, 4, 1, 30, 0);
before = sdFormat.format(Cal0130Yesterday.getTime());
Cal0130Yesterday.add(Calendar.DAY_OF_MONTH, 1);
System.out.println(before + " + 1日   => " + sdFormat.format(Cal0130Yesterday.getTime()));

Calendar Cal0230Yesterday = Calendar.getInstance();
Cal0230Yesterday.set(2017, 10, 4, 2, 30, 0);
before = sdFormat.format(Cal0230Yesterday.getTime());
Cal0230Yesterday.add(Calendar.DAY_OF_MONTH, 1);
System.out.println(before + " + 1日   => " + sdFormat.format(Cal0230Yesterday.getTime()));

実行結果

2017/11/05 00:30 EDT + 1時間 => 2017/11/05 01:30 EDT
2017/11/05 00:30 EDT + 2時間 => 2017/11/05 01:30 EST
2017/11/04 01:30 EDT + 1日   => 2017/11/05 01:30 EDT
2017/11/04 02:30 EDT + 1日   => 2017/11/05 02:30 EST

時を足すと、EDTの01:00台、ESTの01:00台の順に進む。 前日から1日を足すケースは02:00が境界になっているようで、 前日の01:30から1日足すと、24時間加算され、前日の02:30から1日足すと 25時間足されている。

時刻を減算してみる

テストコード

// 東部時間(アメリカ/ニューヨーク)にデフォルトタイムゾーンをセット
TimeZone tz = TimeZone.getTimeZone("America/New_York");
TimeZone.setDefault(tz);

SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm z");
sdFormat.setTimeZone(tz);

String before = "";

Calendar Cal0230 = Calendar.getInstance();
Cal0230.set(2017, 10, 5, 2, 30, 0);
before = sdFormat.format(Cal0230.getTime());
Cal0230.add(Calendar.HOUR_OF_DAY, -1);
System.out.println(before + " - 1時間 => " + sdFormat.format(Cal0230.getTime()));

Calendar Cal0230_2 = Calendar.getInstance();
Cal0230_2.set(2017, 10, 5, 2, 30, 0);
before = sdFormat.format(Cal0230_2.getTime());
Cal0230_2.add(Calendar.HOUR_OF_DAY, -2);
System.out.println(before + " - 2時間 => " + sdFormat.format(Cal0230_2.getTime()));

Calendar Cal0030Tomo = Calendar.getInstance();
Cal0030Tomo.set(2017, 10, 6, 0, 30, 0);
before = sdFormat.format(Cal0030Tomo.getTime());
Cal0030Tomo.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(before + " - 1日   => " + sdFormat.format(Cal0030Tomo.getTime()));

Calendar Cal0100Tomo = Calendar.getInstance();
Cal0100Tomo.set(2017, 10, 6, 1, 00, 0);
before = sdFormat.format(Cal0100Tomo.getTime());
Cal0100Tomo.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(before + " - 1日   => " + sdFormat.format(Cal0100Tomo.getTime()));

Calendar Cal0130Tomo = Calendar.getInstance();
Cal0130Tomo.set(2017, 10, 6, 1, 30, 0);
before = sdFormat.format(Cal0130Tomo.getTime());
Cal0130Tomo.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(before + " - 1日   => " + sdFormat.format(Cal0130Tomo.getTime()));

Calendar Cal0230Tomo = Calendar.getInstance();
Cal0230Tomo.set(2017, 10, 6, 2, 30, 0);
before = sdFormat.format(Cal0230Tomo.getTime());
Cal0230Tomo.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(before + " - 1日   => " + sdFormat.format(Cal0230Tomo.getTime()));

実行結果

2017/11/05 02:30 EST - 1時間 => 2017/11/05 01:30 EST
2017/11/05 02:30 EST - 2時間 => 2017/11/05 01:30 EDT
2017/11/06 00:30 EST - 1日   => 2017/11/05 00:30 EDT
2017/11/06 01:00 EST - 1日   => 2017/11/05 01:00 EST
2017/11/06 01:30 EST - 1日   => 2017/11/05 01:30 EST
2017/11/06 02:30 EST - 1日   => 2017/11/05 02:30 EST

時を減算する場合は、ESTの01:00台、EDTの01:00台の順に遷移する。 標準時2日目から1日減算するケースでは、01:00が境界になっていて、 01:00より前だと25時間引かれ、01:00以降だと24時間引かれる結果となっている。

まとめ

まとめると以下の挙動らしい。
- カレンダーを省略される時間帯(02:00台)にセットすると、夏時間の03:00台として扱われる。
- カレンダーを省略される時間帯に入るように時間をAddした場合は、時を足し引きした場合は、省略される時間帯(02:00)がスキップされる。日をたし引きした場合は、23時間たし引きされるケースと、24時間足し引きされるケースがある。
- カレンダーを重複される時間帯(01:00台)にセットすると、標準時の01:00台として扱われる。
- カレンダーを重複される時間帯(01:00台)に入るように時間を引くと、時を足し引きした場合は、重複する時間帯(01:00)がそれぞれ別の1時間として扱われる。 日を足し引きした場合は、境界前後で24時間加減算されるか25時間加減算されるかが切り替わる。

海外に売るソフトウェアを作る場合は、サマータイムにはくれぐれも気をつけましょう。