Java
JavaScript
BTC
サマータイム

利用者の誕生日を新規登録/参照する際に、サマータイムに気をつける

1. 概要

フロントサイドでミリ秒変換して
バックエンドのAPIに時間の情報を送信する場合、
サマータイムを考慮する必要があります。

Javaのメソッドで変換すればサマータイムを処理してくれますが、
javascriptでは処理してくれないからです。

この記事では、Javaがどのようにサマータイムを処理するか
変換の細かい仕様をお伝えします。

※サマータイムの詳細については以下の記事をご覧ください。
夏時間 - Wikipedia

問題

サマータイム期間中に生まれた人の誕生日が
一日ずれて表示される事象が発生しました。

解決方法

サマータイム期間中は、登録/参照時に1時間分
ミリ秒を足し引きする処理を施す必要があります。

2. 詳細

日本でのサマータイム導入期間は以下です。

開始時刻 終了時刻
1948年(昭和23年) 5月2日(日)午前2時 9月11日(土)午前2時
1949年(昭和24年) 4月3日(日)午前2時 9月10日(土)午前2時
1950年(昭和25年) 5月7日(日)午前2時 9月9日(土)午前2時
1951年(昭和26年) 5月6日(日)午前2時 9月8日(土)午前2時

日時⇒ミリ秒

サマータイム開始

5月2日(月)午前2時になると、サマータイムに突入するため
時間が1時間進められて午前3時とミリ秒が同じになります。

検証用ソース

SummerTime.java
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SummerTime {
    public static void main(String[] args) throws ParseException {
        convertToMilliseconds("1948-05-02 02:00:00");
        convertToMilliseconds("1948-05-02 03:00:00");
    }

    private static void convertToMilliseconds(String dateStr) throws ParseException {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = formatter.parse(dateStr);
        System.out.println(dateStr + " -> " + date.getTime());
    }
}

出力結果

1948-05-02 02:00:00 -> -683794800000
1948-05-02 03:00:00 -> -683794800000

午前2時台の変換は以下のようになっています。

1948-05-02 02:00:00 -> -683794800000
1948-05-02 02:00:01 -> -683794799000
1948-05-02 02:00:02 -> -683794798000
・
・
・
1948-05-02 02:59:58 -> -683791202000
1948-05-02 02:59:59 -> -683791201000
1948-05-02 03:00:00 -> -683794800000

午前2時59分59秒999から午前3時にかけて
補正される仕様がわかりました。
ただ、サマータイム期間中は午前2時台は存在しないため
2時から3時の間のミリ秒変換が使用される想定はありません。

サマータイム終了

9月11日(月)午前1時になると、サマータイムから抜けるため
時間を1時間元に戻さなければなりません。

1948-09-11 00:59:59 -> -672397201000
1948-09-11 01:00:00 -> -672393600000

日時表記では差が1秒ですが、
ミリ秒では補正されて3601000ミリ秒となっています。

ミリ秒⇒日時

サマータイム開始

今度はミリ秒から日時表記に戻してみます。

検証用ソース

SummerTime.java
private static void convertToTimeFormat(Long milliSeconds) throws ParseException {
    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateStr = formatter.format(milliSeconds);
    System.out.println(milliSeconds + " -> " + dateStr);
}

出力結果

-683794801000 -> 1948-05-02 01:59:59
-683794800000 -> 1948-05-02 03:00:00
-683794799000 -> 1948-05-02 03:00:01

サマータイム終了

-672397201000 -> 1948-09-11 00:59:59
-672397200000 -> 1948-09-11 01:00:00
-672397199000 -> 1948-09-11 01:00:01
・
・
・
-672393601000 -> 1948-09-11 01:59:59
-672393600000 -> 1948-09-11 01:00:00
-672393599000 -> 1948-09-11 01:00:01

午前1時台が2回くるような仕様となっています。

3. 対処方法

フロントで変換する際に、該当する期間のミリ秒を補正して
時間の情報をAPIに送るよう修正しました。

dateUtil.ts
    /**
     *サマータイムを考慮した時間を取得(APIからフロントへの表示時に変換)
     */
    export function getSummerTimeDate(birthDate){
        let birth = Number(birthDate)
        if ((birth >= -683794800000 && birth <= -672393600001)
            || (birth >= -654764400000 && birth <= -640944000001)
            || (birth >= -620290800000 && birth <= -609494400001)
            || (birth >= -588841200000 && birth <= -578044800001)) {
            birth += 3600000
        }
        return birth
    }

    /**
     *サマータイムを考慮した時間を代入(フロントからAPIへの登録時に変換)
     */
    export function setSummerTimeDate(birthDate){
        let birth = Number(birthDate)
        if ((birth >= -683791200000 && birth <= -672390000001)
            || (birth >= -654760800000 && birth <= -640940400001)
            || (birth >= -620287200000 && birth <= -609490800001)
            || (birth >= -588837600000 && birth <= -578041200001)) {
            birth -= 3600000
        }
        return birth
    }

4. まとめ

サマータイム導入時期の時間を扱うかどうか、仕様を検討する段階で考慮しておいたほうが良いと思います。
また、タイムゾーンにUTCを使用するなど、回避する方法を別途検討すべきだと思います。

5. 参考

日本のサマータイムとJava
ほんとうにあったサマータイムについて