حساب الفروق بين الأحداث وتنسيق المدد باستخدام Temporal

ميزات الويب: Temporal Intl.DurationFormat

في السابق، كان احتساب الوقت المنقضي أو المتبقي بين الأحداث (مثل انتهاء صلاحية الفترة التجريبية أو العد التنازلي للاشتراك أو نوافذ اتفاقية مستوى الخدمة) وعرض هذه الفترات بتنسيق مترجَم يمكن لشخص عادي قراءته يتطلّب إجراء عمليات حسابية يدوية أو استخدام مكتبات خارجية. يتتبّع عنصر Date القديم فقط الملّي ثانية من بداية العصر، ما يجعل العمليات الحسابية لأشهر التقويم والانتقال إلى نظام التوقيت الصيفي (DST) وموازنة الوحدات عرضة للأخطاء.

تجمع واجهة برمجة التطبيقات Temporal بين 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 })، مع تمرير PlainDate أو ZonedDateTime كـ relativeTo، وذلك عندما تحتوي أيّ من المدّتين على وحدات تقويم (years أو months أو weeks) أو تتجاوز حدود التوقيت الصيفي.
  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 }) عند مقارنة مدّتين، مع توفير نقطة ارتساء 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 الأصلية، استخدِم ميزة الكشف عن الميزات وحمِّل temporal-polyfill بشكل ديناميكي (بحجم 15 كيلوبايت تقريبًا 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