التقاط البيانات غير المرتبطة بالموقع الجغرافي والتواريخ الجزئية باستخدام Temporal

ميزات الويب: Temporal

إنّ تسجيل البيانات الزمنية التي يجب أن تظل متطابقة بغض النظر عن الموقع الجغرافي للمشاهد، مثل تواريخ الميلاد أو المنبّهات اليومية أو تواريخ انتهاء صلاحية بطاقات الائتمان أو دورات الفوترة الشهرية، كان في السابق عرضة للأخطاء مع عنصر Date القديم. بما أنّ Date يمثّل دائمًا لحظة معيّنة في الوقت ويتم عرضه باستخدام المنطقة الزمنية المحلية للمشاهد، فإنّ حفظ تاريخ مثل "1990-01-01" يمكن أن يؤدي إلى ظهور "1989-12-31" للمستخدمين في غرب التوقيت العالمي المنسق، في حين أنّ وضع نماذج للتواريخ الجزئية (مثل انتهاء صلاحية البطاقة في شهر معيّن فقط) يفرض أيامًا عشوائية كعناصر نائبة تتسبب في أخطاء تجاوز السعة.

توفّر واجهة برمجة التطبيقات Temporal مجموعة من الأنواع "العادية" غير المرتبطة بالموقع الجغرافي والتي لا تتضمّن أي فرق في المنطقة الزمنية وتمثّل تواريخ التقويم وأوقات الساعة تمامًا كما تتم قراءتها من التقويم أو الساعة:

النوع ما يمثّله النموذج حالات الاستخدام النموذجية
Temporal.PlainDate تاريخ التقويم الكامل (year، month، day) تواريخ الميلاد وتواريخ الأحداث وتواريخ فوترة الاشتراكات
Temporal.PlainTime الوقت الفعلي (hour، minute، second، ...) منبّهات يومية (عند الساعة 9:00 صباحًا حسب المنطقة الزمنية للمستخدم)، وساعات عمل المتجر
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. يُرجى الانتباه إلى المَعلمة calendar عند إنشاء أنواع Plain: 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' } تلقائيًا، ما يثبّت تواريخ نهاية الشهر مثل 31 يناير إلى 28/29 فبراير. يجب تمرير { overflow: 'reject' } عندما تؤدي التواريخ المستهدَفة غير الصالحة إلى ظهور RangeError بدلاً من التثبيت.

مثال: التواريخ الكاملة وأوقات الساعة الحائطية و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، يمكنك التحويل صراحةً من خلال Temporal.Instant بدلاً من إنشاء سلاسل ISO يدويًا:

// 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 لتواريخ انتهاء الصلاحية أو مدة الفوترة حسب السنة والشهر.
  • لا تُجرِ عمليات حسابية مباشرةً على Temporal.PlainMonthDay، بل حوِّله إلى Temporal.PlainDate أولاً من خلال .toPlainDate({ year }) لأنّ عدد أيام الشهر يختلف حسب السنة.
  • يجب استخدام .add() في Temporal.PlainDate للفواصل الزمنية المتكررة في التقويم، بالاعتماد على overflow: 'constrain' (الإعداد التلقائي) لتثبيت تواريخ نهاية الشهر أو overflow: 'reject' لإظهار RangeError في التواريخ غير الموجودة.
  • يجب تحديد calendar (مثل calendar: 'iso8601') عند إنشاء PlainMonthDay أو أنواع عادية غير متوافقة مع معيار ISO من العناصر.
  • لا تستخدِم أنواع Plain إذا كنت بحاجة إلى تمثيل لحظة معيّنة في الوقت الفعلي (لحظة). استخدِم 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 مباشرةً من نوع Plain، ومقارنتها بـ .equals().
  • يجب تحديد المناطق الزمنية حسب اسم IANA (مثل America/New_York) وليس حسب فرق ثابت عن التوقيت العالمي المنسق (UTC). تتغيّر الفروق الزمنية مع نظام التوقيت الصيفي والتشريعات، لذا تصبح التصنيفات مثل "UTC-5" غير صحيحة لجزء من السنة.
  • يجب تذكُّر أنّ عناصر Temporal غير قابلة للتغيير. تُعرِض الطرق مثل .add() أو .subtract() أو .with() دائمًا مثيلاً جديدًا بمرجع جديد بدلاً من تغيير الكائن الأصلي في مكانه، ما يجعل تخزينها آمنًا مباشرةً في حالة واجهة المستخدم التفاعلية.

استراتيجية الإجراءات الاحتياطية

الخيارات الاحتياطية وتوافق المتصفّحات مع 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();
})();