Randy Apps


Convenient Apps changing tomorrow.

【JavaScript】タイムゾーン計算 #1:標準機能による特定都市からUTCへの逆算


JavaScriptで日時を扱う際、「現在時刻」や「ブラウザのローカル時刻」を取得するのは非常に簡単です。しかし、「特定のタイムゾーンの、特定の時刻」を基準に計算しようとすると、少し工夫が必要になります。

そもそも、なぜそのような計算が必要になるのでしょうか。例えば、あなたが世界時計アプリやミーティング調整ツールを作っていて、次のような変換機能を実装したいとします。

【やりたいこと(目的)】
「ニューヨークの2026年1月10日 朝7時から会議があります。日本にいる私は、何時にPCの前に座ればいいでしょうか?」という疑問を、プログラムで解決して画面に表示させたい。

この「ニューヨークの時間を日本時間に変換する」という目的を達成するためには、プログラムの内部で以下のような3つのステップを踏むのが基本です。

  1. 【スタート】 ユーザーが指定した「ニューヨーク」の「2026-01-10 朝7時」という文字列を受け取る。
  2. 【絶対時間への変換】 その文字列を、世界共通の絶対的な基準であるUTC(協定世界時)Date オブジェクトに一度変換する。
  3. 【ゴール】 変換したUTCを、実行環境(日本)のローカル時刻に直し、「日本時間では21時ですよ」と出力する。

この記事では、最も重要かつ難易度が高い「ステップ2(指定都市のローカル時刻から、正確なUTCを逆算する)」に焦点を当てます。

外部ライブラリを使わず、JavaScriptの標準機能(Date オブジェクトと Intl オブジェクト)のみでこれを実装する方法について解説しつつ、一見完璧に見えるこの手法に潜む「サマータイム(夏時間)の罠」の入り口までをご紹介します。

標準のDateオブジェクトの限界

まず、シンプルに文字列から Date オブジェクトを作ってみます。

const date = new Date('2026-01-10T07:00:00');

このように記述した時、この時刻は一体どこの時刻でしょうか?

正解は JavaScriptを実行している環境(閲覧しているユーザーのブラウザ)での時刻 となります。日本で実行すれば「日本時間の7時」になりますし、ニューヨークで実行すれば「ニューヨークの7時」になります。

日本で実行しながら「これを強制的にニューヨークの7時として扱ってほしい」と直接指定する引数は、標準の Date には用意されていません。

これを解決するには、文字列の末尾にオフセット(時差)を明記する必要があります。ニューヨークの冬時間であれば -05:00 です。

// これなら「ニューヨークの7時(UTCの12時)」として解釈されます
const date = new Date('2026-01-10T07:00:00-05:00'); 

ここで、この -05:00 というオフセットをどうやって知れば良いでしょうか?「ニューヨークは -05:00 なのは知っているからそれを入れればいい」と考える人もいるかもしれません。

しかし、もし他の都市の時間を日本時間に変換したくなったら、その都市のオフセットを調べる必要があります。この -05:00 というオフセットをプログラムで取得できる様にすれば、他の都市でもそのまま同じプログラムで動くはずです。

つまり、Javascriptの標準機能だけでUTCを逆算するためには、「指定した日時における、その都市の正確なオフセット文字列(-05:00 など)をタイムゾーンから動的に取得し、元の時刻文字列と結合する」という作業が必要になります。

Intlオブジェクトを使ったオフセット取得の実装

標準機能の Intl.DateTimeFormat を活用して、特定都市のオフセット文字列を抽出するコードがこちらです。

const targetTimeStr = '2026-01-10T07:00:00';
const targetTimeZone = 'America/New_York';

// 1. 指定したタイムゾーンで日時をフォーマットする準備
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: targetTimeZone,
  timeZoneName: 'longOffset' // "GMT-05:00" のような形式を指定
});

// 2. 基準となるDateオブジェクトを仮で作成する(※後述する罠があります)
const tempDate = new Date(targetTimeStr);

// 3. formatToPartsを使って、日時データをパーツごとに分解する
const parts = formatter.formatToParts(tempDate);

// 4. 分解したパーツの中から「タイムゾーン名」の部分だけを抽出する
const offsetPart = parts.find(part => part.type === 'timeZoneName').value;
// 例: offsetPart には "GMT-05:00" という文字列が入ります

// 5. 文字列を整形して "GMT-05:00" を "-05:00" にする(GMTなら "Z" にする)
const offsetString = offsetPart.replace('GMT', '') || 'Z';

// 6. 元の時間文字列と、取得したオフセットを結合して最終的なDateオブジェクトを生成します
const finalUtcDate = new Date(`${targetTimeStr}${offsetString}`);

console.log(finalUtcDate.toISOString());
// 出力: 2026-01-10T12:00:00.000Z

Intl.DateTimeFormat は本来、ブラウザの画面上に日時を綺麗に「表示」するための機能です。しかしここでは、表示用ではなく「時差情報を取得するための用途」として利用しています。処理の意図をステップごとに詳しく解説します。

ステップ1の意図:フォーマットの固定化

第一引数に 'en-US' を指定しているのには重要な理由があります。もしここを 'ja-JP' や未指定(ユーザーの環境依存)にしてしまうと、ブラウザやOSの言語設定によって出力される文字の形式がブレてしまう危険性があるからです。英語圏の標準フォーマットに固定し、さらに timeZoneName: 'longOffset' を指定することで、どんな環境で実行しても必ず “GMT-05:00”“GMT+09:00” といった、予測可能でパースしやすい形式の文字列を出力させるように仕向けています。

ステップ2の意図:判定のための「種」を作る

オフセットを取得するには、Intl オブジェクトに「いつの時点のオフセットを調べたいのか」を伝えるための Date オブジェクトを渡す必要があります。しかし、この時点ではまだ正確なUTCが分かっていません。そこで、とりあえずローカル環境(日本など)で解釈された「仮のDateオブジェクト」を作成し、判定の「種」として利用しています。(※実は、この「仮のDate」の作り方こそが、後述する不具合の入り口になります)

ステップ3&4の意図:安全な情報抽出

単にフォーマットするだけなら formatter.format(tempDate) でも文字列(例: “1/10/2026, GMT-05:00″)は取得できます。しかし、そこからカンマやスペースを正規表現などで切り分けて時差部分を抽出するのは、少し面倒ですしバグが発生する原因にもなりそうです。
そこで formatToParts() というメソッドを使います。これを使うと、出力が [{type: 'month', value: '1'}, {type: 'timeZoneName', value: 'GMT-05:00'}...] のような配列のオブジェクトとして綺麗に分解されて返ってきます。あとは配列の find メソッドを使って、type'timeZoneName' になっているパーツをピンポイントで取得すれば、安全にオフセット文字列だけを抽出できます。

ステップ5の意図:ISO 8601形式への適合

抽出できた文字列は “GMT-05:00” ですが、これをそのまま次の new Date() に渡すと、フォーマットエラー(Invalid Date)になるブラウザがあります。Date オブジェクトが解釈できる正しい形式(ISO 8601準拠)は、”GMT” の文字が付いていない “-05:00” という純粋な記号と数字の組み合わせです。そのため、replace('GMT', '') で余計な文字を削除しています。
また、ロンドン(冬時間)などのオフセットがゼロの地域の場合、結果が単なる “GMT” になり、replaceすると空文字になってしまうことがあります。その対策として、空文字になった場合は論理和(||)を使って、UTCを表す “Z” (Zulu time)を代入する様にしています。

ステップ6の意図:完全な絶対時間の生成

ここまで苦労して抽出・整形したオフセット文字列(-05:00)を、最初にユーザーが指定した時間文字列(2026-01-10T07:00:00)の末尾に単なる文字列結合でくっつけます。
出来上がった '2026-01-10T07:00:00-05:00' という文字列を new Date() に渡すと、ブラウザは「なるほど、これはローカル時間ではなく、UTCから5時間遅れた地域の7時ですね」と正確に解釈し、世界共通の絶対時間(UTCの12時)を持った完璧な Date オブジェクトを生成してくれます。

ゴール達成:ついに日本時間が判明!

完璧なUTCの Date オブジェクト(finalUtcDate)さえ手に入れば、あとは単純です。冒頭で掲げた「日本にいる私は、何時にPCの前に座ればいいのか?」というゴールを達成してみましょう。

// 実行環境(日本)のローカル時刻として画面に表示する
console.log('日本時間での会議開始時刻:', finalUtcDate.toLocaleString('ja-JP'));
// 出力: 日本時間での会議開始時刻: 2026/1/10 21:00:00

正常に「21時」(ニューヨーク冬時間-05:00との時差14時間)という結果が出力されました! 指定したタイムゾーンの時刻からの逆算、無事にミッションコンプリート(?)です。

完璧に見えるこの方法に潜む「サマータイムの罠」

文字列操作としてはこれで上手くいくように見えますが、実はステップ 2の仮日付を作成する部分に弱点があります。

// 注意が必要なコード
const tempDate = new Date(targetTimeStr);

私が実際にこの問題に遭遇したのは、プログラムの検証中、「どの都市を基準にしても正しく逆算できるか」をテストするため、あえてオーストラリアの「アデレード(Australia/Adelaide)」を対象として検証を行っていた時でした。

アデレードの夏時間の切り替わり日に近い時刻を指定して、上記のコードで一度UTC時刻を逆算し、確認のために再度ローカル時刻に戻してみたところ、本来「1:30」になるはずが「0:30」へと1時間ずれた時刻が取得されてしまったのです。

JavaScriptの標準機能(DateIntl)の組み合わせによるこの計算手法は、なぜ特定の条件下で1時間もズレてしまうのでしょうか。

まとめと次回予告

JavaScriptの標準機能だけでも、文字列操作を組み合わせることで「特定のタイムゾーンの時刻からのUTC逆算」は一応可能です。しかし、実行環境のタイムゾーンとターゲットのタイムゾーンの時差によって、意図せずオフセットを誤判定してしまうリスクを伴います。

次回は、私が遭遇したこのアデレードの事例をもとに、「なぜ1時間ズレてしまうのか」というサマータイムの罠の詳細なメカニズムと、それを標準機能の範囲内で回避しようと試みた方法について解説します。