Przechwytywanie danych niezależnych od lokalizacji i częściowych dat za pomocą typu Temporal

Funkcje internetowe: Temporal

Zapisywanie danych chronologicznych, które powinny pozostać takie same niezależnie od lokalizacji użytkownika – takich jak daty urodzenia, codzienne alarmy, daty ważności kart kredytowych czy miesięczne cykle rozliczeniowe – było w przypadku starszego obiektu Date podatne na błędy. Date zawsze reprezentuje konkretny moment w czasie i jest wyświetlany w lokalnej strefie czasowej użytkownika, więc zapisanie daty takiej jak "1990-01-01" może spowodować, że użytkownicy na zachód od UTC zobaczą "1989-12-31", a modelowanie dat częściowych (np. daty ważności karty zawierającej tylko miesiąc) wymusza stosowanie dowolnych dni zastępczych, co powoduje błędy przepełnienia.

Interfejs API Temporal udostępnia rodzinę niezależnych od lokalizacji typów „Plain”, które nie zawierają przesunięcia strefy czasowej i reprezentują daty kalendarzowe oraz godziny zegarowe dokładnie tak, jak są odczytywane z kalendarza lub zegara:

Typ Co modeluje Typowe przypadki użycia
Temporal.PlainDate Pełna data kalendarzowa (year, month, day) daty urodzenia, daty wydarzeń, daty rozliczenia subskrypcji;
Temporal.PlainTime Czas zegarowy (hour, minute, second, …) Codzienne alarmy (9:00 czasu lokalnego), godziny otwarcia sklepu
Temporal.PlainDateTime Data kalendarzowa + godzina bez strefy czasowej Pływające harmonogramy lokalne, łączące PlainDate i PlainTime przed dołączeniem strefy
Temporal.PlainMonthDay Miesiąc i dzień bez roku powtarzające się co roku urodziny, rocznice i stałe święta;
Temporal.PlainYearMonth Rok i miesiąc bez dnia Wygaśnięcie karty kredytowej (2027-12), miesięczne okresy rozliczeniowe

Jak to zrobić?

  1. Wybierz najbardziej szczegółowy typ Temporal.Plain*, który pasuje do Twoich danych:
    • Używaj Temporal.PlainDate.from() w przypadku pełnych dat kalendarzowych i Temporal.PlainTime.from() w przypadku godzin.
    • Używaj Temporal.PlainDateTime.from() (lub plainDate.toPlainDateTime(plainTime)) do łączenia daty kalendarzowej i godziny bez powiązania ze strefą czasową.
    • Użyj Temporal.PlainMonthDay.from() w przypadku dat rocznych bez roku i przekonwertuj na PlainDate z .toPlainDate({ year }) przed wykonaniem obliczeń daty.
    • W przypadku wartości roku i miesiąca używaj Temporal.PlainYearMonth.from(), aby do obliczeń nigdy nie były używane dowolne numery dni.
  2. Podczas tworzenia typów Plain zwróć uwagę na parametr calendar: ISO 8601 ('iso8601') to domyślny kalendarz podczas analizowania ciągów znaków ISO lub tworzenia za pomocą PlainDate.from({ year, month, day }). Podczas tworzenia obiektów PlainMonthDay lub PlainYearMonth z worków właściwości lub podczas pracy z kalendarzami innymi niż ISO przekazuj jawnie calendar (np. { month: 10, day: 31, calendar: 'iso8601' }).
  3. Używaj .add() z opcjami overflow w przypadku cyklicznych interwałów kalendarzowych: wywołanie plainDate.add({ months: 1 }) domyślnie używa { overflow: 'constrain' }, co powoduje, że daty końca miesiąca, takie jak 31 stycznia, są ograniczane do 28 lub 29 lutego. Przekaż { overflow: 'reject' }, gdy nieprawidłowe daty docelowe powinny powodować błąd RangeError zamiast ograniczenia.

Przykład: pełne daty, godziny i 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;

Przykład: daty częściowe (PlainMonthDay i 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;

Przykład: cykliczne interwały miesięczne (overflow: 'constrain' i '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
}

Przykład: współdziałanie ze starszą wersją Date

Starsza wersja Date to sygnatura czasowa (moment), a nie data kalendarzowa. Przechowywanie wartości "1990-01-01" jako Date wymusza wybranie jakiegoś momentu (zwykle północy czasu UTC), a odczytanie jej w strefie czasowej na zachód od UTC daje wynik "31/12/1989". Jeśli biblioteka lub interfejs API nadal wymagają typu Date, przeprowadź konwersję bezpośrednio za pomocą Temporal.Instant, zamiast ręcznie tworzyć ciągi znaków 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

Strategiczne wdrażanie i sprawdzone metody

  • UŻYWAJ Temporal.PlainDate w przypadku pełnych dat kalendarzowych, Temporal.PlainTime w przypadku godzin, Temporal.PlainDateTime w przypadku par data-godzina bez strefy, Temporal.PlainMonthDay w przypadku cyklicznych dat rocznych i Temporal.PlainYearMonth w przypadku dat ważności lub okresów rozliczeniowych w formacie rok-miesiąc.
  • NIE wykonuj działań arytmetycznych bezpośrednio na typie Temporal.PlainMonthDay. Najpierw przekonwertuj go na typ Temporal.PlainDate za pomocą funkcji .toPlainDate({ year }), ponieważ długość miesięcy różni się w zależności od roku.
  • UŻYWAJ .add() w Temporal.PlainDate w przypadku cyklicznych przedziałów kalendarzowych, polegając na overflow: 'constrain' (domyślnie) do ograniczania dat na koniec miesiąca lub overflow: 'reject' do zgłaszania błędu RangeError w przypadku nieistniejących dat.
  • OKREŚL calendar (np. calendar: 'iso8601') podczas tworzenia typów PlainMonthDay lub zwykłych typów innych niż ISO z obiektów.
  • NIE używaj typów Plain, jeśli chcesz przedstawić konkretny moment w czasie fizycznym (chwilę). Używaj Temporal.Instant lub Temporal.ZonedDateTime w przypadku logów, sygnatur czasowych zdarzeń lub planowania w różnych strefach czasowych.
  • NIE twórz obiektów Date z ręcznie utworzonych ciągów ISO (takich jak `${date}T00:00:00Z`). Utwórz obiekt Date z epochMilliseconds obiektu Temporal.Instant lub Temporal.ZonedDateTime i odczytaj go za pomocą Temporal.Instant.fromEpochMilliseconds(date.getTime()) lub date.toTemporalInstant(), aby założenie dotyczące strefy czasowej było jednoznaczne.
  • NIE używaj funkcji Intl.DateTimeFormat.prototype.formatToParts() do wyodrębniania ani porównywania wartości roku, miesiąca lub dnia. Odczytaj .year, .month i .day bezpośrednio z typu Plain i porównaj z .equals().
  • UŻYWAJ nazw IANA do identyfikowania stref czasowych (np. America/New_York), a nie stałego przesunięcia względem czasu UTC. Przesunięcia zmieniają się wraz z czasem letnim i przepisami, więc etykiety takie jak "UTC-5" stają się nieprawidłowe przez część roku.
  • PAMIĘTAJ, że obiekty Temporal są niezmienne. Metody takie jak .add(), .subtract() czy .with() zawsze zwracają nową instancję z nowym odwołaniem, zamiast mutować pierwotny obiekt na miejscu, dzięki czemu można je bezpiecznie przechowywać bezpośrednio w reaktywnym stanie interfejsu.

Strategia zastępcza

Zastępcze rozwiązania i obsługa Temporal w przeglądarkach

W przypadku przeglądarek, które nie obsługują jeszcze natywnego interfejsu API Temporal, użyj wykrywania funkcji i dynamicznie wczytaj temporal-polyfill (~15 KB min+gz).

Pamiętaj, że zaimportowanie temporal-polyfill nie powoduje automatycznego przypisania obiektu Temporal do zakresu globalnego. Przypisz go bezpośrednio, jeśli Twój kod opiera się na 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();
})();