Menghitung Selisih Peristiwa dan Memformat Durasi dengan Temporal

Menghitung waktu yang telah berlalu atau tersisa di antara peristiwa (seperti masa berlaku uji coba, hitung mundur langganan, atau periode SLA) dan menampilkan rentang tersebut dalam format yang dilokalkan dan dapat dibaca manusia secara historis memerlukan penghitungan manual atau library eksternal. Objek Date lama hanya melacak milidetik epoch, sehingga aritmetika bulan kalender, transisi Waktu Musim Panas (DST), dan penyeimbangan unit rentan terhadap error.

Temporal API menyambungkan Temporal.ZonedDateTime dengan Temporal.Duration dan Intl.DurationFormat untuk memberikan perbedaan waktu yang aman dari DST, penyeimbangan unit yang dapat dikonfigurasi, perbandingan durasi yang mendukung kalender, dan pemformatan yang dilokalkan.

Cara Menerapkan

  1. Mendapatkan endpoint Temporal.ZonedDateTime: Gunakan Temporal.Now.zonedDateTimeISO() untuk waktu saat ini di zona waktu sistem, atau teruskan string zona waktu IANA eksplisit—Temporal.Now.zonedDateTimeISO(timeZone)—saat zona waktu acara target berbeda dari default sistem pengguna.
  2. Menghitung diferensial dengan .since() dan .until():
    • Gunakan now.since(start, options) untuk menghitung waktu yang berlalu sejak peristiwa sebelumnya.
    • Gunakan now.until(end, options) untuk menghitung sisa waktu hingga peristiwa mendatang.
    • Teruskan largestUnit (misalnya, 'year', 'month', 'day', 'hour') dan smallestUnit (misalnya, 'minute') untuk menyeimbangkan dan membulatkan Temporal.Duration yang dihasilkan.
  3. Menyeimbangkan atau membandingkan objek Temporal.Duration mandiri:
    • Buat durasi mandiri dengan Temporal.Duration.from({ minutes: 90 }) dan seimbangkan kembali unit dengan duration.round({ largestUnit: 'hour' }) (berikan relativeTo saat menyeimbangkan di seluruh bulan kalender, tahun, atau transisi DST).
    • Bandingkan dua durasi dengan Temporal.Duration.compare(d1, d2, { relativeTo })—meneruskan PlainDate atau ZonedDateTime sebagai relativeTo diperlukan setiap kali salah satu durasi berisi unit kalender (years, months, weeks) atau melewati batas DST.
  4. Format untuk manusia vs. mesin:
    • Gunakan duration.toLocaleString(locale, options) (yang didelegasikan ke Intl.DurationFormat) untuk teks yang ditampilkan kepada pengguna yang dilokalkan.
    • Gunakan duration.toString() hanya untuk string durasi ISO 8601 yang dapat dibaca mesin (seperti P1M2DT4H).

Contoh: Perbedaan Masa Berlaku Uji Coba dan Pemformatan yang Dilokalkan

// 1. Get the current time in an explicit target IANA time zone (or omit the
// argument to use the system's current time zone).
const tz = 'America/New_York';
const now = Temporal.Now.zonedDateTimeISO(tz);

// 2. Combine separate date and time strings directly.
// Do not build an ISO string with template literals; chain Temporal methods instead.
const startDateStr = '2025-01-01';
const startTimeStr = '12:00:00';
const endDateStr = '2025-01-31';
const endTimeStr = '12:00:00';

const start = Temporal.PlainDate.from(startDateStr).toPlainDateTime(startTimeStr).toZonedDateTime(tz);
const end = Temporal.PlainDate.from(endDateStr).toPlainDateTime(endTimeStr).toZonedDateTime(tz);

// 3. Calculate differences using .since() and .until()
// ZonedDateTime differences default to largestUnit: 'hour' (days are not a fixed
// length across DST, so Temporal won't balance into days unless asked). Pass
// largestUnit: 'day' or a calendar unit ('week', 'month', 'year') when you want a
// human-scale breakdown instead of e.g. PT720H. smallestUnit rounds away
// seconds and sub-second noise so the formatted string stays readable.
const timeActive = now.since(start, { largestUnit: 'year', smallestUnit: 'minute' });
const timeRemaining = now.until(end, { largestUnit: 'year', smallestUnit: 'minute' });

// 4. Format durations for display with .toLocaleString() (Intl.DurationFormat)
// instead of manually concatenating unit numbers and suffixes. 'en-US' is used
// here for deterministic output; pass undefined to use the user's locale.
console.log(`Active: ${timeActive.toLocaleString('en-US', { style: 'narrow' })}`);
console.log(`Remaining: ${timeRemaining.toLocaleString('en-US', { style: 'long' })}`);

// Use .toString() for ISO 8601 serialization (e.g., API payloads or <time datetime="...">)
console.log(`ISO 8601: ${timeRemaining.toString()}`);

// 5. Check status via .sign, and compare durations with Temporal.Duration.compare()
const isExpired = timeRemaining.sign < 0;
// Pass relativeTo when comparing durations that may include calendar months/years or DST shifts
const isPastHalfway = Temporal.Duration.compare(timeActive, timeRemaining, { relativeTo: now }) > 0;

Contoh: Menyeimbangkan Durasi Mandiri (round dan Intl.DurationFormat)

// Convert a raw unit count (e.g., 90 minutes) into a balanced breakdown (1 hour, 30 minutes)
const rawDuration = Temporal.Duration.from({ minutes: 90 });
const balanced = rawDuration.round({ largestUnit: 'hour' });

// Format via duration.toLocaleString() or directly with Intl.DurationFormat
const formatter = new Intl.DurationFormat('en-US', { style: 'long' });
console.log(formatter.format(balanced)); // "1 hour, 30 minutes"

Penerapan Strategis & Praktik Terbaik

  • GUNAKAN Temporal.ZonedDateTime untuk penghitungan yang melibatkan peristiwa dunia nyata yang terjadi di zona waktu tertentu (seperti perpanjangan langganan atau hitung mundur acara), dan teruskan timeZone eksplisit ke Temporal.Now.zonedDateTimeISO(timeZone) saat menghitung terhadap zona selain default sistem.
  • LAKUKAN penggabungan string tanggal dan waktu yang terpisah dengan metode Temporal, misalnya Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz) atau Temporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr }), daripada menginterpolasi string ISO seperti `${dateStr}T${timeStr}`.
  • LAKUKAN penerusan largestUnit (misalnya, 'day', 'month', 'year') di .since(), .until(), atau .round() saat Anda ingin durasi diseimbangkan di luar jam; metode perbedaan ZonedDateTime ditetapkan secara default ke 'hour'.
  • GUNAKAN Temporal.Duration.compare(d1, d2, { relativeTo }) saat membandingkan dua durasi, dengan memberikan anchor relativeTo setiap kali salah satu durasi melibatkan unit kalender (months, years) atau hari yang memperhitungkan zona waktu.
  • LAKUKAN pemformatan durasi untuk tampilan pengguna menggunakan duration.toLocaleString() atau new Intl.DurationFormat(locale, { style }) ('long', 'short', 'narrow', atau 'digital'), bukan menginterpolasi ${duration.years}y ${duration.months}m... secara manual.
  • LAKUKAN penggunaan duration.toString() untuk string durasi ISO 8601 yang dapat dibaca mesin (misalnya, PT1H30M), jangan pernah sebagai teks yang ditampilkan kepada pengguna.
  • LAKUKAN render PlainTime sebagai HH:MM (misalnya, untuk <input type="time">) menggunakan .toPlainTime().toString({ smallestUnit: 'minute' }), bukan string .slice(0, 5).
  • JANGAN mengubah instance secara langsung; objek Temporal bersifat immutable. Operasi seperti .add(), .subtract(), .round(), atau .with() menampilkan instance baru.
  • JANGAN kembali ke aritmetika Date / .getTime() untuk selisihnya; Date tidak memiliki matematika kalender yang mendukung zona waktu dan hanya memiliki presisi milidetik.
  • LAKUKAN penggunaan properti .sign native dari Temporal.Duration (atau Temporal.ZonedDateTime.compare()) yang dihitung untuk memeriksa apakah durasi mewakili titik waktu yang telah berlalu/berakhir (sign < 0) atau waktu mulai mendatang yang tertunda.

Strategi Penggantian

Penggantian & dukungan browser untuk Temporal

Untuk browser yang belum mendukung Temporal API native, gunakan deteksi fitur dan muat temporal-polyfill secara dinamis (~15 KB min+gz).

Perhatikan bahwa mengimpor temporal-polyfill tidak otomatis menetapkan objek Temporal ke cakupan global. Tetapkan secara eksplisit jika kode Anda mengandalkan globalThis.Temporal.

// Check if Temporal is supported natively
(async () => {
  if (typeof Temporal === 'undefined') {
    // Load the polyfill conditionally
    const module = await import('https://esm.sh/temporal-polyfill');
    globalThis.Temporal = module.Temporal;
    // Extend Date.prototype if needed
    Date.prototype.toTemporalInstant = module.toTemporalInstant;
  }
  initializeApp();
})();

Intl.DurationFormat