ในอดีต การคำนวณเวลาที่ผ่านไปหรือเวลาที่เหลืออยู่ระหว่างเหตุการณ์ (เช่น การหมดอายุของช่วงทดลองใช้ การนับถอยหลังการสมัครใช้บริการ หรือกรอบเวลา SLA) และการแสดงช่วงเวลาเหล่านั้นในรูปแบบที่แปลแล้วและมนุษย์อ่านได้นั้นต้องใช้การคำนวณด้วยตนเองหรือไลบรารีภายนอก ออบเจ็กต์ Date เดิมจะติดตามเฉพาะมิลลิวินาทีของ Epoch เท่านั้น ซึ่งทำให้การคำนวณเดือนตามปฏิทิน การเปลี่ยนเวลาออมแสง (DST) และการปรับสมดุลหน่วยมีแนวโน้มที่จะเกิดข้อผิดพลาด
API Temporal จะจับคู่ Temporal.ZonedDateTime กับ Temporal.Duration และ Intl.DurationFormat เพื่อให้ส่วนต่างของเวลาที่ปลอดภัยจาก DST, การปรับสมดุลหน่วยที่กำหนดค่าได้, การเปรียบเทียบระยะเวลาที่คำนึงถึงปฏิทิน และการจัดรูปแบบที่แปลแล้ว
วิธีการนำไปใช้งาน
- รับปลายทาง
Temporal.ZonedDateTime: ใช้Temporal.Now.zonedDateTimeISO()สำหรับเวลาปัจจุบันในเขตเวลาของระบบ หรือส่งสตริงเขตเวลา IANA ที่ชัดเจน (Temporal.Now.zonedDateTimeISO(timeZone)) เมื่อเขตเวลาของกิจกรรมเป้าหมายแตกต่างจากค่าเริ่มต้นของระบบของผู้ใช้ - คำนวณดิฟเฟอเรนเชียลด้วย
.since()และ.until():- ใช้
now.since(start, options)เพื่อคำนวณเวลาที่ผ่านไปนับตั้งแต่เหตุการณ์ที่ผ่านมา - ใช้
now.until(end, options)เพื่อคำนวณเวลาที่เหลือจนกว่าจะถึงเหตุการณ์ในอนาคต - ส่ง
largestUnit(เช่น'year','month','day','hour') และsmallestUnit(เช่น'minute') เพื่อปรับสมดุลและปัดเศษTemporal.Durationที่ได้
- ใช้
- ปรับสมดุลหรือเปรียบเทียบออบเจ็กต์
Temporal.Durationแบบสแตนด์อโลน:- สร้างระยะเวลาแบบสแตนด์อโลนด้วย
Temporal.Duration.from({ minutes: 90 })และปรับสมดุลหน่วยด้วยduration.round({ largestUnit: 'hour' })(ระบุrelativeToเมื่อปรับสมดุลตามเดือน ปี หรือการเปลี่ยนเวลา DST ในปฏิทิน) - เปรียบเทียบระยะเวลา 2 ช่วงด้วย
Temporal.Duration.compare(d1, d2, { relativeTo })โดยต้องส่งPlainDateหรือZonedDateTimeเป็นrelativeToเมื่อระยะเวลาใดระยะเวลาหนึ่งมีหน่วยปฏิทิน (years,months,weeks) หรือข้ามขอบเขต DST
- สร้างระยะเวลาแบบสแตนด์อโลนด้วย
- จัดรูปแบบสำหรับมนุษย์เทียบกับเครื่องจักร
- ใช้
duration.toLocaleString(locale, options)(ซึ่งมอบสิทธิ์ให้Intl.DurationFormat) สำหรับข้อความที่แปลแล้วซึ่งแสดงต่อผู้ใช้ - ใช้
duration.toString()สำหรับสตริงระยะเวลา ISO 8601 ที่เครื่องอ่านได้เท่านั้น (เช่นP1M2DT4H)
- ใช้
ตัวอย่าง: ความแตกต่างของวันหมดอายุของช่วงทดลองใช้และการจัดรูปแบบที่แปลแล้ว
// 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;
ตัวอย่าง: การปรับสมดุลระยะเวลาแบบสแตนด์อโลน (round และ 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"
การนำไปใช้เชิงกลยุทธ์และแนวทางปฏิบัติแนะนำ
- ควรใช้
Temporal.ZonedDateTimeสำหรับการคำนวณที่เกี่ยวข้องกับเหตุการณ์ในชีวิตจริงที่เกิดขึ้นในเขตเวลาที่เฉพาะเจาะจง (เช่น การต่ออายุการสมัครใช้บริการหรือการนับถอยหลังของกิจกรรม) และส่งtimeZoneที่ชัดเจนไปยังTemporal.Now.zonedDateTimeISO(timeZone)เมื่อคำนวณกับเขตเวลาอื่นที่ไม่ใช่ค่าเริ่มต้นของระบบ - ควรรวมสตริงวันที่และเวลาที่แยกกันด้วยเมธอด Temporal เช่น
Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz)หรือTemporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr })แทนการแทรกสตริง ISO เช่น`${dateStr}T${timeStr}` - ต้องส่ง
largestUnit(เช่น'day','month','year') ใน.since(),.until()หรือ.round()เมื่อต้องการปรับสมดุลระยะเวลาที่เกินชั่วโมง โดยเมธอดความแตกต่างZonedDateTimeจะมีค่าเริ่มต้นเป็น'hour' - ควรใช้
Temporal.Duration.compare(d1, d2, { relativeTo })เมื่อเปรียบเทียบระยะเวลา 2 ช่วง โดยระบุจุดยึดrelativeToเมื่อระยะเวลาใดระยะเวลาหนึ่งเกี่ยวข้องกับหน่วยปฏิทิน (months,years) หรือวันที่อิงตามเขตเวลา - ควรจัดรูปแบบระยะเวลาสำหรับการแสดงผลของผู้ใช้โดยใช้
duration.toLocaleString()หรือnew Intl.DurationFormat(locale, { style })('long','short','narrow'หรือ'digital') แทนการประมาณค่าในช่วง${duration.years}y ${duration.months}m...ด้วยตนเอง - ควรใช้
duration.toString()สำหรับสตริงระยะเวลา ISO 8601 ที่เครื่องอ่านได้ (เช่นPT1H30M) และไม่ควรใช้เป็นข้อความที่แสดงต่อผู้ใช้ - ควรแสดงผล
PlainTimeเป็นHH:MM(เช่น สำหรับ<input type="time">) โดยใช้.toPlainTime().toString({ smallestUnit: 'minute' })แทนสตริง.slice(0, 5) - อย่าแก้ไขอินสแตนซ์โดยตรง ออบเจ็กต์
Temporalเปลี่ยนแปลงไม่ได้ การดำเนินการอย่าง.add(),.subtract(),.round()หรือ.with()จะแสดงผลอินสแตนซ์ใหม่ - อย่าใช้การคำนวณ
Date/.getTime()สำหรับส่วนต่างDateไม่มีคณิตศาสตร์ปฏิทินที่คำนึงถึงเขตเวลาและมีความแม่นยำระดับมิลลิวินาทีเท่านั้น - ควรใช้พร็อพเพอร์ตี้
.signแบบเนทีฟของTemporal.Duration(หรือTemporal.ZonedDateTime.compare()) ที่คำนวณแล้วเพื่อตรวจสอบว่าระยะเวลาแสดงถึงจุดเวลาที่ผ่านมา/หมดอายุ (sign < 0) หรือการเริ่มต้นที่รอดำเนินการในอนาคต
กลยุทธ์สำรอง
เบราว์เซอร์ที่รองรับและทางเลือกสำรองสำหรับ Temporal
สำหรับเบราว์เซอร์ที่ยังไม่รองรับ Temporal API แบบเนทีฟ ให้ใช้การตรวจหาฟีเจอร์และโหลด temporal-polyfill แบบไดนามิก (~15 KB min+gz)
โปรดทราบว่าการนำเข้า temporal-polyfill จะไม่กำหนดออบเจ็กต์ Temporal ให้กับขอบเขตส่วนกลางโดยอัตโนมัติ กำหนดอย่างชัดเจนหากโค้ดของคุณใช้ 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();
})();