איסוף נתונים שלא תלויים במיקום ותאריכים חלקיים באמצעות Temporal

תכונות באינטרנט: Temporal

בעבר, תיעוד נתונים כרונולוגיים שאמורים להישאר זהים בלי קשר למיקום של הצופה – כמו תאריכי לידה, התראות יומיות, תאריכי תפוגה של כרטיסי אשראי או מחזורי חיוב חודשיים – היה מועד לשגיאות באובייקט Date הקודם. מכיוון ש-Date תמיד מייצג רגע ספציפי בזמן ומוצג באמצעות אזור הזמן המקומי של הצופה, שמירת תאריך כמו "1990-01-01" יכולה לגרום למשתמשים ממערב ל-UTC לראות את "1989-12-31", בעוד שמידול תאריכים חלקיים (כמו תפוגת כרטיס בחודש בלבד) מאלץ ימי פלייסהולדר שרירותיים שגורמים לבאגים של גלישה.

‫API של 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 מ-property bags – או כשעובדים עם לוחות שנה שהם לא 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 מאלץ אתכם לבחור רגע כלשהו (בדרך כלל חצות לפי שעון UTC), וקריאה שלו בחזרה באזור זמן ממערב ל-UTC מניבה את "31/12/1989". אם ספרייה או API עדיין דורשים 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 או סוגי 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 ישירות מסוג Plain, ומשווים ל-.equals().
  • מומלץ לזהות אזורי זמן לפי שם IANA (למשל, America/New_York) ולא לפי הפרש קבוע מ-UTC. ההפרשים משתנים בהתאם לשעון הקיץ ולחקיקה, ולכן תוויות כמו "UTC-5" הופכות לשגויות בחלק מהשנה.
  • חשוב לזכור שהאובייקטים של Temporal הם לא ניתנים לשינוי. שיטות כמו .add(),‏ .subtract() או .with() תמיד מחזירות מופע חדש עם קובץ עזר חדש במקום לשנות את האובייקט המקורי, כך שאפשר לאחסן אותן בבטחה ישירות במצב ממשק משתמש ריאקטיבי.

אסטרטגיית גיבוי

תמיכה ב-Temporal בדפדפנים ופתרונות חלופיים

בדפדפנים שעדיין לא תומכים ב-API המקורי של Temporal, צריך להשתמש בזיהוי תכונות ולטעון באופן דינמי את temporal-polyfill (‎~15 KB 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();
})();