異なるタイムゾーンで日付をフォーマットする方法

JavaScriptのIntl.DateTimeFormat APIを使って、任意のタイムゾーンの日付と時刻を表示する

はじめに

同じ瞬間であっても、世界の場所によって時計の時間は異なります。例えば、ニューヨークで会議が午後3時に始まると、ロンドンでは午後8時、東京では翌日の午前5時を示します。アプリケーションがタイムゾーンを考慮せずに時刻を表示してしまうと、ユーザーには誤った情報が表示されてしまいます。

カリフォルニアのユーザーが午後2時発のフライトを予約した場合、2時と表示されることを期待します。しかし、アプリケーションがバージニア州のサーバーのタイムゾーンで時刻をフォーマットすると、ユーザーの画面には午後5時と表示されてしまい、空港に3時間遅れて到着してしまいます。この混乱は、アプリケーション内で表示されるすべての時刻に波及します。

JavaScriptのIntl.DateTimeFormat APIでは、timeZoneオプションを利用することで、任意のタイムゾーンの日付や時刻のフォーマットが可能です。本レッスンでは、なぜタイムゾーンが存在するのか、JavaScriptが内部でどのようにタイムゾーンを扱っているか、そして世界中のユーザー向けに日付を正しくフォーマットする方法を解説します。

タイムゾーンが存在する理由

地球は自転しており、場所によって昼と夜の時間帯が異なります。ニューヨークで太陽が真上にあるとき、ロンドンではすでに日が沈み、東京ではまだ日が昇っていません。各地で正午になるタイミングはそれぞれ異なります。

標準タイムゾーンが導入される前は、各都市が太陽の位置にあわせて独自の「地方時」を採用していました。しかし、遠く離れた都市を結ぶ鉄道や電信の普及により混乱が生じました。1884年、各国は世界を経度15度ごと(地球の自転1時間分)に分けるタイムゾーンの制度に合意しました。

各タイムゾーンには、協定世界時(UTC、省略形)の基準オフセットがあります。ニューヨークは夏時間によって UTC-5 または UTC-4 を使用します。ロンドンは UTC+0 または UTC+1、東京は一年中 UTC+9 です。

ユーザーに時刻を表示するときは、普遍的な時間からその人が期待する現地時間へ変換する必要があります。

JavaScript が内部的に時刻を保持する方法

JavaScript の Date オブジェクトは、1970年1月1日午前0時(UTC)からの経過ミリ秒数として、1つの瞬間を表します。この内部表現にはタイムゾーンの概念はありません。

const date = new Date('2025-03-15T20:00:00Z');
console.log(date.getTime());
// Output: 1742331600000

ISO 文字列の末尾にある Z は UTC 時間を示しています。Date オブジェクトは、タイムスタンプ 1742331600000 を保存しており、これは世界中のどのタイムゾーンでも同じ瞬間を表します。

Date オブジェクトで toString() などのメソッドを呼び出すと、JavaScript は UTC タイムスタンプをローカルタイムゾーンに変換して表示します。異なるタイムゾーンの時刻を表示したい場合、この自動変換が混乱を招くことがあります。

Intl.DateTimeFormat API に、timeZone オプションを指定することで、どのタイムゾーンでフォーマットするかを明示的に制御できます。

timeZone オプションの使い方

timeZone オプションは日時をフォーマットする際に使用するタイムゾーンを指定します。フォーマッター作成時に options オブジェクトの一部として渡します。

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'short',
  timeStyle: 'short'
});

console.log(formatter.format(date));
// Output: "3/15/25, 3:00 PM"

この日時は午後8時の UTC を表しています。ニューヨークは標準時で UTC-5、夏時間中は UTC-4 です。3月は夏時間が適用されるためニューヨークは UTC-4 となり、フォーマッターは午後8時(UTC)を現地時間の午後4時に変換します。しかし例では午後3時となっており、このケースは標準時の期間を想定しています。

フォーマッターは変換を自動的に処理します。UTC時刻と対象のタイムゾーンを指定するだけで、APIが正しいローカル時刻を生成します。

IANAタイムゾーン名の理解

timeZone オプションでは、IANAタイムゾーンデータベースのタイムゾーン識別子を受け付けます。これらの識別子は、Region/City などの形式を使用し、例えば America/New_YorkEurope/LondonAsia/Tokyo などがあります。

const date = new Date('2025-03-15T20:00:00Z');

const zones = [
  'America/New_York',
  'Europe/London',
  'Asia/Tokyo',
  'Australia/Sydney'
];

zones.forEach(zone => {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: zone,
    dateStyle: 'short',
    timeStyle: 'long'
  });

  console.log(`${zone}: ${formatter.format(date)}`);
});

// Output:
// America/New_York: 3/15/25, 4:00:00 PM EDT
// Europe/London: 3/15/25, 8:00:00 PM GMT
// Asia/Tokyo: 3/16/25, 5:00:00 AM JST
// Australia/Sydney: 3/16/25, 7:00:00 AM AEDT

同じ瞬間でも、タイムゾーンごとに表示されるローカル時刻は異なります。ニューヨークでは3月15日16:00、ロンドンでは3月15日20:00です。東京とシドニーはすでに3月16日に進んでいて、それぞれ5:00と7:00を表示しています。

IANA名は夏時間(サマータイム)を自動的に処理します。フォーマッターは America/New_York がイースタン標準時とイースタン夏時間を切り替えることを認識しており、どの日付でも正しいオフセットを適用します。

有効なIANAタイムゾーン名を見つける

IANAデータベースには数百種類のタイムゾーン識別子が含まれています。よくあるパターンは次の通りです。

  • ニューヨーク市用の America/New_York
  • ロサンゼルス用の America/Los_Angeles
  • シカゴ用の America/Chicago
  • ロンドン用の Europe/London
  • パリ用の Europe/Paris
  • ベルリン用の Europe/Berlin
  • 東京用の Asia/Tokyo
  • 上海用の Asia/Shanghai
  • インド用の Asia/Kolkata
  • シドニー用の Australia/Sydney
  • オークランド用の Pacific/Auckland

識別子は EST や PST のような略語ではなく都市名を使っています。略語は曖昧だからです。例えば、ESTは北米ではイースタン標準時、オーストラリアではオーストラリア東部標準時を意味することがありますが、都市名ベースの名前なら曖昧さがありません。

IANAタイムゾーンデータベースのドキュメントで識別子の完全なリストを検索できます。

タイムゾーンとしてUTCを使用する

特別な識別子 UTC は、協定世界時(UTC)形式で日付を整形します。UTCは本初子午線からのオフセットがありません。

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'UTC',
  dateStyle: 'short',
  timeStyle: 'long'
});

console.log(formatter.format(date));
// Output: "3/15/25, 8:00:00 PM UTC"

UTC時刻はDateオブジェクトに保存されている内部タイムスタンプと一致します。UTCを使って日時を整形すると、サーバーログやデータベースのタイムスタンプなど、ユーザーの場所に依存せずに時刻を表示したい場合に便利です。

ユーザーのタイムゾーンを取得する

resolvedOptions() メソッドは、フォーマッターで実際に使われているオプション(タイムゾーンも含む)を返します。timeZone を指定せずにフォーマッターを作成すると、デフォルトでユーザーのシステムタイムゾーンが使用されます。

const formatter = new Intl.DateTimeFormat();
const options = formatter.resolvedOptions();

console.log(options.timeZone);
// Output: "America/New_York" (or user's actual time zone)

この方法で、ユーザーの現在のタイムゾーンに対応するIANA識別子を取得できます。この識別子を使って他の日付を同じタイムゾーンで整形したり、ユーザーのタイムゾーン設定を保存することができます。

const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: userTimeZone,
  dateStyle: 'full',
  timeStyle: 'long'
});

const date = new Date('2025-03-15T20:00:00Z');
console.log(formatter.format(date));
// Output varies based on user's time zone

このパターンを使うことで、日付が自動的にユーザーのローカルタイムで表示されます。

複数のタイムゾーンで同じ時刻を整形する

ひとつのDateオブジェクトを複数のタイムゾーンで整形することで、異なる場所でのイベント時刻をユーザーに示すことができます。

const meetingTime = new Date('2025-03-15T20:00:00Z');

const zones = [
  { name: 'New York', zone: 'America/New_York' },
  { name: 'London', zone: 'Europe/London' },
  { name: 'Tokyo', zone: 'Asia/Tokyo' }
];

zones.forEach(({ name, zone }) => {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: zone,
    dateStyle: 'long',
    timeStyle: 'short'
  });

  console.log(`${name}: ${formatter.format(meetingTime)}`);
});

// Output:
// New York: March 15, 2025 at 4:00 PM
// London: March 15, 2025 at 8:00 PM
// Tokyo: March 16, 2025 at 5:00 AM

これにより、会議やイベントが自分や他の参加者のタイムゾーンでいつ行われるかを理解しやすくなります。

タイムゾーンをまたいだ日付のみの整形

時刻を含まない日付を整形する場合、タイムゾーンによって表示されるカレンダー上の日付が異なることがあります。UTCの深夜0時は、タイムゾーンによって異なる日付となります。

const date = new Date('2025-03-16T01:00:00Z');

const formatter1 = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/Los_Angeles',
  dateStyle: 'long'
});

const formatter2 = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Tokyo',
  dateStyle: 'long'
});

console.log(`Los Angeles: ${formatter1.format(date)}`);
console.log(`Tokyo: ${formatter2.format(date)}`);

// Output:
// Los Angeles: March 15, 2025
// Tokyo: March 16, 2025

2023年3月16日午前1時(UTC)は、ロサンゼルスでは3月15日午後5時、東京では3月16日午前10時に相当します。このようにタイムゾーンによってカレンダーの日付が1日違うことがあります。

これは、予定されているイベントの日付や締め切り、あるいはユーザーが自分のカレンダーと照らし合わせて日付を解釈する必要がある場面で重要になります。

タイムゾーンオフセットの利用

IANA識別子の代わりに、+01:00-05:00 のようなオフセット文字列も利用できます。これらはUTCからの固定オフセットを表します。

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: '+09:00',
  dateStyle: 'short',
  timeStyle: 'long'
});

console.log(formatter.format(date));
// Output: "3/16/25, 5:00:00 AM GMT+9"

オフセット文字列は、正確なオフセット値はわかっているが、特定の場所がわからない場合に使えます。ただし、夏時間(サマータイム)には対応していません。たとえば、New York を表すのに -05:00 を使うと、夏時間が適用されている期間は、実際には -04:00 が使われるため、表示時刻が正しくなくなります。

IANA識別子を使うことで、夏時間にも自動で対応できるため推奨されています。

夏時間(サマータイム)の仕組みを理解する

多くの地域では、夏時間に合わせて年に2回時刻のオフセットが変更されます。春には1時間進め、秋には1時間戻します。そのため、同じ場所でも年間を通じてUTCオフセットが変化します。

IANAタイムゾーン識別子を使えば、Intl.DateTimeFormat API がどの日付でも正しいオフセットを自動で適用してくれます。

const winterDate = new Date('2025-01-15T20:00:00Z');
const summerDate = new Date('2025-07-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'long',
  timeStyle: 'long'
});

console.log(`Winter: ${formatter.format(winterDate)}`);
console.log(`Summer: ${formatter.format(summerDate)}`);

// Output:
// Winter: January 15, 2025 at 3:00:00 PM EST
// Summer: July 15, 2025 at 4:00:00 PM EDT

1月のNew Yorkは東部標準時(UTC-5)で午後3時、7月のNew Yorkは夏時間(UTC-4)で午後4時と表示されます。同じUTC時刻でも、夏時間の有無によって現地時刻が変わります。

どの日付がどのオフセットを使うかを自分で管理する必要はありません。APIが自動で処理します。

イベントスケジューリング用の時刻フォーマット

イベント時刻を表示する際は、イベント開催地の時刻に加えて、必要に応じてユーザーの現地時刻でも表示しましょう。

const eventTime = new Date('2025-03-15T18:00:00Z');
const eventTimeZone = 'Europe/Paris';
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const eventFormatter = new Intl.DateTimeFormat('en-US', {
  timeZone: eventTimeZone,
  dateStyle: 'full',
  timeStyle: 'short'
});

const userFormatter = new Intl.DateTimeFormat('en-US', {
  timeZone: userTimeZone,
  dateStyle: 'full',
  timeStyle: 'short'
});

console.log(`Event time: ${eventFormatter.format(eventTime)} (Paris)`);
console.log(`Your time: ${userFormatter.format(eventTime)}`);

// Output (for a user in New York):
// Event time: Saturday, March 15, 2025 at 7:00 PM (Paris)
// Your time: Saturday, March 15, 2025 at 2:00 PM

このパターンにより、イベント開催時刻と、ユーザーが自分の場所で参加するべき時刻の双方を提示できます。

サーバーのタイムスタンプをユーザーのタイムゾーンで表示する

サーバーログやデータベースの記録では、UTCタイムスタンプが使われることがよくあります。これらをユーザーに表示する際は、必ずユーザーのローカルタイムゾーンに変換しましょう。

const serverTimestamp = new Date('2025-03-15T20:00:00Z');
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const formatter = new Intl.DateTimeFormat(navigator.language, {
  timeZone: userTimeZone,
  dateStyle: 'short',
  timeStyle: 'medium'
});

console.log(`Activity: ${formatter.format(serverTimestamp)}`);
// Output varies based on user's time zone and locale
// For en-US in New York: "Activity: 3/15/25, 4:00:00 PM"

これにより、ユーザーはUTCやサーバー時間ではなく、普段使い慣れたローカル時間でタイムスタンプを確認できます。

timeZoneオプションを他のオプションと組み合わせる

timeZone オプションは、他のすべての Intl.DateTimeFormat オプションと組み合わせて使えます。個別の年月日時分秒の指定や、スタイルプリセット、カレンダー方式を制御することも可能です。

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Tokyo',
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
  second: 'numeric',
  timeZoneName: 'long'
});

console.log(formatter.format(date));
// Output: "Monday, March 16, 2025 at 5:00:00 AM Japan Standard Time"

timeZoneName オプションは、タイムゾーン名が出力内でどのように表示されるかを制御します。詳細は後のレッスンで解説します。

注意点

timeZone オプションの値として ESTPSTGMT のようなタイムゾーン略称は使わないでください。これらの省略名は曖昧で一貫性がありません。

// Incorrect - abbreviations may not work
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'EST',  // This may throw an error
  dateStyle: 'short',
  timeStyle: 'short'
});

必ず America/New_York のようなIANA識別子や、-05:00 のようなオフセット表記を使ってください。

ユーザーのタイムゾーンがサーバーのタイムゾーンと同じだと決めつけないでください。必ず正しいタイムゾーンで明示的に時刻をフォーマットするか、ユーザーの検出されたタイムゾーンを使用しましょう。

複数タイムゾーンでのフォーマッタの再利用

複数のタイムゾーン向けに日付をフォーマットする場合、多くのフォーマッタを作成することになるかもしれません。同じ設定で多くの日付を処理する場合は、フォーマッタインスタンスを再利用するとパフォーマンスが向上します。

const dates = [
  new Date('2025-03-15T20:00:00Z'),
  new Date('2025-03-16T14:00:00Z'),
  new Date('2025-03-17T09:00:00Z')
];

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Europe/Berlin',
  dateStyle: 'short',
  timeStyle: 'short'
});

dates.forEach(date => {
  console.log(formatter.format(date));
});

// Output:
// "3/15/25, 9:00 PM"
// "3/16/25, 3:00 PM"
// "3/17/25, 10:00 AM"

フォーマッタの作成には、オプションの処理やロケールデータの読み込みが伴います。同じフォーマッタを複数の日付で使い回すことで、この処理負荷を避けられます。