محاسبه تفاضل رویدادها و قالب‌بندی مدت‌ها با Temporal

ویژگی‌های وب: Temporal Intl.DurationFormat

محاسبه زمان سپری‌شده یا باقی‌مانده بین رویدادها (مانند انقضای دوره آزمایشی، شمارش معکوس اشتراک، یا پنجره‌های SLA) و ارائه این بازه‌ها در قالب بومی‌سازی‌شده و قابل‌خواندن برای انسان، از گذشته نیازمند محاسبات دستی یا کتابخانه‌های خارجی بوده است. شیء قدیمی Date فقط میلی‌ثانیه‌های مبدأ زمانی را ردیابی می‌کند، که باعث می‌شود محاسبات ماه تقویمی، انتقال‌های ساعت تابستانی (DST)، و توازن واحدها مستعد خطا باشند.

Temporal API Temporal.ZonedDateTime با Temporal.Duration و Intl.DurationFormat جفت می‌شود تا تفاوت‌های زمانی ایمن از ساعت تابستانی، توازن واحد قابل‌پیکربندی، مقایسه مدت‌زمان با آگاهی از تقویم، و قالب‌بندی بومی‌سازی‌شده را ارائه دهد.

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

  1. دریافت نقاط پایانی Temporal.ZonedDateTime: از Temporal.Now.zonedDateTimeISO() برای زمان فعلی در منطقه زمانی سیستم استفاده کنید، یا رشته منطقه زمانی صریح IANA (‏Temporal.Now.zonedDateTimeISO(timeZone)) را زمانی که منطقه زمانی رویداد هدف با پیش‌فرض سیستم کاربر متفاوت است ارسال کنید.
  2. محاسبه دیفرانسیل با .since() و .until():
    • از now.since(start, options) برای محاسبه زمان سپری‌شده از رویدادی در گذشته استفاده کنید.
    • از now.until(end, options) برای محاسبه زمان باقی‌مانده تا رویدادی در آینده استفاده کنید.
    • ‫largestUnit (مثلاً 'year'، 'month'، 'day'، 'hour') و smallestUnit (مثلاً 'minute') را برای تراز کردن و گرد کردن Temporal.Duration حاصل ارسال کنید.
  3. تراز کردن یا مقایسه کردن اشیاء Temporal.Duration مستقل:
    • مدت‌های مستقل را با Temporal.Duration.from({ minutes: 90 }) ایجاد کنید و واحدها را با duration.round({ largestUnit: 'hour' }) تراز کنید (relativeTo را هنگام تراز کردن در ماه‌ها، سال‌های تقویم، یا انتقال‌های ساعت تابستانی ارائه دهید).
    • دو مدت‌زمان را با Temporal.Duration.compare(d1, d2, { relativeTo }) مقایسه کنید—هرگاه یکی از مدت‌زمان‌ها حاوی واحدهای تقویم (years،‏ months،‏ weeks) باشد یا از مرزهای ساعت تابستانی عبور کند، ارسال PlainDate یا ZonedDateTime به‌عنوان relativeTo الزامی است.
  4. قالب‌بندی برای انسان‌ها درمقابل ماشین‌ها:
    • از 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; Date has 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();
})();

Intl.DurationFormat