محاسبه زمان سپریشده یا باقیمانده بین رویدادها (مانند انقضای دوره آزمایشی، شمارش معکوس اشتراک، یا پنجرههای SLA) و ارائه این بازهها در قالب بومیسازیشده و قابلخواندن برای انسان، از گذشته نیازمند محاسبات دستی یا کتابخانههای خارجی بوده است. شیء قدیمی Date فقط میلیثانیههای مبدأ زمانی را ردیابی میکند، که باعث میشود محاسبات ماه تقویمی، انتقالهای ساعت تابستانی (DST)، و توازن واحدها مستعد خطا باشند.
Temporal API Temporal.ZonedDateTime با Temporal.Duration و Intl.DurationFormat جفت میشود تا تفاوتهای زمانی ایمن از ساعت تابستانی، توازن واحد قابلپیکربندی، مقایسه مدتزمان با آگاهی از تقویم، و قالببندی بومیسازیشده را ارائه دهد.
نحوه پیادهسازی
- دریافت نقاط پایانی
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را هنگام تراز کردن در ماهها، سالهای تقویم، یا انتقالهای ساعت تابستانی ارائه دهید). - دو مدتزمان را با
Temporal.Duration.compare(d1, d2, { relativeTo })مقایسه کنید—هرگاه یکی از مدتزمانها حاوی واحدهای تقویم (years،months،weeks) باشد یا از مرزهای ساعت تابستانی عبور کند، ارسالPlainDateیاZonedDateTimeبهعنوانrelativeToالزامی است.
- مدتهای مستقل را با
- قالببندی برای انسانها درمقابل ماشینها:
- از
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 })هنگام مقایسه دو مدت استفاده کنید و هرگاه یکی از مدتها شامل واحدهای تقویم (months،years) یا روزهای آگاه از منطقه زمانی باشد، یک لنگرrelativeToارائه دهید. - باید مدتها را برای نمایش به کاربر بااستفاده از
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()نمونه جدیدی را برمیگردانند. - DO NOT fall back to
Date/.getTime()arithmetic for the differential itself;Datehas no time-zone-aware calendar math and only millisecond precision. - باید از ویژگی بومی
.signمربوط بهTemporal.Duration(یاTemporal.ZonedDateTime.compare()) محاسبهشده استفاده کنید تا بررسی کنید آیا مدت نشاندهنده نقطه زمانی گذشته/منقضیشده (sign < 0) است یا شروع معلق در آینده.
راهبرد جایگزین
پشتیبانی مرورگر و بازگشت به حالت قبلی برای Temporal
برای مرورگرهایی که هنوز از Temporal API بومی پشتیبانی نمیکنند، از تشخیص ویژگی استفاده کنید و temporal-polyfill (~۱۵ کیلوبایت 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();
})();