全3回にわたってお届けしているJSタイムゾーン計算シリーズの最終回です。
第1回、第2回の記事では、JavaScriptの標準機能(Date と Intl)のみを用いて特定都市の時刻からUTC(協定世界時)を逆算する手法と、そこに潜む「実行環境の時差に依存してしまう問題(サマータイムの罠)」について解説しました。
補足しておくと、絶対的な基準である「正確なUTC時刻」さえ手元にあれば、それを「特定都市のローカル時刻」に変換して画面に表示する処理自体は、標準機能の Intl オブジェクトで十分正確に行うことができます。
しかし、その逆である「特定都市のローカル時刻文字列」から「UTC」を逆算して作り出そうとした途端に、標準機能では実行環境の時差という壁に阻まれてしまうのです。
自力でこのエッジケースをすべて網羅するロジックを組むのは現実的ではないというのが私個人の結論です。そこで今回は、この「ローカル時刻からUTCへの変換」という難所のみを安全に処理するために私が導入した、定番の公式ライブラリ「@date-fns/tz」を使った実装方法をご紹介します。
@date-fns/tz とは?
@date-fns/tz は、JavaScriptで定番の日付操作ライブラリ date-fns を拡張するための公式タイムゾーンパッケージです。ブラウザに内蔵されているIANAタイムゾーンデータベース(世界中の時差やサマータイムのルールが記録された標準辞書)にアクセスする仕組みを利用しており、環境に依存しない正確なタイムゾーン計算を提供してくれます。
今回はWebアプリやブラウザ上で手軽に検証できるよう、type="module" を指定してCDN(jsDelivrの +esm 機能)経由で読み込むアプローチを採用します。ビルド環境がなくても依存関係を自動で解決してくれるため、非常に便利です。
<script type="module">
// date-fns本体のパース機能と、@date-fns/tzのタイムゾーン機能を読み込む
import { parseISO } from 'https://cdn.jsdelivr.net/npm/date-fns@4.1.0/+esm';
import { tz } from 'https://cdn.jsdelivr.net/npm/@date-fns/tz@1.4.1/+esm';
// 以降の処理をここに記述します
</script>
シンプルな実装コード
私が実現したかった「指定したタイムゾーンのローカル時刻文字列から、正確なUTCを逆算する」という処理は、これらを組み合わせることで非常にシンプルに記述できます。
ライブラリの恩恵を実感していただくために、第1回で書いた「標準機能だけを使ったコード」と、今回のライブラリを使ったコードを見比べてみましょう。
const targetTimeStr = '2026-01-10T07:00:00';
const targetTimeZone = 'America/New_York';
// 1. フォーマッターの準備
const formatter = new Intl.DateTimeFormat('en-US', { timeZone: targetTimeZone, timeZoneName: 'longOffset' });
// 2. 実行環境の時差に影響される「仮のDate」を作成
const tempDate = new Date(targetTimeStr);
// 3. 配列に分解して時差パーツを抽出
const offsetPart = formatter.formatToParts(tempDate).find(p => p.type === 'timeZoneName').value;
// 4. 文字列の整形(GMTを消すなど)
const offsetString = offsetPart.replace('GMT', '') || 'Z';
// 5. 元の文字列に時差を結合して再生成
const finalUtcDate = new Date(`${targetTimeStr}${offsetString}`);
import { parseISO } from 'https://cdn.jsdelivr.net/npm/date-fns@4.1.0/+esm';
import { tz } from 'https://cdn.jsdelivr.net/npm/@date-fns/tz@1.4.1/+esm';
const targetTimeStr = '2026-01-10T07:00:00';
const targetTimeZone = 'America/New_York';
// 指定したタイムゾーンのコンテキスト(in: tz)を持たせて、時刻文字列を直接パースする
const tzDate = parseISO(targetTimeStr, { in: tz(targetTimeZone) });
// 最後に標準のDateに戻すことで、どの環境でも必ず末尾がZのUTC日時が取得できる
const finalUtcDate = new Date(tzDate.getTime());
console.log('UTC日時:', finalUtcDate.toISOString());
// 出力: 2026-01-10T12:00:00.000Z
何がどう簡単になったのか?
見比べていただければ一目瞭然ですが、あんなに長かった処理が実質1行(parseISO)に置き換わっています。具体的にどの苦労がなくなったのか、詳しく解説します。
- 「仮のDateオブジェクト」が不要になった
標準機能では、Intlにオフセットを計算させるための「種」として、実行環境(ブラウザ)の時差に影響されたtempDateを一度作らざるを得ませんでした(これがサマータイムの罠を引き起こす元凶でした)。ライブラリを使えば、パースの時点でタイムゾーンを指定できるため、厄介な手順が根本から不要になります。最初から「ニューヨークの7時」として安全に計算をスタートできるのです。 - 文字列の分解と抽出が不要になった
formatToPartsを使って配列に変換し、そこからtype === 'timeZoneName'を探し出すという回りくどい処理が丸ごと消えました。 - “GMT” を削る手動の文字列操作が不要になった
取得した “GMT-05:00” から余計な文字をreplaceで削り取り、元の時間文字列の末尾に結合し直すという、バグの温床になりやすい手動の文字列操作が一切なくなりました。
私たちがやるべきことは、parseISO() のオプションに { in: tz('調べたいタイムゾーン') } を指定するだけです。あとはライブラリの裏側で、ブラウザ内蔵のIANAタイムゾーンデータベースと照らし合わせ、実行環境の時差を完全に無視した上で「正確な絶対時間」を自動で算出してくれます。
【おまけ】年月日や時間を「数値」で持っている場合
プログラムの仕様によっては、時刻が文字列('2026-01-10T07:00:00')ではなく、年月日や時間が個別の変数(数値)として分かれているケースもあると思います。
標準の Date オブジェクトでも new Date(2026, 0, 10, 7, 0) のように数値から生成することは可能ですが、これには「タイムゾーンを指定する引数」が存在しないため、強制的に実行環境(日本時間など)として判定されてしまいます。
しかし、@date-fns/tz の TZDate クラスを使えば、引数の最後にタイムゾーン名を直接渡すことで、この問題を美しく解決できます。
import { TZDate } from 'https://cdn.jsdelivr.net/npm/@date-fns/tz@1.4.1/+esm';
// 年月日や時間が別々の変数(数値)で用意されている場合
const y = 2026;
const m = 1; // 1月
const d = 10;
const h = 7; // 朝7時
const min = 0; // 0分
const targetTimeZone = 'America/New_York';
// 引数: 年, 月(※0始まりのため-1する), 日, 時, 分, 秒, ミリ秒, タイムゾーン
// 文字列パースを介さないため、ローカル時間の罠にはまらず安全に生成できる
const tzDate = new TZDate(y, m - 1, d, h, min, 0, 0, targetTimeZone);
const finalUtcDate = new Date(tzDate.getTime());
console.log('UTC日時:', finalUtcDate.toISOString());
// 出力: 2026-01-10T12:00:00.000Z
※注意点として、JavaScript標準の Date の仕様にならい、月(month)のみ「0始まり(1月=0、2月=1…)」になる点だけ気をつけてください。文字列のパース処理を介さないため、環境依存の罠を完全に回避できる非常に堅牢なアプローチです。
サマータイムの切り替え日時の検証
コードが簡潔になることも利点ですが、本来の目的は「サマータイムの境界線付近で発生するズレ」を解消できるかどうかです。
第2回で不具合の要因となった、「アデレード(オーストラリア)」と「ニューヨーク」のデータを用いて、動作を検証してみます。
生成された正確な絶対時間(UTC)さえあれば、標準機能である toLocaleString を使ってどのタイムゾーンの時刻としても正確に出力できるため、ここでは確認用に標準機能を用いてローカル時間へ戻して出力しています。
検証1:アデレードの切り替え直前(夏時間前)
アデレードでは、2026年10月4日の午前2:00に夏時間が開始されます。標準機能だけで逆算しようとした際は、その直前の「1:30」を指定した時に誤って夏時間と判定され、「0:30」へとズレてしまう現象が起きていました。
import { parseISO } from 'https://cdn.jsdelivr.net/npm/date-fns@4.1.0/+esm';
import { tz } from 'https://cdn.jsdelivr.net/npm/@date-fns/tz@1.4.1/+esm';
// アデレード:夏時間開始の30分前
const targetTimeStr = '2026-10-04T01:30:00';
const targetTimeZone = 'Australia/Adelaide';
// ライブラリを使ってUTCへ逆算
const safeTzDate = parseISO(targetTimeStr, { in: tz(targetTimeZone) });
const safeDate = new Date(safeTzDate.getTime());
// 確認のため、標準機能でローカル時間に戻して出力
console.log(safeDate.toLocaleString('ja-JP', { timeZone: targetTimeZone }));
// 出力例: 2026/10/4 1:30:00
検証2:ニューヨークの切り替え直後(夏時間)
ニューヨークでは、2026年3月8日の午前2:00が3:00へと進み、夏時間が始まります。標準機能では「5:00」を指定した時、冬時間と誤認されて「6:00」にズレてしまうケースでした。
import { parseISO } from 'https://cdn.jsdelivr.net/npm/date-fns@4.1.0/+esm';
import { tz } from 'https://cdn.jsdelivr.net/npm/@date-fns/tz@1.4.1/+esm';
// ニューヨーク:夏時間開始から2時間後
const nyTimeStr = '2026-03-08T05:00:00';
const nyTimeZone = 'America/New_York';
// ライブラリを使ってUTCへ逆算
const nySafeTzDate = parseISO(nyTimeStr, { in: tz(nyTimeZone) });
const nySafeDate = new Date(nySafeTzDate.getTime());
// 確認のため、標準機能でローカル時間に戻して出力
console.log(nySafeDate.toLocaleString('ja-JP', { timeZone: nyTimeZone }));
// 出力例: 2026/3/8 5:00:00
検証の結果、どちらのケースにおいても実行環境の時差に影響されることなく、指定したタイムゾーンのサマータイムのルールに従って正確なUTCが逆算できていることが確認できました。
おわりに:適材適所でのライブラリ活用
全3回にわたって、JavaScriptでの時差計算について掘り下げてきました。
「標準機能だけでどこまで実装できるか」を試みることは、エンジニアとして仕組みを理解するために大切なプロセスだと感じています。検証の結果、UTCから各都市の時刻を表示する処理は標準機能で十分対応可能ですが、「特定のローカル時刻からUTCを逆算する」という処理においては、すべてを自前のロジックでカバーしようとするのはバグのリスクを高める要因になると判断しました。
もし、Webアプリでタイムゾーンをまたいだ日時の計算を実装する機会があり、原因不明の時間のズレに悩まされた時は、この記事で触れた「実行環境の時差」や「サマータイムの罠」を思い出し、標準機能とライブラリを適材適所で使い分ける選択肢を検討してみてはいかがでしょうか。