Dopasowywanie typów czasowych do rzeczywistych zadań datowych: siedem praktycznych przykładów
Przepracuj siedem codziennych zadań związanych z datami i godzinami za pomocą JavaScript Temporal – od prostych dat i przekroczeń miesięcy po strefy czasowe, arytmetykę DST oraz weryfikację danych.
Źródła: JavaScript Temporal: 7 Date and Time Problems Explained Simply
Wymagania związane z datami zazwyczaj wydają się błahe: przedłużenie subskrypcji o miesiąc, pokazanie spotkania w strefie czasowej kolegi, liczenie dni pomiędzy dwiema datami. Komplikacje pojawiają się później, gdy okazuje się, że luty ma mniej dni, zmiana czasu letniego sprawia, że „jutro” jest oddalone o 23 godziny, lub urodziny trzeba przechowywać mimo braku strefy czasowej. API Temporal w JavaScript rozwiązuje ten problem, przypisując każdemu rodzajowi wartości własny typ, dzięki czemu sam typ dokumentuje znaczenie tej wartości. Siedem przykładów poniżej pokazuje, jaki typ pasuje do jakiego zastosowania, gdzie domyślne ustawienia mogą cię zaskoczyć oraz jakie decyzje Temporal celowo pozostawia twojemu aplikacji.
Przygotowanie środowiska do uruchamiania przykładów
Wspieranie natywne jest nadal w fazie wdrażania. W momencie pisania tego tekstu MDN wskazuje, że Temporal jest dostępny jedynie w ograniczonym stopniu, dlatego przed poleganiem na wbudowanej implementacji sprawdź dostępność w wybranych przeglądarkach i wersji Node.js. Strona referencyjna Temporal na MDN odnotowuje aktualny stan kompatybilności.
Aby eksperymentować bez wsparcia natywnego, zainstaluj Temporal polyfill w pustej folderze projektu. Potrzebujesz Node.js i npm:
mkdir temporal-demo
cd temporal-demo
npm init -y
npm install @js-temporal/polyfill@0.5.1
Stwórz plik o nazwie demo.mjs i zacznij go od tego importu:
import { Temporal } from "@js-temporal/polyfill";
Dodaj jeden z poniższych przykładów pod importem i uruchom plik:
node demo.mjs
Wszystkie przykłady zakładają ten sam import. Polyfill eksportuje lokalną zmienną typu Temporal i nie instaluje żadnej globalnej, więc każdy plik, który go używa, musi dokonać tego importu. Wersja jest ustalona na 0,5.1, ponieważ to właśnie ta wersja była podstawą do napisania przykładów; przed użyciem w rzeczywistym projekcie sprawdź dostępne wersje polyfilla.
1. Przechowywanie daty kalendarzowej bez godziny i strefy czasowej
Dzień urodzin lub termin umowy często nie ma określonej godziny dnia. Jeśli wymaganie brzmi po prostu „15 października”, dodawanie północy i strefy czasowej to tworzenie niepotrzebnych informacji, a właśnie one powodują późniejsze przesunięcie daty o jeden dzień podczas jej wyświetlania w innym miejscu. Temporal.PlainDate reprezentuje wyłącznie datę:
const deadline = Temporal.PlainDate.from("2026-10-15");
Jego pola to zwykłe liczby, a on sam serializuje się z powrotem do ciągu znaków w formacie ISO:
console.log(deadline.year); // 2026
console.log(deadline.month); // 10
console.log(deadline.day); // 15
console.log(deadline.toString()); // 2026-10-15
Wartość nie zawiera godziny, odchylenia UTC ani strefy czasowej. Domyślnie używa kalendarza ISO, a w odróżnieniu od starszego obiektu Date miesiące są numerowane od 1 do 12, a nie od zera.
Pamiętaj o tej ograniczeniu: PlainDate nie określa żadnego konkretnego momentu w czasie absolutnym. Jeśli termin oznacza faktycznie „17:00 w Delhi”, potrzebny jest również czas i strefa czasowa, do czego służy ZonedDateTime.
2. Tworzenie nowych dat bez modyfikacji oryginału
Dane zmienne udostępniane między częściami kodu są klasycznym źródłem błędów: jedna część kodu modyfikuje wartość, której druga część nadal używa. Obiekty czasowe są niezmienne, a operacje arytmetyczne zawsze tworzą nowy obiekt:
const originalDate = Temporal.PlainDate.from("2026-10-15");
const extendedDate = originalDate.add({ days: 7 });
Zapisywanie obu wartości pokazuje, że oryginał pozostał nietknięty:
console.log(originalDate.toString()); // 2026-10-15
console.log(extendedDate.toString()); // 2026-10-22
To oznacza, że możesz zachować poprzedni termin oraz wydłużony obok siebie bez konieczności najpierw klonowania czegokolwiek.
Z drugiej strony istnieje powszechny błąd: wywoływanie originalDate.add({ days: 7 }) na osobnej linii i oczekiwanie, że originalDate się zmieni. Tak nie jest. Przypisz lub zwróć wynik.
3. Zdecyduj, co oznacza „jeden miesiąc później”
Dodanie miesiąca do 31 stycznia nie ma jednej poprawnej odpowiedzi, ponieważ 31 lutego nie istnieje. Twoja aplikacja potrzebuje reguły. Domyślna wartość w Temporal to ograniczenie wyniku do najbliższej ważnej daty. Zacznij od końca stycznia:
const januaryEnd = Temporal.PlainDate.from("2026-01-31");
Dodanie jednego miesiąca ogranicza dzień do końca lutego:
const nextMonth = januaryEnd.add({ months: 1 });console.log(nextMonth.toString()); // 2026-02-28
Gdy ciche przesuwanie daty jest nie do przyjęcia, należy poprosić Temporal o rzucenie błędu, przekazując overflow: „reject”. Jako przykład weźmy datę fakturowania:
const billingDate = Temporal.PlainDate.from("2026-01-31");
Odrzucenie objawia się jako RangeError, który ten fragment kodu łapie, przerzucając jednocześnie wszelkie inne błędy:
try {
billingDate.add({ months: 1 }, { overflow: "reject" });
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("That day does not exist in the next month.");
}
Żadna z tych opcji nie stanowi polityki fakturowania; każda jest jedynie mechanizmem do jej wdrożenia. Należy również zwrócić uwagę na efekt kumulatywny. Gdy data 31 stycznia zostanie ograniczona do 28 lutego, dodanie kolejnego miesiąca daje 28 marca, a nie 31 marca, więc harmonogram budowany poprzez ciągłe dodawanie miesiąca do poprzedniej wartości ulega odchyleniu. Jeśli zasadą jest „ostatni dzień każdego miesiąca”, należy tę zasadę sformułować wyraźnie, na przykład obliczając zawsze na podstawie pierwotnej daty lub używając długości miesiąca, zamiast łączyć kolejne dodania.
4. Wyświetlanie jednej chwili w kilku strefach czasowych
Data i godzina zdarzenia określają dokładną chwilę. Lokalna data i godzina na ekranie danej osoby zależą od jej lokalizacji. Temporal.Instant reprezentuje samą tę chwilę:
const meeting = Temporal.Instant.from("2026-10-15T09:00:00Z");
Przekształcenie jej na nazwane strefy czasowe daje lokalną godzinę zegara w każdym miejscu, a ponowne przekształcenie pokazuje, że obie wskazują nadal na tę samą chwilę:
const delhi = meeting.toZonedDateTimeISO("Asia/Kolkata");
const london = meeting.toZonedDateTimeISO("Europe/London");console.log(delhi.toString());
// 2026-10-15T14:30:00+05:30[Asia/Kolkata]console.log(london.toString());
// 2026-10-15T10:00:00+01:00[Europe/London]console.log(delhi.toInstant().equals(london.toInstant()));
// true
Lokalne reprezentacje różnią się, ale podstawowa chwila jest identyczna. (Linie połączone w tym fragmencie powinny być osobnymi zdaniami.)
Należy używać nazwanych stref czasowych, takich jak Europe/London, zamiast stałych odstępstw. Nazwana strefa zawiera zasady zmiany jej odstępstwa w ciągu roku; sztywno zdefiniowane +01:00 opisuje jedynie Londyn w czasie letnim.
Tutaj zaczyna się wiele błędów: offset to nie strefa czasowa. Offset wskazuje różnicę względem UTC w konkretnym momencie, natomiast nazwana strefa czasowa dostarcza zasad określających tę różnicę w dowolnym momencie.
Powyższe ciągi znaków są skierowane do maszyn, włączając zapis strefy czasowej w nawiasach. Wszystko, co jest pokazywane użytkownikom, powinno być sformatowane zgodnie z ich ustawieniami lokalnymi, na przykład za pomocą toLocaleString() lub Intl.DateTimeFormat, zamiast wyświetlania surowego ciągu znaków.
5. Trzymaj „jutro” i „za 24 godziny” oddzielnie
Przez większość roku te wyrażenia dają ten sam wynik. W okresie zmiany czasu letniego tak nie jest. Weźmy południe w Nowym Jorku 7 marca 2026 roku, dzień przed przestawieniem zegarów:
const beforeClockChange = Temporal.ZonedDateTime.from(
"2026-03-07T12:00:00-05:00[America/New_York]"
);
Dodanie jednego dnia kalendarzowego i dodanie 24 godzin daje różne czasy lokalne:
const tomorrow = beforeClockChange.add({ days: 1 });
const after24Hours = beforeClockChange.add({ hours: 24 });console.log(tomorrow.toString());
// 2026-03-08T12:00:00-04:00[America/New_York]console.log(after24Hours.toString());
// 2026-03-08T13:00:00-04:00[America/New_York]
Artymetyka kalendarza utrzymuje czas zegara ściennego na południu. Artymetyka czasu upływu liczy dokładnie 24 godziny, a ponieważ zegar przeskoczył o godzinę w nocy, wynik jest 13:00.
Każda z tych metod nadaje się do różnych potrzeb. Umowa na dzień później zazwyczaj oznacza „ten sam czas lokalny jutro”, więc należy dodać dni. Token lub rezerwacja, która wygasa dokładnie po 24 godzinach, powinna uwzględniać dodanie godzin. Biblioteka Temporal może obliczyć jedno lub drugie; trzeba zdecydować, jakie znaczenie potrzebuje dana funkcjonalność, i warto to zapisać.
6. Odrzucanie nieważnych dat i nieistniejących godzin lokalnych
Podczas walidacji danych z formularza niemożliwa data powinna spowodować błąd, a nie po cichu stać się inną datą. W przypadku danych obiektowych należy zażądać ścisłego obsługi poprzez użycie overflow: „reject”:
try {
Temporal.PlainDate.from(
{ year: 2026, month: 2, day: 30 },
{ overflow: "reject" }
);
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("Please choose a valid calendar date.");
}
Bez tej opcji obiekt { year: 2026, month: 2, day: 30 } byłby ograniczony do 28 lutego. Ciągi ISO zachowują się inaczej: parsowanie "2026-02-30" jest odrzucane bez żadnej korekty. Nie należy zakładać, że parsowanie ciągów tekstowych i tworzenie obiektów mają takie samo zachowanie przy przekroczeniu zakresu.
Ważna data może nadal łączyć się z czasem lokalnym, który nigdy nie występuje. Gdy Nowy Jork przestawia zegary o 8 marca 2026 roku, godzina 2:30 rano jest całkowicie pomijana. Opcja disambiguation określa, co dzieje się z takimi danymi wejściowymi:
try {
Temporal.ZonedDateTime.from(
"2026-03-08T02:30[America/New_York]",
{ disambiguation: "reject" }
);
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("Choose a time that exists and is unambiguous.");
}
Przy ustawieniu disambiguation: „reject” Temporal rzuca błędy zarówno w przypadku pominiętych, jak i powtórzonych godzin, czyli tych, które występują dwa razy podczas cofania zegarów. Pozostałe ustawienia automatycznie wybierają odpowiednią interpretację. W przypadku formularzy rezerwacyjnych prośba o wybór ważnej godziny jest zazwyczaj jaśniejsza niż zmiana terminu bez poinformowania o tym użytkownika.
7. Porównuj, mierz i sortuj daty w sposób wyraźny
Dla porównań chronologicznych używaj metod właściwych dla danego typu. Zacznij od dwóch dat:
const startDate = Temporal.PlainDate.from("2026-10-15");
const endDate = Temporal.PlainDate.from("2026-10-20");
Temporal.PlainDate.compare() zwraca -1, 0 lub 1 w zależności od tego, czy pierwszy argument jest wcześniejszy, równy lub późniejszy. Funkcja until() zwraca wartość typu Temporal.Duration, która jest wyświetlana w formacie czasu ISO 8601, więc pięć dni przedstawia się jako P5D:
console.log(Temporal.PlainDate.compare(startDate, endDate));
// -1const difference = startDate.until(endDate, {
largestUnit: "days",
});console.log(difference.days); // 5
console.log(difference.toString()); // P5D
Dla PlainDate różnica jest mierzona w dniach kalendarzowych. Nie oblicza upływających godzin w żadnym strefie czasowej, ani nie decyduje, czy produkt uwzględnia zarówno dzień startu, jak i dzień końca; to zasada błędu o jeden element należy do Ciebie.
Ponieważ compare ma taką samą strukturę jak ta oczekiwana przez Array.prototype.sort, można go bezpośrednio przekazać. Dla nieuporządkowanej listy dat:
const dates = [
"2026-11-03",
"2026-10-15",
"2026-10-20",
].map(value => Temporal.PlainDate.from(value));
sortowanie staje się zapytaniem jednozdaniowym:
dates.sort(Temporal.PlainDate.compare);console.log(dates.map(date => date.toString()).join(", "));
// 2026-10-15, 2026-10-20, 2026-11-03
Unikaj nawyków przejętych z Date. Odliczanie obiektów temporalnych lub porównywanie ich za pomocą < i > powoduje błędy dla tych typów, co jest celowe – zapobiega to przypadkowym porównaniom niewłaściwego rodzaju. Z kolei === sprawdza tożsamość obiektów, więc dwa równe daty utworzone oddzielnie nie są równoważne za pomocą ===; do sprawdzania równości wartości należy używać .equals(). Aby dowiedzieć się więcej o subtelniejszych sposobach, w jakie stare nawyki mogą się pojawić ponownie, zapoznaj się z naszym przewodnikiem o pułapkach APITemporal, które ponownie wprowadzają błędy związane z datami.
Wybierz typ przed napisaniem obliczenia
Większość błędów związanych z datami to w rzeczywistości błędy modelowania, więc najpierw określ, co oznacza dana wartość:
- Data bez godziny ani strefy czasowej:
PlainDate - Godzina bez daty ani strefy czasowej:
PlainTime
PlainDateTimeInstantZonedDateTimeDurationTa kwestia wymaga omówienia podczas projektowania umowy API lub kolumny w bazie danych. Pole o nazwie po prostu date może prowadzić do różnic zdań co do tego, czy powinno przechowywać datę kalendarzową, zapis czasu w UTC czy godzinę lokalną.
Migracja nie musi odbywać się za jednym razem. Wybierz jeden funkcjonalność o jasnych wymaganiach, zdefiniuj jego format wejściowy, typ temporalny, politykę strefy czasowej oraz oczekiwany wynik, a także dodaj przypadki testowe dotyczące końców miesięcy i zmian czasu, gdzie to ma znaczenie. Kod dotyczący dat może pozostać bez zmian, dopóki nie pojawi się powód, by go edytować.
Lista kontrolna przed produkcją
- Zaufaj sprawdzeniu obsługi czasowej we wszystkich środowiskach, do których dostarczasz produkt, a jeśli używasz polyfilla, uwzględnij jego koszt pobierania i przetwarzania w budżecie Twojego pliku.
- Doskonale uzgodnij zasadę serializacji z backendem. Zwykła data, wartość UTC oraz ciąg znaków z identyfikatorem strefy w nawiasach to trzy różne formaty.
- Sprecyzuj zasady biznesowe: czy „jeden miesiąc później” może zmienić dzień, jakie terminy spotkań należy odrzucić w przypadku niejednoznaczności, oraz czy określona długość odnosi się do dni kalendarzowych czy upływających godzin?
Podsumowanie
Głównym wkładem Temporal nie jest lepsza interfejs API, lecz nakazanie rozróżnień, które Date zamazał: daty w porównaniu z momentami, odchylenia czasowe w porównaniu z strefami czasowymi, arytmetyka kalendarzowa w porównaniu z upływem czasu. Dostarcza precyzyjnych narzędzi do każdego przypadku, w tym rygorystyczne tryby, które wywołują wyraźny błąd zamiast zgadywać. Nie może natomiast decydować za Ciebie o zasadach działania. Samodzielnie ustal te reguły, uwzględnij je w typach i opcjach, które przekazujesz, oraz przygotuj testy na przypadki krawędziowe.