ضبط داده‌های مستقل از مکان و تاریخ‌های جزئی با Temporal

ویژگی‌های وب: Temporal

ثبت داده‌های زمانی که باید بدون توجه به مکان بیننده یکسان باقی بمانند (مانند تاریخ تولد، زنگ‌های روزانه، تاریخ انقضای کارت اعتباری، یا چرخه‌های صورت‌حساب ماهانه) در گذشته با شیء قدیمی 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)، دوره‌های صورت‌حساب ماهانه

نحوه پیاده‌سازی

  1. محدودترین نوع Temporal.Plain* را که با داده‌هایتان مطابقت دارد انتخاب کنید:
    • از Temporal.PlainDate.from() برای تاریخ‌های کامل تقویم و از Temporal.PlainTime.from() برای زمان‌های ساعت دیواری استفاده کنید.
    • هنگام ترکیب تاریخ تقویم و زمان ساعت دیواری بدون اتصال به منطقه زمانی، از Temporal.PlainDateTime.from() (یا plainDate.toPlainDateTime(plainTime)) استفاده کنید.
    • از Temporal.PlainMonthDay.from() برای تاریخ‌های سالانه بدون سال استفاده کنید و قبل‌از انجام محاسبات تاریخ، آن را به PlainDate با .toPlainDate({ year }) تبدیل کنید.
    • از Temporal.PlainYearMonth.from() برای مقادیر سال و ماه استفاده کنید تا اعداد روز دلخواه هرگز به محاسبات نفوذ نکنند.
  2. هنگام ساختن انواع Plain، به پارامتر calendar توجه کنید: ISO 8601 ('iso8601') تقویم پیش‌فرض هنگام تجزیه رشته‌های ISO یا ساختن با PlainDate.from({ year, month, day }) است. هنگام ساختن PlainMonthDay یا PlainYearMonth از کیسه‌های ویژگی—یا هنگام کار با تقویم‌های غیر ISO—calendar را به‌طور صریح ارسال کنید (مثلاً { month: 10, day: 31, calendar: 'iso8601' }).
  3. از .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 a Temporal.PlainDate first 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();
})();