JavaScriptでローカルタイムのDateオブジェクトを夏時間終了前後のUTCへ変換する

世界中からアクセスされるWebアプリケーションで時刻を扱うとき夏時間を考慮する必要がある。

  • サーバはユーザやクライアント環境(OS)の属しているタイムゾーンを知らない。
  • ユーザの属しているタイムゾーンはクライアント環境(OS)のタイムゾーンと同じ。

というのが一般的に考えられる状況だ。

ここで、ある時系列データ(UTC時刻と値のセット)からユーザが入力した時刻(ローカルタイム)範囲に含まれるデータを検索することを考える。

一般的に夏時間の終了日は1日25時間となり、同一日に同じ時刻が2度現れることになる。このため論理的にはローカルタイムからUTCへの変換はできないということになる。

かといって時刻範囲の開始時刻、終了時刻、それぞれの時刻が夏時間かどうか、という入力を求めるのはユーザにとって面倒だ。

例えばアメリカ合衆国のユーザが時刻範囲の開始時刻として2018年11月4日 午前01:00(夏時間終了の重複時刻)を入力した場合、それは夏時間だろうか。終了時刻の場合はどうだろうか。

入力した時刻範囲で1日25時間を網羅することを考えると、開始時刻の場合は夏時間、終了時刻なら夏時間終了後と解釈すべきだろう。

こんなときクライアント側で夏時間終了前後を指定してローカルタイムをUTCへ変換する簡便な手段が欲しくなる。

ECMAScript® 2019 Language Specification によると夏時間終了の重複時刻から生成されたDateオブジェクトは、常に夏時間終了前を指すと定義されている。

つまりJavaScriptで夏時間終了の重複時刻から夏時間終了「後」のUTCを指すDateオブジェクトを得るための正当な方法は無いのだ。

もし moment.js を使っていたとしても、その実体はDateオブジェクトなので同じことだ。特定のタイムゾーンから別のタイムゾーンへの変換といった高度な変換が必要な場合には Moment Timezone を使ってもいいだろう。

ここではもっと簡便な手段を考えてみよう。

function utc(localtime, after) {
  const result = new Date(localtime)
  if (after) {
    const clock = date => date.getUTCHours() * 60 + date.getUTCMinutes()
    const nextday = new Date(localtime)
    nextday.setDate(localtime.getDate() + 1)
    const adjust = clock(nextday) - clock(localtime)
    if (adjust > 0) {
      const advanced = new Date(localtime).setMinutes(localtime.getMinutes() + adjust)
      const advancedUTC = new Date(localtime).setUTCMinutes(localtime.getUTCMinutes() + adjust)
      if (advanced !== advancedUTC) {
        result.setUTCMinutes(localtime.getUTCMinutes() + adjust)
      }
    }
  }
  return result
}

ローカルタイム上の翌日同一時刻と元の時刻とのUTC時分の差をとると24時間以内に夏時間終了に伴って修正された時間(重複期間、多くの場合1時間)がわかる。

元の時刻をローカルタイム上とUTC上それぞれで重複期間だけ進め、その差がある場合、元の時刻は夏時間終了の重複時刻であることがわかることになる。

恐らくほとんどのケースではこの対応で十分だろうと思う。

ほとんど、と書いたのは、時刻を扱うポリシーは慎重に検討されるべきだからだ。

冒頭で

  • サーバはユーザやクライアント環境(OS)の属しているタイムゾーンを知らない。
  • ユーザの属しているタイムゾーンはクライアント環境(OS)のタイムゾーンと同じ。

という条件を書いたが、これが本当に守られるか、よく考えてほしい。

例えば普段は日本に住んでおり一時的にアメリカ合衆国でアプリを使用する場合も、アプリ上の時刻は日本時刻として扱いたい。が、OSのタイムゾーンは現地に合わせるといった使われ方をする場合、クライアント環境のタイムゾーンとユーザが欲するタイムゾーンが違うことになり、上記の関数は意味を成さないことになる。

また、実はもう一つ重要な前提が隠れている。

  • クライアント環境(OS)のタイムゾーンデータベースは正しくメンテナンスされている。

例えばブラジルの夏時間は毎年実施時期が異なるし、そもそもタイムゾーンというのは人間が決めた法律で年々改定されていくためタイムゾーンデータベースの更新が必要だ。

もし特殊な事情でそれができないようなクローズド、アンマネージドな環境でそのアプリが使われる場合、上記のような対処では不完全だ。