FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

日付の豆知識とJavaの話

こんにちは。 株式会社FLINTERSの高嶋です。

この記事は10周年記念として133日間ブログを書き続けるチャレンジの132日目の記事となります。

なくなった12月31日

ここで少し日付に関する豆知識です。 フィリピンでは1521年から1844年まで日付変更線の”東側”にありました。 しかし1844年12月30日からフィリピンは日付変更線の"西側"に異動することになり、その際に12月31日を削除して翌日を1845年1月1日とすることで周囲の国と日付を調整しました。

フィリピンでかつてこんなことがありましたよという話では豆知識として面白いだけですがエンジニアにとっては少し怖い話ですよね。 フィリピンの1844年12月31日を扱う可能性は低いかもしれませんが、時間というのは政治的な事情に左右され直感的ではない変化をする可能性があるので今後同様のことが起こりえるかも?と思うと不安ですよね。 今回はJavaがどういった対応をしているのかを紹介したいと思います。

Javaでの対応状況

Javaの現在の日付関連の標準ライブラリであるDate Time APIでは過去に起きたこういったタイムゾーンの変更などにしっかりと対応されており、普段使う分にはあまり意識せずにこの問題に対応できるようになっています。

ここではみんなもう導入しているであろうJava21で試します。

import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Main {
    public static void main(String[] args) {
        var PHT = ZoneId.of("Asia/Manila");
        var dayOf12_30 = ZonedDateTime.of(1844, 12, 30, 0, 0, 0, 0,PHT);
        var nextDayOf12_30 = dayOf12_30.plusDays(1);

        System.out.println(dayOf12_30);
        System.out.println(nextDayOf12_30);
    }
}

ちゃんと日付が一つ飛んでますよね。 ちなみにこれをJSTにするとちゃんと1844年12月31日は現れますよ!

import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Main {
    public static void main(String[] args) {
        var JST = ZoneId.of("Asia/Tokyo");
        var dayOf12_30 = ZonedDateTime.of(1844, 12, 30, 0, 0, 0, 0,JST);
        var nextDayOf12_30 = dayOf12_30.plusDays(1);

        System.out.println(dayOf12_30);
        System.out.println(nextDayOf12_30);
    }
}

どうやってるの?

ちゃんと日付飛んでる!っていうのはいいのですが、後で自分で対応せざるを得ない時になった時のためにどうやって実現されているかは気になるところですよね! こういう時はデバッガが便利なのでコードにブレークポイントをはってステップインを連打して日付を飛ばしそうなところを見つけてみましょう

plusDays(1) の処理のどこかでそれが行われているに違いない気がするのでそこにブレークポイントを張ってみます。 (標準ライブラリはステップインの対象から外されている場合があるので、ステップインできなかったら設定の確認をしてみましょう)

ステップインを連打しているとそれっぽいところを見つけました

このコードからはそこはかとなく、時間の調整をしていそうな匂いを感じます。 特にこのZoneRuleなんてもう絶対これが関係しているに違いないって気がする名前をしています。 JavaDocを確認すると、 docs.oracle.com

単一タイムゾーンのゾーンオフセットがどのように変化するかを定義するルール。 このルールはタイムゾーンの履歴および将来のすべての遷移をモデル化します。 ZoneOffsetTransitionは既知の遷移(通常は履歴)に使用されます。 ZoneOffsetTransitionRuleは、アルゴリズムの結果に基づく将来の遷移に使用されます。

ルールはZoneIdを使用してZoneRulesProvider経由でロードされます。 同じルールが複数のゾーンID間で内部的に共有されることがあります。

もうほぼこれにより過去の日付変更などの管理がされている感じがしてきましたね。 さらにZoneRulesProviderというものがいて、そちらから時間の遷移のルールについて提供されていそうですね。

ZoneRulesProviderのJavaDocでは、

docs.oracle.com

システムへのタイムゾーン・ルールのプロバイダ。 このクラスはタイムゾーン・ルールの構成を管理します。staticメソッドは、プロバイダの管理に使用できるpublic APIを提供します。抽象メソッドは、ルールが提供されることを許可するSPIを提供します。

ZoneRulesProviderは、拡張クラス(通常の拡張ディレクトリに配置されるjarファイル)として、Javaプラットフォームのインスタンスにインストールできます。インストールされたプロバイダは、ServiceLoaderクラスで定義されているサービス・プロバイダのロード機能を使用してロードされます。ZoneRulesProviderは、リソース・ディレクトリMETA-INF/services内のjava.time.zone.ZoneRulesProviderというプロバイダ構成ファイルを使用して自身を識別します。このファイルは、完全修飾された具象ゾーン・ルール・プロバイダ・クラス名を指定する行を含んでいるはずです。プロバイダは、クラスパスにそれらを追加するか、registerProvider(java.time.zone.ZoneRulesProvider)メソッド経由で自身を登録することによって、使用可能にすることもできます。

Java仮想マシンは、IANAタイムゾーン・データベース(TZDB)で定義されるタイムゾーンにゾーン・ルールを提供するデフォルト・プロバイダを持っています。システム・プロパティjava.time.zone.DefaultZoneRulesProviderが定義されている場合、具象ZoneRulesProviderクラスの完全修飾名と見なされ、システム・クラス・ローダーを使用してデフォルト・プロバイダとしてロードされます。このシステム・プロパティが定義されていない場合、システム・デフォルト・プロバイダがロードされ、デフォルト・プロバイダとして機能します。

ルールは、ZoneIdで使用されるゾーンIDによって主に参照されます。ゾーン地域IDのみが使用できます。ここではゾーン・オフセットIDは使用されません。

タイムゾーン・ルールは政治的であるため、データはいつでも変わる可能性があります。各プロバイダはゾーンIDごとに最新ルールを提供しますが、ルールがどのように変更されたかの履歴を提供することもできます。

これを見ると基本デフォルトで提供されているもの以外に、必要であれば自分でZoneRulesProviderを定義して利用することもできそうです。

とはいえ、多くの場合は突然日付に関する調整が行われることはなく、事前に決めておいて行われるものなので適切にバージョンアップしていれば問題になることはあまりないかもしれませんが、こういう機能があるんだと知っておくと何かの役に立つかもしれませんね。

まとめ

というわけで、今回は日付の話のちょっとしたトリビアとJavaの日付ライブラリを見てみました。 Javaに限らず普段日付については標準ライブラリでサクッと済ませる場合が多いかもしれませんが、利用している言語・ライブラリにてこの辺の挙動がサポートされているかな?などと今後気にしてみると面白いかもしれませんね。