Ostateczny przewodnik po manipulacji datami i godzinami
Opublikowany: 2022-03-11Jako programista nie możesz uciec od manipulacji datami. Prawie każda aplikacja tworzona przez programistę będzie miała jakiś komponent, w którym data/godzina musi zostać pobrana od użytkownika, przechowywana w bazie danych i wyświetlana z powrotem użytkownikowi.
Zapytaj dowolnego programistę o jego doświadczenie w obsłudze dat i stref czasowych, a prawdopodobnie podzielą się historiami wojennymi. Obsługa pól daty i czasu z pewnością nie jest nauką rakietową, ale często może być żmudna i podatna na błędy.
Istnieją setki artykułów na ten temat, jednak większość z nich jest albo zbyt akademicka, skupiająca się na najdrobniejszych szczegółach, albo jest zbyt niejednolita, zawierając krótkie fragmenty kodu bez towarzyszących im dodatkowych wyjaśnień. Ten szczegółowy przewodnik po manipulacji DateTime powinien pomóc w zrozumieniu koncepcji programowania i najlepszych praktyk dotyczących czasu i daty bez konieczności przeglądania mnóstwa informacji na ten temat.
W tym artykule pomogę Ci jasno przemyśleć pola daty i czasu oraz zasugerować kilka najlepszych praktyk, które pomogą Ci uniknąć piekła daty/czasu. W tym miejscu omówimy niektóre kluczowe pojęcia, które są niezbędne do prawidłowego manipulowania wartościami daty i godziny, formaty, które są wygodne do przechowywania wartości DateTime i przesyłania ich przez interfejsy API i nie tylko.
Na początek właściwą odpowiedzią na kod produkcyjny jest prawie zawsze użycie odpowiedniej biblioteki, a nie rozwijanie własnej. Potencjalne trudności z obliczaniem DateTime omówione w tym artykule to tylko wierzchołek góry lodowej, ale nadal warto o nich wiedzieć, z biblioteką lub bez niej.
Biblioteki DateTime pomagają, jeśli dobrze je rozumiesz
Biblioteki dat pomagają na wiele sposobów, aby ułatwić Ci życie. Znacznie upraszczają analizowanie dat, operacje arytmetyczne i logiczne oraz formatowanie daty. Możesz znaleźć niezawodną bibliotekę dat zarówno dla front-endu, jak i back-endu, aby wykonać większość ciężkich zadań za Ciebie.
Jednak często korzystamy z bibliotek dat, nie myśląc o tym, jak faktycznie działa data/godzina. Data/czas to złożona koncepcja. Błędy, które pojawiają się z powodu ich nieprawidłowego zrozumienia, mogą być niezwykle trudne do zrozumienia i naprawienia, nawet przy pomocy bibliotek dat. Jako programista musisz zrozumieć podstawy i umieć docenić problemy, które rozwiązują biblioteki date, aby jak najlepiej je wykorzystać.
Ponadto biblioteki daty/godziny mogą zabrać Cię tylko do tej pory. Wszystkie biblioteki dat działają, zapewniając dostęp do wygodnych struktur danych do reprezentowania daty i godziny. Jeśli wysyłasz i odbierasz dane za pośrednictwem interfejsu API REST, ostatecznie będziesz musiał przekonwertować datę na ciąg i odwrotnie, ponieważ JSON nie ma natywnej struktury danych do reprezentowania DateTime. Koncepcje, które tu nakreśliłem, pomogą ci uniknąć niektórych typowych problemów, które mogą się pojawić podczas przeprowadzania transformacji typu data-na-ciąg i ciąg-do-daty.
Uwaga: Mimo że używam JavaScript jako języka programowania omawianego w tym artykule, są to ogólne koncepcje, które mają zastosowanie w dużej mierze do praktycznie wszystkich języków programowania i ich bibliotek dat. Więc nawet jeśli nigdy wcześniej nie napisałeś linijki JavaScript, możesz kontynuować czytanie, ponieważ nie zakładam żadnej wcześniejszej znajomości JavaScript w tym artykule.
Standaryzacja czasu
DateTime to bardzo konkretny punkt w czasie. Pomyślmy o tym. Kiedy piszę ten artykuł, zegar na moim laptopie pokazuje 21 lipca 13:29. To jest to, co nazywamy „czasem lokalnym”, czas, który widzę na zegarach ściennych wokół mnie i na moim zegarku na rękę.
Daj lub weź kilka minut, jeśli poproszę koleżankę o spotkanie w pobliskiej kawiarni o 15:00, mogę się spodziewać, że ją tam zobaczę mniej więcej w tym czasie. Podobnie nie byłoby zamieszania, gdybym zamiast tego powiedział na przykład „spotkajmy się za półtorej godziny”. Rutynowo rozmawiamy w ten sposób o czasie z ludźmi mieszkającymi w tym samym mieście lub strefie czasowej.
Pomyślmy o innym scenariuszu: chcę powiedzieć przyjacielowi mieszkającemu w Uppsali w Szwecji, że chcę z nim porozmawiać o 17:00. Wysyłam mu wiadomość: „Hej Anton, porozmawiajmy o 17:00”. Od razu otrzymuję odpowiedź: „Twój czas czy mój czas?”
Anton powiedział mi, że mieszka w środkowoeuropejskiej strefie czasowej, czyli UTC+01:00. Mieszkam w UTC+05:45. Oznacza to, że kiedy mieszkam o godzinie 17:00, jest godzina 17:00 - 17:45 = 11:15 UTC, co przekłada się na 11:15 UTC + 01:00 = 12:15 w Uppsali, idealne dla obu z nas.
Należy również pamiętać o różnicy między strefą czasową (czas środkowoeuropejski) a przesunięciem strefy czasowej (UTC+05:45). Kraje mogą również zdecydować o zmianie przesunięć stref czasowych dla czasu letniego z powodów politycznych. Prawie co roku następuje zmiana reguł w co najmniej jednym kraju, co oznacza, że każdy kod z tymi regułami musi być aktualizowany — warto zastanowić się, od czego zależy baza kodu dla każdej warstwy aplikacji.
To kolejny dobry powód, dla którego zalecamy, aby w większości przypadków tylko interfejs obsługiwał strefy czasowe. Jeśli tak nie jest, co się dzieje, gdy reguły używane przez silnik bazy danych nie są zgodne z regułami w interfejsie lub zapleczu?
Ten problem zarządzania dwiema różnymi wersjami czasu, względem użytkownika i względem powszechnie akceptowanego standardu, jest trudny, tym bardziej w świecie programowania, gdzie precyzja jest kluczowa i nawet jedna sekunda może zrobić ogromną różnicę. Pierwszym krokiem do rozwiązania tych problemów jest przechowywanie DateTime w UTC.
Standaryzacja formatu
Standaryzacja czasu jest cudowna, ponieważ muszę tylko przechowywać czas UTC i dopóki znam strefę czasową użytkownika, zawsze mogę przeliczyć na jego czas. I odwrotnie, jeśli znam czas lokalny użytkownika i jego strefę czasową, mogę przekonwertować go na UTC.
Ale daty i godziny można podawać w wielu różnych formatach. Jako datę możesz napisać „30 lipca” lub „30 lipca” lub „30 lipca” (lub 30 lipca, w zależności od miejsca zamieszkania). Na razie możesz napisać „21:30” lub „2130”.
Naukowcy z całego świata zebrali się, aby rozwiązać ten problem i zdecydowali się na format opisujący czas, który programiści naprawdę lubią, ponieważ jest krótki i precyzyjny. Lubimy go nazywać „formatem daty ISO”, który jest uproszczoną wersją rozszerzonego formatu ISO-8601 i wygląda to tak:
Dla 00:00 lub UTC używamy zamiast tego „Z”, co oznacza czas Zulu, inną nazwę UTC.
Manipulacja datami i arytmetyka w JavaScript
Zanim zaczniemy od najlepszych praktyk, nauczymy się manipulować datami za pomocą JavaScript, aby zrozumieć składnię i ogólne koncepcje. Chociaż używamy JavaScript, możesz łatwo dostosować te informacje do swojego ulubionego języka programowania.
Użyjemy arytmetyki dat do rozwiązania typowych problemów związanych z datami, z którymi spotyka się większość programistów.
Moim celem jest umożliwienie wygodnego tworzenia obiektu daty z ciągu znaków i wyciągania z niego komponentów. Jest to coś, w czym może ci pomóc biblioteka dat, ale zawsze lepiej jest zrozumieć, jak to się robi za kulisami.
Kiedy już zabrudzimy sobie ręce datą/godziną, łatwiej jest pomyśleć o problemach, z którymi się borykamy, wyodrębnić najlepsze praktyki i iść do przodu. Jeśli chcesz przejść do najlepszych praktyk, możesz to zrobić, ale gorąco polecam przynajmniej przejrzenie poniższej sekcji dotyczącej arytmetyki dat.
Obiekt daty JavaScript
Języki programowania zawierają przydatne konstrukcje, które ułatwiają nam życie. Obiekt JavaScript Date
jest jedną z takich rzeczy. Oferuje wygodne metody uzyskiwania bieżącej daty i czasu, przechowywania daty w zmiennej, wykonywania arytmetyki dat i formatowania daty w oparciu o ustawienia regionalne użytkownika.
Ze względu na różnice między implementacjami przeglądarki i nieprawidłową obsługę czasu letniego (DST), w zależności od obiektu Date dla aplikacji o znaczeniu krytycznym nie jest zalecane i prawdopodobnie powinieneś używać biblioteki DateTime, takiej jak Luxon, date-fns lub dayjs. (Bez względu na to, czego używasz, unikaj popularnego niegdyś pliku Moment.js — często nazywanego po prostu moment
, jak pojawia się w kodzie — ponieważ jest teraz przestarzały).
Ale do celów edukacyjnych użyjemy metod udostępnianych przez obiekt Date(), aby dowiedzieć się, jak JavaScript obsługuje DateTime.
Pobieranie aktualnej daty
const currentDate = new Date();
Jeśli nie przekażesz niczego do konstruktora Date, zwrócony obiekt Date zawiera bieżącą datę i godzinę.
Następnie możesz go sformatować, aby wyodrębnić tylko część daty w następujący sposób:
const currentDate = new Date(); const currentDayOfMonth = currentDate.getDate(); const currentMonth = currentDate.getMonth(); // Be careful! January is 0, not 1 const currentYear = currentDate.getFullYear(); const dateString = currentDayOfMonth + "-" + (currentMonth + 1) + "-" + currentYear; // "27-11-2020"
Uwaga: Pułapka „Styczeń to 0” jest powszechna, ale nie powszechna. Warto dokładnie sprawdzić dokumentację dowolnego języka (lub formatu konfiguracyjnego: np. cron jest w szczególności oparty na 1), zanim zaczniesz go używać.
Uzyskiwanie aktualnego znacznika czasu
Jeśli zamiast tego chcesz uzyskać aktualny znacznik czasu, możesz utworzyć nowy obiekt Date i użyć metody getTime().
const currentDate = new Date(); const timestamp = currentDate.getTime();
W JavaScript znacznik czasu to liczba milisekund, które minęły od 1 stycznia 1970 roku.
Jeśli nie zamierzasz obsługiwać <IE8, możesz użyć Date.now()
, aby bezpośrednio uzyskać sygnaturę czasową bez konieczności tworzenia nowego obiektu Date.
Parsowanie daty
Konwersja ciągu do obiektu daty JavaScript odbywa się na różne sposoby.
Konstruktor obiektu Date akceptuje wiele różnych formatów daty:
const date1 = new Date("Wed, 27 July 2016 13:30:00"); const date2 = new Date("Wed, 27 July 2016 07:45:00 UTC"); const date3 = new Date("27 July 2016 13:30:00 UTC+05:45");
Pamiętaj, że nie musisz podawać dnia tygodnia, ponieważ JS może określić dzień tygodnia dla dowolnej daty.
Możesz również podać rok, miesiąc, dzień, godziny, minuty i sekundy jako osobne argumenty:
const date = new Date(2016, 6, 27, 13, 30, 0);
Oczywiście zawsze możesz użyć formatu daty ISO:
const date = new Date("2016-07-27T07:45:00Z");
Możesz jednak wpaść w kłopoty, jeśli nie podasz wyraźnie strefy czasowej!
const date1 = new Date("25 July 2016"); const date2 = new Date("July 25, 2016");
Każdy z nich daje 25 lipca 2016 00:00:00 czasu lokalnego.
Jeśli używasz formatu ISO, nawet jeśli podasz tylko datę, a nie godzinę i strefę czasową, automatycznie zaakceptuje strefę czasową jako UTC.
To znaczy że:
new Date("25 July 2016").getTime() !== new Date("2016-07-25").getTime() new Date("2016-07-25").getTime() === new Date("2016-07-25T00:00:00Z").getTime()
Formatowanie daty
Na szczęście współczesny JavaScript ma kilka wygodnych funkcji internacjonalizacji wbudowanych w standardową przestrzeń nazw Intl
, dzięki którym formatowanie daty jest prostą operacją.
Do tego będziemy potrzebować dwóch obiektów: Date
i Intl.DateTimeFormat
, zainicjowanych naszymi preferencjami wyjściowymi. Przypuśćmy, że chcielibyśmy użyć formatu amerykańskiego (M/D/RRRR), wyglądałoby to tak:
const firstValentineOfTheDecade = new Date(2020, 1, 14); // 1 for February const enUSFormatter = new Intl.DateTimeFormat('en-US'); console.log(enUSFormatter.format(firstValentineOfTheDecade)); // 2/14/2020
Gdybyśmy zamiast tego chcieli formatu holenderskiego (D/M/RRRR), po prostu przekazalibyśmy inny kod kultury do konstruktora DateTimeFormat
:
const nlBEFormatter = new Intl.DateTimeFormat('nl-BE'); console.log(nlBEFormatter.format(firstValentineOfTheDecade)); // 14/2/2020
Lub dłuższa forma formatu amerykańskiego, z podaną nazwą miesiąca:
const longEnUSFormatter = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); console.log(longEnUSFormatter.format(firstValentineOfTheDecade)); // February 14, 2020
Teraz, gdybyśmy chcieli mieć właściwy format porządkowy w dniu miesiąca — to znaczy „14” zamiast „14” — wymaga to niestety pewnego obejścia, ponieważ jedynymi prawidłowymi wartościami day
w chwili pisania tego tekstu są "numeric"
lub "2-digit"
. Pożyczając wersję kodu Mathiasa Bynensa autorstwa Flavio Copesa, aby wykorzystać do tego inną część Intl
, możemy dostosować wynik dnia miesiąca za pomocą formatToParts()
:
const pluralRules = new Intl.PluralRules('en-US', { type: 'ordinal' }) const suffixes = { 'one': 'st', 'two': 'nd', 'few': 'rd', 'other': 'th' } const convertToOrdinal = (number) => `${number}${suffixes[pluralRules.select(number)]}` // At this point: // convertToOrdinal("1") === "1st" // convertToOrdinal("2") === "2nd" // etc. const extractValueAndCustomizeDayOfMonth = (part) => { if (part.type === "day") { return convertToOrdinal(part.value); } return part.value; }; console.log( longEnUSFormatter.formatToParts(firstValentineOfTheDecade) .map(extractValueAndCustomizeDayOfMonth) .join("") ); // February 14th, 2020
Niestety formatToParts
nie jest w ogóle obsługiwany przez Internet Explorer (IE) w chwili pisania tego tekstu, ale wszystkie inne technologie desktopowe, mobilne i back-end (tj. Node.js) mają wsparcie. Dla tych, którzy potrzebują obsługi IE i absolutnie potrzebują liczb porządkowych, odpowiedź znajduje się w poniższym przypisie (lub lepiej, odpowiedniej bibliotece dat).
Jeśli potrzebujesz obsługiwać starsze przeglądarki, takie jak IE przed wersją 11, formatowanie daty w JavaScript jest trudniejsze, ponieważ nie było standardowych funkcji formatowania daty, takich jak strftime
w Pythonie lub PHP.
Na przykład w PHP funkcja strftime("Today is %b %d %Y %X", mktime(5,10,0,12,30,99))
daje Ci Today is Dec 30 1999 05:10:00
.
Możesz użyć innej kombinacji liter poprzedzonych %
, aby otrzymać datę w różnych formatach. (Uważaj, nie każdy język przypisuje to samo znaczenie każdej literze — w szczególności „M” i „m” można zamienić na minuty i miesiące).
Jeśli jesteś pewien formatu, którego chcesz użyć, najlepiej wyodrębnić poszczególne bity za pomocą funkcji JavaScript, które omówiliśmy powyżej, i samodzielnie utworzyć ciąg.
var currentDate = new Date (); var date = currentDate.getDate(); var month = currentDate.getMonth(); var year = currentDate.getFullYear();
Datę możemy uzyskać w formacie MM/DD/RRRR jako
var monthDateYear = (month+ 1 ) + "/" + date + "/" + year;
Problem z tym rozwiązaniem polega na tym, że daty mogą mieć niespójną długość, ponieważ niektóre miesiące i dni miesiąca są jednocyfrowe, a inne dwucyfrowe. Może to być problematyczne, na przykład, jeśli wyświetlasz datę w kolumnie tabeli, ponieważ daty nie są wyrównane.
Możemy rozwiązać ten problem za pomocą funkcji „pad”, która dodaje wiodące 0.
function pad ( n ) { return n< 10 ? '0' +n : n; }
Teraz otrzymujemy poprawną datę w formacie MM/DD/RRRR za pomocą:
var mmddyyyy = pad(month + 1 ) + "/" + pad(date) + "/" + year;
Jeśli zamiast tego chcemy podać DD-MM-RRRR, proces jest podobny:
var ddmmyyyy = pad(date) + "-" + pad(month + 1 ) + "-" + year;
Podnieś stawkę i spróbuj wydrukować datę w formacie „Miesiąc Data, Rok”. Będziemy potrzebować mapowania indeksów miesięcy na nazwy:
var monthNames = [ "January" , "February" , "March" , "April" , "May" , "June" , "July" , "August" , "September" , "October" , "November" , "December" ]; var dateWithFullMonthName = monthNames[month] + " " + pad(date) + ", " + year;
Niektórzy lubią wyświetlać datę jako 1 stycznia 2013. Nie ma problemu, potrzebujemy tylko ordinal
funkcji pomocniczej, która zwraca 1st dla 1, 12th dla 12, 103. dla 103 itd., a reszta jest prosta:
var ordinalDate = ordinal(date) + " " + monthNames[month] + ", " + year;
Łatwo jest określić dzień tygodnia na podstawie obiektu daty, więc dodajmy, że w:
var daysOfWeek = [ "Sun" , "Mon" , "Tue" , "Wed" , "Thu" , "Fri" , "Sat" ]; ordinalDateWithDayOfWeek = daysOfWeek[currentDate.getDay()] + ", " + ordinalDate;
Ważniejszą kwestią jest to, że po wydobyciu liczb z daty formatowanie jest głównie związane z ciągami.
Zmiana formatu daty
Gdy już wiesz, jak przeanalizować datę i sformatować ją, zmiana daty z jednego formatu na inny to tylko kwestia połączenia tych dwóch.
Na przykład, jeśli masz datę w formacie 21.07.2013 i chciałeś zmienić format na 21-07-2013, możesz to osiągnąć tak:
const myDate = new Date("Jul 21, 2013"); const dayOfMonth = myDate.getDate(); const month = myDate.getMonth(); const year = myDate.getFullYear(); function pad(n) { return n<10 ? '0'+n : n } const ddmmyyyy = pad(dayOfMonth) + "-" + pad(month + 1) + "-" + year; // "21-07-2013"
Korzystanie z funkcji lokalizacji obiektu daty w JavaScript
Omówione powyżej metody formatowania daty powinny działać w większości aplikacji, ale jeśli naprawdę chcesz zlokalizować formatowanie daty, sugeruję użycie metody toLocaleDateString()
obiektu Date
:
const today = new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', });
…daje nam coś takiego jak 26 Jul 2016
.

Zmiana lokalizacji na „en-US” daje zamiast tego „26 lipca 2016”. Zwróć uwagę, jak zmieniło się formatowanie, ale opcje wyświetlania pozostały takie same - bardzo przydatna funkcja. Jak pokazano w poprzedniej sekcji, nowsza technika oparta na Intl.DateTimeFormat
działa bardzo podobnie do tej, ale umożliwia ponowne użycie obiektu formatera, dzięki czemu wystarczy ustawić opcje tylko raz.
Dzięki toLocaleDateString()
dobrym nawykiem jest zawsze przekazywanie opcji formatowania, nawet jeśli dane wyjściowe wyglądają dobrze na komputerze. Może to chronić interfejs użytkownika przed włamaniem się w nieoczekiwane ustawienia regionalne z naprawdę długimi nazwami miesięcy lub niewygodnym wyglądem z powodu krótkich.
Gdybym chciał zamiast tego pełny miesiąc „lipiec”, wystarczyłoby zmienić parametr miesiąca w opcjach na „długi”. JavaScript obsługuje wszystko za mnie. W przypadku en-US mam teraz 26 lipca 2016 r.
Uwaga: jeśli chcesz, aby przeglądarka automatycznie używała ustawień regionalnych użytkownika, możesz podać „undefined” jako pierwszy parametr.
Jeśli chcesz pokazać numeryczną wersję daty i nie chcesz zawracać sobie głowy MM/DD/RRRR vs. DD/MM/RRRR dla różnych lokalizacji, proponuję następujące proste rozwiązanie:
const today = new Date().toLocaleDateString(undefined, { day: 'numeric', month: 'numeric', year: 'numeric', });
Na moim komputerze to 7/26/2016
. Jeśli chcesz mieć pewność, że miesiąc i data mają dwie cyfry, po prostu zmień opcje:
const today = new Date().toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric', });
To 07/26/2016
. Właśnie tego chcieliśmy!
Możesz także użyć innych powiązanych funkcji, aby zlokalizować sposób wyświetlania czasu i daty:
Kod | Wyjście | Opis |
---|---|---|
| „4:21:38” | Wyświetlaj zlokalizowaną wersję tylko czasu |
| „04:21:38” | Wyświetlaj zlokalizowany czas na podstawie dostarczonych opcji |
| "22.07.2016, 4:21:38" | Wyświetl datę i godzinę dla lokalizacji użytkownika |
| "22.07.2016, 04:21" | Wyświetlaj zlokalizowaną datę i godzinę na podstawie dostarczonych opcji |
Obliczanie względnych dat i godzin
Oto przykład dodawania 20 dni do daty JavaScript (tj. obliczanie daty 20 dni po znanej dacie):
const myDate = new Date("July 20, 2016 15:00:00"); const nextDayOfMonth = myDate.getDate() + 20; myDate.setDate(nextDayOfMonth); const newDate = myDate.toLocaleString();
Oryginalny obiekt date reprezentuje teraz datę 20 dni po 20 lipca, a newDate
zawiera zlokalizowany ciąg reprezentujący tę datę. W mojej przeglądarce newDate
zawiera „8/9/2016, 15:00:00”.
Aby obliczyć względne znaczniki czasu z dokładniejszą różnicą niż całe dni, możesz użyć Date.getTime()
i Date.setTime()
do pracy z liczbami całkowitymi reprezentującymi liczbę milisekund od pewnej epoki — mianowicie 1 stycznia 1970 roku. na przykład, jeśli chcesz wiedzieć, kiedy jest 17 godzin później:
const msSinceEpoch = (new Date()).getTime(); const seventeenHoursLater = new Date(msSinceEpoch + 17 * 60 * 60 * 1000);
Porównywanie dat
Podobnie jak wszystko inne związane z datą, porównywanie dat ma swoje własne problemy.
Najpierw musimy stworzyć obiekty daty. Na szczęście <, >, <= i >= wszystkie działają. Zatem porównanie 19 lipca 2014 r. i 18 lipca 2014 r. jest tak proste, jak:
const date1 = new Date("July 19, 2014"); const date2 = new Date("July 28, 2014"); if(date1 > date2) { console.log("First date is more recent"); } else { console.log("Second date is more recent"); }
Sprawdzanie równości jest trudniejsze, ponieważ dwa obiekty daty reprezentujące tę samą datę są nadal dwoma różnymi obiektami daty i nie będą równe. Porównywanie ciągów dat jest złym pomysłem, ponieważ na przykład „20 lipca 2014” i „20 lipca 2014” reprezentują tę samą datę, ale mają różne reprezentacje ciągów. Poniższy fragment ilustruje pierwszy punkt:
const date1 = new Date("June 10, 2003"); const date2 = new Date(date1); const equalOrNot = date1 == date2 ? "equal" : "not equal"; console.log(equalOrNot);
To da wynik not equal
.
Ten konkretny przypadek można naprawić, porównując liczby całkowite dat (ich znaczniki czasu) w następujący sposób:
date1.getTime() == date2.getTime()
Widziałem ten przykład w wielu miejscach, ale nie podoba mi się, ponieważ zwykle nie tworzy się obiektu daty z innego obiektu daty. Dlatego uważam, że przykład jest ważny tylko z akademickiego punktu widzenia. Ponadto wymaga to, aby oba obiekty Date odwoływały się do dokładnie tej samej sekundy, podczas gdy możesz chcieć tylko wiedzieć, czy odnoszą się do tego samego dnia, godziny lub minuty.
Spójrzmy na bardziej praktyczny przykład. Próbujesz porównać, czy data urodzin wprowadzona przez użytkownika jest taka sama jak szczęśliwa data, którą otrzymujesz z interfejsu API.
const userEnteredString = "12/20/1989"; // MM/DD/YYYY format const dateStringFromAPI = "1989-12-20T00:00:00Z"; const dateFromUserEnteredString = new Date(userEnteredString) const dateFromAPIString = new Date(dateStringFromAPI); if (dateFromUserEnteredString.getTime() == dateFromAPIString.getTime()) { transferOneMillionDollarsToUserAccount(); } else { doNothing(); }
Obie reprezentowały tę samą datę, ale niestety twój użytkownik nie dostanie miliona dolarów.
Oto problem: JavaScript zawsze zakłada, że strefa czasowa jest tą, którą udostępnia przeglądarka, chyba że wyraźnie określono inaczej.
Dla mnie oznacza to, że new Date ("12/20/1989")
utworzy datę 1989-12-20T00:00:00+5:45 lub 1989-12-19T18:15:00Z, która nie jest taka sama jak 1989-12-20T00:00:00Z pod względem znacznika czasu.
Nie można zmienić samej strefy czasowej istniejącego obiektu daty, więc naszym celem jest teraz utworzenie nowego obiektu daty, ale z czasem UTC zamiast lokalnej strefy czasowej.
Zignorujemy strefę czasową użytkownika i użyjemy czasu UTC podczas tworzenia obiektu daty. Można to zrobić na dwa sposoby:
- Utwórz ciąg daty w formacie ISO na podstawie daty wprowadzonej przez użytkownika i użyj go do utworzenia obiektu Date. Użycie prawidłowego formatu daty ISO do utworzenia obiektu Date, przy jednoczesnym bardzo jasnym określeniu intencji UTC w porównaniu z lokalnym.
const userEnteredDate = "12/20/1989"; const parts = userEnteredDate.split("/"); const userEnteredDateISO = parts[2] + "-" + parts[0] + "-" + parts[1]; const userEnteredDateObj = new Date(userEnteredDateISO + "T00:00:00Z"); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDateObj.getTime() == dateFromAPI.getTime(); // true
Działa to również, jeśli nie określisz czasu, ponieważ domyślnie będzie to północ (tj. 00:00:00 Z):
const userEnteredDate = new Date("1989-12-20"); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDate.getTime() == dateFromAPI.getTime(); // true
Pamiętaj: Jeśli do konstruktora daty zostanie przekazany ciąg znaków w poprawnym formacie daty ISO RRRR-MM-DD, automatycznie przyjmuje on czas UTC.
- JavaScript udostępnia zgrabną funkcję Date.UTC(), której można użyć do uzyskania znacznika czasu UTC daty. Wyciągamy składniki z daty i przekazujemy je do funkcji.
const userEnteredDate = new Date("12/20/1989"); const userEnteredDateTimeStamp = Date.UTC(userEnteredDate.getFullYear(), userEnteredDate.getMonth(), userEnteredDate.getDate(), 0, 0, 0); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDateTimeStamp == dateFromAPI.getTime(); // true ...
Znajdowanie różnicy między dwiema datami
Typowym scenariuszem, z którym się spotkasz, jest znalezienie różnicy między dwiema datami.
Omawiamy dwa przypadki użycia:
Znajdowanie liczby dni między dwiema datami
Konwertuj obie daty na znacznik czasu UTC, znajdź różnicę w milisekundach i znajdź równoważne dni.
const dateFromAPI = "2016-02-10T00:00:00Z"; const now = new Date(); const datefromAPITimeStamp = (new Date(dateFromAPI)).getTime(); const nowTimeStamp = now.getTime(); const microSecondsDiff = Math.abs(datefromAPITimeStamp - nowTimeStamp); // Math.round is used instead of Math.floor to account for certain DST cases // Number of milliseconds per day = // 24 hrs/day * 60 minutes/hour * 60 seconds/minute * 1000 ms/second const daysDiff = Math.round(microSecondsDiff / (1000 * 60 * 60 * 24)); console.log(daysDiff);
Znajdowanie wieku użytkownika od daty urodzenia
const birthDateFromAPI = "12/10/1989";
Uwaga: mamy niestandardowy format. Przeczytaj dokumentację API, aby ustalić, czy oznacza to 12 października czy 10 grudnia. Zmień odpowiednio na format ISO.
const parts = birthDateFromAPI.split("/"); const birthDateISO = parts[2] + "-" + parts[0] + "-" + parts[1]; const birthDate = new Date(birthDateISO); const today = new Date(); let age = today.getFullYear() - birthDate.getFullYear(); if(today.getMonth() < birthDate.getMonth()) { age--; } if(today.getMonth() == birthDate.getMonth() && today.getDate() < birthDate.getDate()) { age--; }
Wiem, że są bardziej zwięzłe sposoby na napisanie tego kodu, ale lubię pisać w ten sposób ze względu na czystą logikę.
Sugestie, aby uniknąć piekła randkowego
Teraz, gdy już dobrze znamy się na arytmetyce dat, jesteśmy w stanie zrozumieć najlepsze praktyki i powody ich stosowania.
Pobieranie daty i godziny od użytkownika
Jeśli otrzymujesz datę i godzinę od użytkownika, najprawdopodobniej szukasz ich lokalnej daty i godziny. Widzieliśmy w sekcji arytmetycznej daty, że konstruktor Date
może akceptować datę na wiele różnych sposobów.
Aby usunąć wszelkie nieporozumienia, zawsze sugeruję utworzenie daty przy użyciu new Date(year, month, day, hours, minutes, seconds, milliseconds)
, nawet jeśli masz już datę w prawidłowym formacie parsowalnym. Jeśli wszyscy programiści w twoim zespole będą przestrzegać tej prostej zasady, bardzo łatwo będzie utrzymać kod na dłuższą metę, ponieważ jest on tak jednoznaczny, jak to tylko możliwe w przypadku konstruktora Date
.
Fajną częścią jest to, że możesz używać odmian, które pozwalają pominąć dowolny z czterech ostatnich parametrów, jeśli są one równe zero; tj. new Date(2012, 10, 12)
jest tym samym co new Date(2012, 10, 12, 0, 0, 0, 0)
ponieważ nieokreślone parametry domyślnie wynoszą zero.
Na przykład, jeśli używasz selektora daty i godziny, który podaje datę 2012-10-12 i godzinę 12:30, możesz wyodrębnić części i utworzyć nowy obiekt Date w następujący sposób:
const dateFromPicker = "2012-10-12"; const timeFromPicker = "12:30"; const dateParts = dateFromPicker.split("-"); const timeParts = timeFromPicker.split(":"); const localDate = new Date(dateParts[0], dateParts[1]-1, dateParts[2], timeParts[0], timeParts[1]);
Staraj się unikać tworzenia daty z ciągu znaków, chyba że jest ona w formacie daty ISO. Zamiast tego użyj metody Date(rok, miesiąc, data, godziny, minuty, sekundy, mikrosekundy).
Uzyskiwanie tylko daty
Jeśli otrzymujesz tylko datę, na przykład datę urodzenia użytkownika, najlepiej jest przekonwertować format na prawidłowy format daty ISO, aby wyeliminować wszelkie informacje o strefie czasowej, które mogą powodować przesunięcie daty do przodu lub do tyłu podczas konwersji na UTC. Na przykład:
const dateFromPicker = "12/20/2012"; const dateParts = dateFromPicker.split("/"); const ISODate = dateParts[2] + "-" + dateParts[0] + "-" + dateParts[1]; const birthDate = new Date(ISODate).toISOString();
Jeśli zapomniałeś, jeśli utworzysz obiekt Date
z danymi wejściowymi w prawidłowym formacie daty ISO (RRRR-MM-DD), domyślnie będzie to UTC zamiast domyślnej strefy czasowej przeglądarki.
Przechowywanie daty
Zawsze przechowuj DateTime w UTC. Zawsze wysyłaj ciąg daty ISO lub sygnaturę czasową na zaplecze.
Pokolenia programistów komputerowych uświadomiły sobie tę prostą prawdę po gorzkich doświadczeniach próbujących pokazać użytkownikowi prawidłowy czas lokalny. Przechowywanie czasu lokalnego w zapleczu to zły pomysł, lepiej pozwolić przeglądarce obsłużyć konwersję na czas lokalny w interfejsie.
Ponadto powinno być oczywiste, że nigdy nie należy wysyłać ciągu DateTime, takiego jak „20 lipca 1989, 12:10” na zapleczu. Nawet jeśli wyślesz również strefę czasową, zwiększasz wysiłek, aby inni programiści zrozumieli Twoje intencje oraz poprawnie przeanalizowali i zachowali datę.
Użyj toISOString()
lub toJSON()
obiektu Date, aby przekonwertować lokalną DateTime na UTC.
const dateFromUI = "12-13-2012"; const timeFromUI = "10:20"; const dateParts = dateFromUI.split("-"); const timeParts = timeFromUI.split(":"); const date = new Date(dateParts[2], dateParts[0]-1, dateParts[1], timeParts[0], timeParts[1]); const dateISO = date.toISOString(); $.post("http://example.com/", {date: dateISO}, ...)
Wyświetlanie daty i godziny
- Uzyskaj sygnaturę czasową lub datę w formacie ISO z interfejsu API REST.
- Utwórz obiekt
Date
. - Użyj metod
toLocaleString()
lubtoLocaleDateString()
itoLocaleTimeString()
albo biblioteki dat, aby wyświetlić czas lokalny.
const dateFromAPI = "2016-01-02T12:30:00Z"; const localDate = new Date(dateFromAPI); const localDateString = localDate.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric', }); const localTimeString = localDate.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit', });
Kiedy należy przechowywać również czas lokalny?
„Czasami ważne jest, aby znać strefę czasową, w której miało miejsce zdarzenie, a konwersja do jednej strefy czasowej nieodwołalnie wymazuje te informacje.
„Jeśli robisz promocję marketingową i chcesz wiedzieć, którzy klienci złożyli zamówienia w porze lunchu, zamówienie, które wydaje się, że zostało złożone w południe GMT, nie jest zbyt pomocne, gdy faktycznie zostało złożone podczas śniadania w Nowym Jorku”.
Jeśli natkniesz się na taką sytuację, rozsądniej byłoby zaoszczędzić również czas lokalny. Jak zwykle chcielibyśmy utworzyć datę w formacie ISO, ale najpierw musimy znaleźć przesunięcie strefy czasowej.
Funkcja getTimeZoneOffset()
obiektu Date informuje nas o liczbie minut, które po dodaniu do danego czasu lokalnego dają równoważny czas UTC. Proponuję przekonwertować go do formatu (+-) gg: mm, ponieważ bardziej oczywiste jest, że jest to przesunięcie strefy czasowej.
const now = new Date(); const tz = now.gettime zoneOffset();
Dla mojej strefy czasowej +05:45 otrzymuję -345, to nie tylko przeciwny znak, ale liczba taka jak -345 może być kompletnie kłopotliwa dla programisty zaplecza. Więc konwertujemy to na +05:45.
const sign = tz > 0 ? "-" : "+"; const hours = pad(Math.floor(Math.abs(tz)/60)); const minutes = pad(Math.abs(tz)%60); const tzOffset = sign + hours + ":" + minutes;
Teraz pobieramy resztę wartości i tworzymy poprawny ciąg ISO, który reprezentuje lokalną datę i godzinę.
const localDateTime = now.getFullYear() + "-" + pad(now.getMonth()+1) + "-" + pad(now.getDate()) + "T" + pad(now.getHours()) + ":" + pad(now.getMinutes()) + ":" + pad(now.getSeconds());
Jeśli chcesz, możesz umieścić w obiekcie daty UTC i lokalne.
const eventDate = { utc: now.toISOString(), local: localDateTime, tzOffset: tzOffset, }
Teraz na zapleczu, jeśli chcesz dowiedzieć się, czy zdarzenie miało miejsce przed południem czasu lokalnego, możesz przeanalizować datę i po prostu użyć funkcji getHours()
.
const localDateString = eventDate.local; const localDate = new Date(localDateString); if(localDate.getHours() < 12) { console.log("Event happened before noon local time"); }
Nie użyliśmy tutaj tzOffset
, ale nadal go przechowujemy, ponieważ możemy go potrzebować w przyszłości do celów debugowania. W rzeczywistości możesz po prostu wysłać przesunięcie strefy czasowej i tylko czas UTC. Ale lubię też przechowywać czas lokalny, ponieważ ostatecznie będziesz musiał przechowywać datę w bazie danych, a przechowywanie czasu lokalnego osobno pozwala na bezpośrednie zapytanie na podstawie pola, zamiast wykonywania obliczeń w celu uzyskania lokalnej daty.
Czasami, nawet z zapisaną lokalną strefą czasową, będziesz chciał wyświetlić daty w określonej strefie czasowej. Na przykład godziny wydarzeń mogą mieć większy sens w strefie czasowej bieżącego użytkownika, jeśli są wirtualne, lub w strefie czasowej, w której będą się odbywać fizycznie, jeśli tak nie jest. W każdym razie warto wcześniej przyjrzeć się ustalonym rozwiązaniom formatowania z wyraźnymi nazwami stref czasowych.
Konfiguracja serwera i bazy danych
Zawsze konfiguruj serwery i bazy danych tak, aby korzystały ze strefy czasowej UTC. (Zauważ, że UTC i GMT to nie to samo — na przykład GMT może sugerować przejście na BST latem, podczas gdy UTC nigdy tego nie zrobi).
Widzieliśmy już, jak bardzo bolesne mogą być konwersje stref czasowych, zwłaszcza gdy są niezamierzone. Zawsze wysyłanie UTC DateTime i konfigurowanie serwerów tak, aby działały w strefie czasowej UTC, może ułatwić Ci życie. Twój kod zaplecza będzie znacznie prostszy i czystszy, ponieważ nie będzie musiał wykonywać żadnych konwersji stref czasowych. DateTime data coming in from servers across the world can be compared and sorted effortlessly.
Code in the back end should be able to assume the time zone of the server to be UTC (but should still have a check in place to be sure). A simple configuration check saves having to think about and code for conversions every time new DateTime code is written.
It's Time for Better Date Handling
Date manipulation is a hard problem. The concepts behind the practical examples in this article apply beyond JavaScript, and are just the beginning when it comes to properly handling DateTime data and calculations. Plus, every helper library will come with its own set of nuances—which is even true of the eventual official standard support{target=”_blank”} for these types of operations.
The bottom line is: Use ISO on the back end, and leave the front end to format things properly for the user. Professional programmers will be aware of some of the nuances, and will (all the more decidedly) use well-supported DateTime libraries on both the back end and the front end. Built-in functions on the database side are another story, but hopefully this article gives enough background to make wiser decisions in that context, too.