JavaScript Temporal API完全ガイド


JavaScript Temporal APIは、従来のDateオブジェクトの問題を解決する新しい標準APIです。本記事では、Temporal APIの実践的な使い方について詳しく解説します。

Temporal APIの必要性

Dateオブジェクトの問題点

従来のDateオブジェクトには多くの問題がありました。

// ❌ 月が0始まり(混乱の元)
const date = new Date(2024, 0, 15); // 2024年1月15日

// ❌ タイムゾーンの扱いが困難
const now = new Date(); // ローカルタイムゾーン固定

// ❌ 日付操作が複雑
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

// ❌ 不変性がない(ミュータブル)
const original = new Date();
const copy = original;
copy.setDate(10);
console.log(original); // originalも変更される

Temporal APIの利点

// ✅ 直感的なAPI
const date = Temporal.PlainDate.from('2024-01-15');

// ✅ タイムゾーンサポート
const zonedDate = Temporal.ZonedDateTime.from('2024-01-15T12:00:00+09:00[Asia/Tokyo]');

// ✅ 簡単な日付操作
const tomorrow = date.add({ days: 1 });

// ✅ 不変性(イミュータブル)
const original = Temporal.Now.plainDateISO();
const modified = original.add({ days: 1 });
console.log(original); // originalは変更されない

基本的な型

PlainDate - 日付のみ

時刻やタイムゾーンを含まない純粋な日付。

// 作成
const date = Temporal.PlainDate.from('2024-01-15');
const dateFromObject = Temporal.PlainDate.from({
  year: 2024,
  month: 1,
  day: 15,
});

// 現在の日付を取得
const today = Temporal.Now.plainDateISO();

// プロパティへのアクセス
console.log(date.year); // 2024
console.log(date.month); // 1
console.log(date.day); // 15
console.log(date.dayOfWeek); // 1-7 (月曜日が1)
console.log(date.dayOfYear); // 1-366
console.log(date.weekOfYear); // 1-53

// 文字列化
console.log(date.toString()); // "2024-01-15"
console.log(date.toJSON()); // "2024-01-15"

PlainTime - 時刻のみ

日付やタイムゾーンを含まない純粋な時刻。

// 作成
const time = Temporal.PlainTime.from('14:30:00');
const timeFromObject = Temporal.PlainTime.from({
  hour: 14,
  minute: 30,
  second: 0,
});

// 現在の時刻を取得
const now = Temporal.Now.plainTimeISO();

// プロパティへのアクセス
console.log(time.hour); // 14
console.log(time.minute); // 30
console.log(time.second); // 0
console.log(time.millisecond); // 0
console.log(time.microsecond); // 0
console.log(time.nanosecond); // 0

// 文字列化
console.log(time.toString()); // "14:30:00"

PlainDateTime - 日付と時刻

タイムゾーンを含まない日付と時刻。

// 作成
const dateTime = Temporal.PlainDateTime.from('2024-01-15T14:30:00');
const dateTimeFromObject = Temporal.PlainDateTime.from({
  year: 2024,
  month: 1,
  day: 15,
  hour: 14,
  minute: 30,
});

// PlainDateとPlainTimeから結合
const date = Temporal.PlainDate.from('2024-01-15');
const time = Temporal.PlainTime.from('14:30:00');
const combined = date.toPlainDateTime(time);

// 分割
const { date: extractedDate, time: extractedTime } = dateTime;
console.log(extractedDate.toString()); // "2024-01-15"
console.log(extractedTime.toString()); // "14:30:00"

ZonedDateTime - タイムゾーン付き日時

タイムゾーン情報を含む完全な日時。

// 作成
const zonedDateTime = Temporal.ZonedDateTime.from(
  '2024-01-15T14:30:00+09:00[Asia/Tokyo]'
);

// 現在の日時をタイムゾーン付きで取得
const nowInTokyo = Temporal.Now.zonedDateTimeISO('Asia/Tokyo');
const nowInNewYork = Temporal.Now.zonedDateTimeISO('America/New_York');

// タイムゾーン変換
const tokyoTime = Temporal.ZonedDateTime.from(
  '2024-01-15T14:30:00+09:00[Asia/Tokyo]'
);
const newYorkTime = tokyoTime.withTimeZone('America/New_York');

console.log(tokyoTime.toString());
// "2024-01-15T14:30:00+09:00[Asia/Tokyo]"
console.log(newYorkTime.toString());
// "2024-01-15T00:30:00-05:00[America/New_York]"

// タイムゾーン情報へのアクセス
console.log(zonedDateTime.timeZone.id); // "Asia/Tokyo"
console.log(zonedDateTime.offset); // "+09:00"

Duration - 期間の表現

時間の長さを表現します。

// 作成
const duration = Temporal.Duration.from({
  days: 5,
  hours: 3,
  minutes: 30,
});

// 文字列から
const durationFromString = Temporal.Duration.from('P5DT3H30M');

// プロパティへのアクセス
console.log(duration.days); // 5
console.log(duration.hours); // 3
console.log(duration.minutes); // 30

// 期間の計算
const date1 = Temporal.PlainDate.from('2024-01-15');
const date2 = Temporal.PlainDate.from('2024-01-20');
const diff = date1.until(date2);
console.log(diff.days); // 5

// 期間の加算
const sum = duration.add({ hours: 2, minutes: 15 });
console.log(sum.hours); // 5
console.log(sum.minutes); // 45

// 負の期間
const negativeDuration = Temporal.Duration.from({ days: -5 });

日付操作

日付の加算と減算

const date = Temporal.PlainDate.from('2024-01-15');

// 加算
const tomorrow = date.add({ days: 1 });
const nextWeek = date.add({ weeks: 1 });
const nextMonth = date.add({ months: 1 });
const nextYear = date.add({ years: 1 });

console.log(tomorrow.toString()); // "2024-01-16"
console.log(nextWeek.toString()); // "2024-01-22"
console.log(nextMonth.toString()); // "2024-02-15"
console.log(nextYear.toString()); // "2025-01-15"

// 減算
const yesterday = date.subtract({ days: 1 });
const lastWeek = date.subtract({ weeks: 1 });
const lastMonth = date.subtract({ months: 1 });

console.log(yesterday.toString()); // "2024-01-14"
console.log(lastWeek.toString()); // "2024-01-08"
console.log(lastMonth.toString()); // "2023-12-15"

// 複数単位の組み合わせ
const future = date.add({ months: 2, days: 10 });
console.log(future.toString()); // "2024-03-25"

日付の比較

const date1 = Temporal.PlainDate.from('2024-01-15');
const date2 = Temporal.PlainDate.from('2024-01-20');

// 比較メソッド
console.log(Temporal.PlainDate.compare(date1, date2)); // -1 (date1 < date2)
console.log(Temporal.PlainDate.compare(date2, date1)); // 1 (date2 > date1)
console.log(Temporal.PlainDate.compare(date1, date1)); // 0 (equal)

// 等価性チェック
console.log(date1.equals(date2)); // false
console.log(date1.equals(date1)); // true

// 期間の計算
const duration = date1.until(date2);
console.log(duration.days); // 5

const durationInWeeks = date1.until(date2, { largestUnit: 'weeks' });
console.log(durationInWeeks.weeks); // 0
console.log(durationInWeeks.days); // 5

特定の日付への変更

const date = Temporal.PlainDate.from('2024-01-15');

// 特定のフィールドを変更
const newDate = date.with({
  day: 20,
});
console.log(newDate.toString()); // "2024-01-20"

const newMonth = date.with({
  month: 6,
  day: 30,
});
console.log(newMonth.toString()); // "2024-06-30"

// 月の最初/最後の日
const firstDayOfMonth = date.with({ day: 1 });
const lastDayOfMonth = date.with({ day: date.daysInMonth });

console.log(firstDayOfMonth.toString()); // "2024-01-01"
console.log(lastDayOfMonth.toString()); // "2024-01-31"

タイムゾーン操作

タイムゾーン間の変換

// 同じ瞬間を異なるタイムゾーンで表現
const instant = Temporal.Instant.from('2024-01-15T14:30:00Z');

const inTokyo = instant.toZonedDateTimeISO('Asia/Tokyo');
const inNewYork = instant.toZonedDateTimeISO('America/New_York');
const inLondon = instant.toZonedDateTimeISO('Europe/London');

console.log(inTokyo.toString());
// "2024-01-15T23:30:00+09:00[Asia/Tokyo]"
console.log(inNewYork.toString());
// "2024-01-15T09:30:00-05:00[America/New_York]"
console.log(inLondon.toString());
// "2024-01-15T14:30:00+00:00[Europe/London]"

サマータイムの処理

// サマータイム開始前
const beforeDST = Temporal.ZonedDateTime.from(
  '2024-03-10T01:00:00-05:00[America/New_York]'
);

// 2時間後(サマータイム開始をまたぐ)
const afterDST = beforeDST.add({ hours: 2 });

console.log(beforeDST.toString());
// "2024-03-10T01:00:00-05:00[America/New_York]"
console.log(afterDST.toString());
// "2024-03-10T04:00:00-04:00[America/New_York]"
// オフセットが-05:00から-04:00に変化

タイムゾーン情報の取得

const zoned = Temporal.Now.zonedDateTimeISO('Asia/Tokyo');

// タイムゾーンID
console.log(zoned.timeZone.id); // "Asia/Tokyo"

// UTCオフセット
console.log(zoned.offset); // "+09:00"
console.log(zoned.offsetNanoseconds); // 32400000000000

// Instant(絶対時刻)への変換
const instant = zoned.toInstant();
console.log(instant.toString()); // "2024-01-15T05:30:00Z"

実践例

営業日計算

function isWeekday(date) {
  const dayOfWeek = date.dayOfWeek;
  return dayOfWeek >= 1 && dayOfWeek <= 5; // 月-金
}

function addBusinessDays(date, days) {
  let current = date;
  let remaining = days;

  while (remaining > 0) {
    current = current.add({ days: 1 });
    if (isWeekday(current)) {
      remaining--;
    }
  }

  return current;
}

const today = Temporal.PlainDate.from('2024-01-15'); // 月曜日
const fiveBusinessDaysLater = addBusinessDays(today, 5);
console.log(fiveBusinessDaysLater.toString()); // "2024-01-22"

年齢計算

function calculateAge(birthDate, referenceDate = Temporal.Now.plainDateISO()) {
  const duration = birthDate.until(referenceDate, { largestUnit: 'years' });
  return duration.years;
}

const birthDate = Temporal.PlainDate.from('1990-05-15');
const age = calculateAge(birthDate);
console.log(`Age: ${age} years`);

会議スケジューラー

function scheduleRecurringMeeting(startDate, occurrences, interval) {
  const meetings = [];
  let current = startDate;

  for (let i = 0; i < occurrences; i++) {
    meetings.push(current);
    current = current.add({ weeks: interval });
  }

  return meetings;
}

const firstMeeting = Temporal.PlainDateTime.from('2024-01-15T14:00:00');
const meetings = scheduleRecurringMeeting(firstMeeting, 5, 1);

meetings.forEach((meeting, index) => {
  console.log(`Meeting ${index + 1}: ${meeting.toString()}`);
});

タイムゾーン変換ユーティリティ

function convertMeetingTime(meetingTime, fromTZ, toTZ) {
  // ローカル時刻をタイムゾーン付きに変換
  const zonedTime = meetingTime.toZonedDateTime(fromTZ);

  // 別のタイムゾーンに変換
  const convertedTime = zonedTime.withTimeZone(toTZ);

  return {
    original: zonedTime.toPlainDateTime(),
    converted: convertedTime.toPlainDateTime(),
    originalTZ: fromTZ,
    convertedTZ: toTZ,
  };
}

const meeting = Temporal.PlainDateTime.from('2024-01-15T14:00:00');
const result = convertMeetingTime(meeting, 'Asia/Tokyo', 'America/New_York');

console.log(`Original: ${result.original} (${result.originalTZ})`);
console.log(`Converted: ${result.converted} (${result.convertedTZ})`);

ポリフィルの使用

Temporal APIは比較的新しいため、ポリフィルが必要な場合があります。

npm install @js-temporal/polyfill
import { Temporal } from '@js-temporal/polyfill';

const date = Temporal.PlainDate.from('2024-01-15');
console.log(date.toString());

まとめ

Temporal APIは、JavaScriptにおける日時操作を劇的に改善します。

  • 型システム: 用途に応じた適切な型(PlainDate、PlainTime、ZonedDateTimeなど)
  • 不変性: すべての操作が新しいオブジェクトを返す
  • タイムゾーンサポート: 完全なタイムゾーン対応
  • 直感的なAPI: 読みやすく理解しやすいメソッド名

従来のDateオブジェクトの問題を解決し、より安全で予測可能な日時操作を実現します。今後の標準として期待される重要なAPIです。