ثبت دادههای زمانی که باید بدون توجه به مکان بیننده یکسان باقی بمانند (مانند تاریخ تولد، زنگهای روزانه، تاریخ انقضای کارت اعتباری، یا چرخههای صورتحساب ماهانه) در گذشته با شیء قدیمی Date مستعد خطا بوده است. چون Date همیشه نشاندهنده لحظه خاصی در زمان است و بااستفاده از منطقه زمانی محلی بیننده نمایش داده میشود، ذخیره کردن تاریخی مثل "1990-01-01" میتواند باعث شود کاربران غرب UTC تاریخ "1989-12-31" را ببینند، درحالیکه مدلسازی تاریخهای جزئی (مثل انقضای کارت که فقط ماه را نشان میدهد) روزهای جایبان دلخواهی را تحمیل میکند که باعث ایجاد اشکالهای سرریز میشود.
Temporal API خانوادهای از انواع «ساده» مستقل از مکان را ارائه میدهد که هیچ انحراف منطقه زمانیای ندارند و تاریخهای تقویم و زمانهای ساعت دیواری را دقیقاً همانطور که از تقویم یا ساعت خوانده میشوند نشان میدهند:
| نوع | چه چیزی را مدلسازی میکند | موارد استفاده معمول |
|---|---|---|
Temporal.PlainDate |
تاریخ کامل تقویم (year، month، day) |
تاریخ تولد، تاریخ رویداد، تاریخ صورتحساب اشتراک |
Temporal.PlainTime |
زمان ساعت دیواری (hour، minute، second، ...) |
هشدارهای روزانه (ساعت ۹:۰۰ صبح در هر جایی که کاربر باشد)، ساعت کاری فروشگاه |
Temporal.PlainDateTime |
تاریخ تقویم + زمان ساعت دیواری بدون منطقه زمانی | زمانبندیهای محلی شناور، ترکیب PlainDate و PlainTime قبلاز پیوست کردن منطقه |
Temporal.PlainMonthDay |
ماه و روز بدون سال | تولدها، سالگردها، و تعطیلات ثابت سالانه تکرارشونده |
Temporal.PlainYearMonth |
سال و ماه بدون روز | انقضای کارت اعتباری (2027-12)، دورههای صورتحساب ماهانه |
نحوه پیادهسازی
- محدودترین نوع
Temporal.Plain*را که با دادههایتان مطابقت دارد انتخاب کنید:- از
Temporal.PlainDate.from()برای تاریخهای کامل تقویم و ازTemporal.PlainTime.from()برای زمانهای ساعت دیواری استفاده کنید. - هنگام ترکیب تاریخ تقویم و زمان ساعت دیواری بدون اتصال به منطقه زمانی، از
Temporal.PlainDateTime.from()(یاplainDate.toPlainDateTime(plainTime)) استفاده کنید. - از
Temporal.PlainMonthDay.from()برای تاریخهای سالانه بدون سال استفاده کنید و قبلاز انجام محاسبات تاریخ، آن را بهPlainDateبا.toPlainDate({ year })تبدیل کنید. - از
Temporal.PlainYearMonth.from()برای مقادیر سال و ماه استفاده کنید تا اعداد روز دلخواه هرگز به محاسبات نفوذ نکنند.
- از
- هنگام ساختن انواع Plain، به پارامتر
calendarتوجه کنید: ISO 8601 ('iso8601') تقویم پیشفرض هنگام تجزیه رشتههای ISO یا ساختن باPlainDate.from({ year, month, day })است. هنگام ساختنPlainMonthDayیاPlainYearMonthاز کیسههای ویژگی—یا هنگام کار با تقویمهای غیر ISO—calendarرا بهطور صریح ارسال کنید (مثلاً{ month: 10, day: 31, calendar: 'iso8601' }). - از
.add()با گزینههایoverflowبرای فواصل تکرارشونده تقویم استفاده کنید: فراخوانیplainDate.add({ months: 1 })بهطور پیشفرض به{ overflow: 'constrain' }میرود و تاریخهای پایان ماه مانند ۳۱ ژانویه را به ۲۸/۲۹ فوریه محدود میکند. وقتی تاریخهای هدف نامعتبر باید بهجای محدود شدن،RangeErrorرا برگردانند،{ overflow: 'reject' }را ارسال کنید.
مثال: تاریخ کامل، زمان ساعت دیواری، و PlainDateTime
// 1. Parse a calendar date and a wall-clock time from input strings
const birthdate = Temporal.PlainDate.from('1990-01-01');
const reminderTime = Temporal.PlainTime.from('09:00');
// Combine into a location-agnostic PlainDateTime without string interpolation
const localAppointment = birthdate.toPlainDateTime(reminderTime);
// 2. Display without time zone conversion; outputs the same date in any time zone
console.log(birthdate.toLocaleString('en-GB'));
console.log(localAppointment.toString()); // "1990-01-01T09:00:00"
// 3. Read calendar fields directly from the Plain type. A PlainDate already *is*
// a calendar date, so there is no need for the formatToParts() workaround that
// legacy Date code uses to recover year/month/day from an instant.
const { year, month, day } = birthdate;
مثال: تاریخهای ناقص (PlainMonthDay و PlainYearMonth)
// Annual recurring event (no year component)
// Include explicit calendar when constructing from an object
const annualHoliday = Temporal.PlainMonthDay.from({
month: 10,
day: 31,
calendar: 'iso8601',
});
const today = Temporal.Now.plainDateISO();
const isToday = annualHoliday.equals(today.toPlainMonthDay());
// Convert PlainMonthDay to a PlainDate with a specific year before doing arithmetic,
// because month lengths (such as February) depend on whether the year is a leap year.
const occurrenceThisYear = annualHoliday.toPlainDate({ year: today.year });
// Monthly expiration (no day component, preventing day-of-month overflow bugs)
const cardExpiry = Temporal.PlainYearMonth.from({
year: 2027,
month: 12,
calendar: 'iso8601',
});
const currentMonth = today.toPlainYearMonth();
const isExpired = Temporal.PlainYearMonth.compare(cardExpiry, currentMonth) < 0;
مثال: فواصل ماهانه تکرارشونده (overflow: 'constrain' درمقابل 'reject')
const billingStart = Temporal.PlainDate.from('2024-01-31');
// Default overflow: 'constrain' clamps Jan 31 + 1 month to the last valid day of February
const nextBilling = billingStart.add({ months: 1 }, { overflow: 'constrain' });
console.log(nextBilling.toString()); // "2024-02-29" (2024 is a leap year)
// Use overflow: 'reject' to throw a RangeError when the target day does not exist
try {
billingStart.add({ months: 1 }, { overflow: 'reject' });
} catch (err) {
// RangeError: day 31 does not exist in February
}
مثال: تعاملپذیری با Date قدیمی
Date قدیمی مُهر زمان (یک لحظه) است، نه تاریخ تقویم. ذخیره کردن "1990-01-01" بهعنوان Date شما را مجبور میکند یک لحظه (معمولاً نیمهشب بهوقت ساعت هماهنگ جهانی) را انتخاب کنید و خواندن آن در منطقه زمانی غرب ساعت هماهنگ جهانی "31/12/1989" را نشان میدهد. اگر کتابخانه یا میانای برنامهسازی کاربردی همچنان به Date نیاز دارد، بهجای ساختن رشتههای ISO بهصورت دستی، صریحاً ازطریق Temporal.Instant تبدیل کنید:
// PlainDate -> Date: attach a time zone (time of day defaults to the start of
// that day) to get an Instant, then hand the epoch milliseconds to Date.
// 'UTC' is a deliberate choice and must be documented; any zone would do,
// but the stored instant depends on it.
const instant = birthdate.toZonedDateTime('UTC').toInstant();
const legacyDate = new Date(instant.epochMilliseconds);
// Date -> PlainDate: go back through Instant (legacyDate.toTemporalInstant()
// is equivalent), then project into an explicit time zone. The result depends
// on the zone, which is exactly the drift risk.
const viewedDate = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime())
.toZonedDateTimeISO('America/New_York')
.toPlainDate();
// Compare Plain types with .equals() (or Temporal.PlainDate.compare), not strings.
console.log(viewedDate.equals(birthdate)); // false: the calendar date drifted to 1989-12-31
پیادهسازی استراتژیک و روالهای مطلوب
- باید از
Temporal.PlainDateبرای تاریخهای کامل تقویم، ازTemporal.PlainTimeبرای زمانهای ساعت دیواری، ازTemporal.PlainDateTimeبرای جفتهای تاریخ-زمان محلی بدون منطقه، ازTemporal.PlainMonthDayبرای تاریخهای سالانه تکرارشونده، و ازTemporal.PlainYearMonthبرای تاریخهای انقضای سال-ماه یا دورههای صورتحساب استفاده کنید. - DO NOT perform arithmetic directly on
Temporal.PlainMonthDay; convert it to aTemporal.PlainDatefirst via.toPlainDate({ year })because month lengths vary by year. - باید از
.add()درTemporal.PlainDateبرای فاصلههای تکرارشونده تقویم استفاده کنید، با تکیه برoverflow: 'constrain'(پیشفرض) برای محدود کردن تاریخهای پایان ماه یاoverflow: 'reject'برای ایجادRangeErrorدر تاریخهای ناموجود. - باید
calendar(مثلاًcalendar: 'iso8601') را هنگام ساختنPlainMonthDayیا انواع Plain غیر ISO از اشیاء مشخص کنید. - اگر لازم است لحظه خاصی را در زمان فیزیکی (یک آن) نشان دهید، از انواع «ساده» استفاده نکنید. از
Temporal.InstantیاTemporal.ZonedDateTimeبرای گزارشها، مُهرهای زمان رویداد، یا زمانبندی بین منطقههای زمانی استفاده کنید. - اشیاء
Dateرا از رشتههای ISO دستساز (مانند`${date}T00:00:00Z`) نسازید. یکDateرا ازepochMillisecondsیکTemporal.InstantیاTemporal.ZonedDateTimeتولید کنید و یکی را باTemporal.Instant.fromEpochMilliseconds(date.getTime())یاdate.toTemporalInstant()بخوانید تا فرض منطقه زمانی صریح باشد. - از
Intl.DateTimeFormat.prototype.formatToParts()برای استخراج یا مقایسه مقادیر سال، ماه، یا روز استفاده نکنید..year،.month، و.dayرا مستقیماً از نوع «ساده» بخوانید و با.equals()مقایسه کنید. - باید مناطق زمانی را با نام IANA (مثلاً
America/New_York) شناسایی کنید و هرگز از اختلاف ثابت UTC استفاده نکنید. تفاوتهای زمانی با ساعت تابستانی و قوانین تغییر میکنند، بنابراین برچسبهایی مانند"UTC-5"برای بخشی از سال اشتباه میشوند. - بهیاد داشته باشید که اشیاء
Temporalتغییرناپذیر هستند. روشهایی مانند.add()،.subtract()، یا.with()همیشه یک نمونه جدید با یک مرجع جدید برمیگردانند، بهجای اینکه شیء اصلی را در محل تغییر دهند، که باعث میشود ذخیره مستقیم آنها در حالت واسط کاربر واکنشی ایمن باشد.
راهبرد جایگزین
پشتیبانی مرورگر و بازگشت به حالت قبلی برای 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();
})();