Kurs JavaScript i TypeScript · Moduł 5: Zaawansowany JavaScript

Obiekt Date i praca z czasem

10 min czytania
W tej lekcji7

W centrum dowodzenia Parku Jurajskiego, Ray Arnold monitoruje harmonogramy karmienia dinozaurów. "Czas to kluczowy element bezpieczeństwa," wyjaśnia. "Muszę wiedzieć dokładnie kiedy każdy dinozaur był karmiony, kiedy będzie następne karmienie, i ile czasu minęło od ostatniej inspekcji ogrodzenia." Czas w JavaScripcie potrafi jednak zaskoczyć: czerwiec ma tu numer 5, a ta sama data zapisana na dwa sposoby może wskazać różne godziny. Rozbrójmy te pułapki po kolei.

Tworzenie obiektów Date

Obiekt Date tworzysz konstruktorem new Date(). Bez argumentów dostajesz bieżący moment, a z liczbami konkretną datę złożoną z roku, miesiąca i dnia:

1// Aktualna data i czas
2const now = new Date();
3console.log(now); // np. "Sat Jun 15 2024 14:30:00 GMT+0200"
4
5// Z konkretną datą (uwaga: miesiące są od 0!)
6const jurassicOpening = new Date(1993, 5, 11); // 11 czerwca 1993
7console.log(jurassicOpening);
8
9// Z pełną datą i czasem
10const incident = new Date(1993, 5, 11, 20, 30, 0);
11// 11 czerwca 1993, 20:30:00

Zwróć uwagę na piątkę: miesiące liczone są od zera, więc styczeń to 0, a czerwiec 5. Dni miesiąca liczą się już normalnie, od 1. To najczęstsza pułapka Date.

Datę odtworzysz też z tekstu albo z liczby milisekund, czyli znacznika czasu (timestamp):

1// Z ciągu tekstowego (ISO 8601)
2const dateFromString = new Date("2024-06-15T14:30:00");
3// Sama data bez godziny jest traktowana jako UTC!
4const dateOnly = new Date("2024-06-15");
5
6// Z timestampa (milisekundy od 1 stycznia 1970)
7const fromTimestamp = new Date(1718451000000);
8const epoch = new Date(0); // 1 stycznia 1970, 00:00:00 UTC
9
10// Timestamp obecnego momentu
11const timestamp = Date.now();
12console.log(timestamp); // np. 1718451000000

Tekst z datą i godziną, ale bez strefy, jest czytany jako czas lokalny, a sama data - jako północ UTC. W Polsce dateOnly pokaże więc 1 albo 2 w nocy, zależnie od pory roku. Date.now() zwraca liczbę milisekund od 1 stycznia 1970, a nie obiekt Date.

Pobieranie składników daty

Gdy data już istnieje, metody get... wyciągają z niej pojedyncze składniki - rok, miesiąc, dzień czy godzinę:

1const feedingTime = new Date("2024-06-15T08:30:00");
2
3// Pobieranie składników
4console.log(feedingTime.getFullYear());    // 2024
5console.log(feedingTime.getMonth());       // 5 (czerwiec - miesiące od 0!)
6console.log(feedingTime.getDate());        // 15 (dzień miesiąca)
7console.log(feedingTime.getDay());         // 6 (sobota - dni od 0, niedziela = 0)
8console.log(feedingTime.getHours());       // 8
9console.log(feedingTime.getMinutes());     // 30
10console.log(feedingTime.getSeconds());     // 0
11console.log(feedingTime.getMilliseconds()); // 0
12
13// Timestamp
14console.log(feedingTime.getTime()); // milisekundy od 1970
15
16// Wersje UTC
17console.log(feedingTime.getUTCHours()); // godzina w UTC

getDate() zwraca dzień miesiąca, a getDay() dzień tygodnia liczony od niedzieli jako 0 - łatwo je pomylić. Metody pracują w strefie czasowej komputera, a wersje z UTC w nazwie w czasie uniwersalnym.

Ustawianie składników daty

Metody set... działają odwrotnie i zmieniają datę w miejscu, bo obiekt Date jest mutowalny:

1const nextFeeding = new Date();
2
3// Ustawianie składników
4nextFeeding.setFullYear(2024);
5nextFeeding.setMonth(6);          // Lipiec (miesiące od 0!)
6nextFeeding.setDate(20);
7nextFeeding.setHours(9);
8nextFeeding.setMinutes(0);
9nextFeeding.setSeconds(0);
10
11// Ustawienie wielu naraz
12nextFeeding.setFullYear(2024, 6, 20); // rok, miesiąc, dzień
13
14console.log(nextFeeding);

setFullYear() przyjmuje od razu rok, miesiąc i dzień. Settery same przenoszą nadmiar: setDate(32) w czerwcu da 2 lipca, a dodanie godzin przez setHours() przejdzie przez północ do kolejnego dnia.

Formatowanie daty

Wbudowane metody

Do wyświetlania daty obiekt Date ma kilka gotowych metod. Część daje stały format, a część zależy od ustawień języka:

1const event = new Date("2024-06-15T14:30:00");
2
3// Różne formaty
4console.log(event.toString());
5// "Sat Jun 15 2024 14:30:00 GMT+0200 (Central European Summer Time)"
6
7console.log(event.toDateString());
8// "Sat Jun 15 2024"
9
10console.log(event.toTimeString());
11// "14:30:00 GMT+0200 (Central European Summer Time)"
12
13console.log(event.toISOString());
14// "2024-06-15T12:30:00.000Z" (UTC)
15
16console.log(event.toLocaleDateString());
17// "15.06.2024" (zależy od locale)
18
19console.log(event.toLocaleTimeString());
20// "14:30:00" (zależy od locale)
21
22console.log(event.toLocaleString());
23// "15.06.2024, 14:30:00"

toISOString() zawsze podaje czas UTC, dlatego 14:30 w polskim lecie staje się 12:30 z literą Z. Wyniki metod toLocale... zależą od języka przeglądarki - komentarze pokazują polskie ustawienia.

Intl.DateTimeFormat - zaawansowane formatowanie

Pełną kontrolę nad formatem daje Intl.DateTimeFormat. Podajesz język i opcje, a formatter sam zadba o nazwy dni i miesięcy:

1const date = new Date("2024-06-15T14:30:00");
2
3// Polski format
4const plFormatter = new Intl.DateTimeFormat('pl-PL', {
5  weekday: 'long',
6  year: 'numeric',
7  month: 'long',
8  day: 'numeric'
9});
10console.log(plFormatter.format(date));
11// "sobota, 15 czerwca 2024"
12
13// Format z czasem
14const fullFormatter = new Intl.DateTimeFormat('pl-PL', {
15  dateStyle: 'full',
16  timeStyle: 'short'
17});
18console.log(fullFormatter.format(date));
19// "sobota, 15 czerwca 2024 14:30"
20
21// Krótki format
22const shortFormatter = new Intl.DateTimeFormat('pl-PL', {
23  dateStyle: 'short'
24});
25console.log(shortFormatter.format(date));
26// "15.06.2024"
27
28// Skrót: toLocaleDateString() przyjmuje te same opcje
29console.log(date.toLocaleDateString('pl-PL', { weekday: 'long' }));
30// "sobota"

Formatter tworzysz raz i używasz wielokrotnie. dateStyle i timeStyle to gotowe zestawy opcji, a toLocaleDateString() przyjmuje te same opcje do jednorazowego formatu.

Obliczenia na datach

Różnica między datami

Odejmowanie dwóch dat daje liczbę milisekund, bo JavaScript zamienia każdą datę na jej znacznik czasu:

1const lastFeeding = new Date("2024-06-15T08:00:00");
2const now = new Date("2024-06-15T14:30:00");
3
4// Różnica w milisekundach
5const diffMs = now - lastFeeding;
6console.log(diffMs); // 23400000
7
8// Konwersja na różne jednostki
9const diffSeconds = diffMs / 1000;
10const diffMinutes = diffMs / (1000 * 60);
11const diffHours = diffMs / (1000 * 60 * 60);
12const diffDays = diffMs / (1000 * 60 * 60 * 24);
13
14console.log(`Minęło ${diffHours} godzin od ostatniego karmienia`);
15// "Minęło 6.5 godzin od ostatniego karmienia"
16
17// Porównania: < i > działają, === porównuje obiekty
18console.log(lastFeeding < now); // true
19console.log(new Date("2024-06-15T08:00:00") === lastFeeding); // false
20console.log(new Date("2024-06-15T08:00:00").getTime() === lastFeeding.getTime()); // true

Dzielenie przez 1000, 60 i 60 zamienia milisekundy na godziny. Operatory < i > porównują daty poprawnie, ale === sprawdza, czy to ten sam obiekt - osobne daty porównuj przez getTime().

Dodawanie/odejmowanie czasu

Czas dodajesz, doliczając milisekundy do znacznika czasu albo przesuwając składnik setterem. Funkcja pomocnicza zamyka oba warianty w jednym miejscu:

1const baseDate = new Date("2024-06-15T08:00:00");
2
3// Dodaj 3 godziny
4const plus3Hours = new Date(baseDate.getTime() + 3 * 60 * 60 * 1000);
5console.log(plus3Hours.toLocaleTimeString()); // "11:00:00"
6
7// Dodaj 7 dni
8const plus7Days = new Date(baseDate);
9plus7Days.setDate(plus7Days.getDate() + 7);
10console.log(plus7Days.toLocaleDateString()); // "22.06.2024"
11
12// Odejmij 1 miesiąc
13const minus1Month = new Date(baseDate);
14minus1Month.setMonth(minus1Month.getMonth() - 1);
15console.log(minus1Month.toLocaleDateString()); // "15.05.2024"
16
17// Funkcja pomocnicza do dodawania czasu
18function addTime(date, amount, unit) {
19  const result = new Date(date);
20
21  switch(unit) {
22    case 'seconds':
23      result.setSeconds(result.getSeconds() + amount);
24      break;
25    case 'minutes':
26      result.setMinutes(result.getMinutes() + amount);
27      break;
28    case 'hours':
29      result.setHours(result.getHours() + amount);
30      break;
31    case 'days':
32      result.setDate(result.getDate() + amount);
33      break;
34    case 'months':
35      result.setMonth(result.getMonth() + amount);
36      break;
37    case 'years':
38      result.setFullYear(result.getFullYear() + amount);
39      break;
40  }
41
42  return result;
43}
44
45console.log(addTime(baseDate, 2, 'hours').toLocaleTimeString()); // "10:00:00"

Każdy przykład zaczyna od kopii new Date(baseDate), więc baseDate zostaje nietknięta. Do dni i miesięcy polecam settery, bo milisekundy nie wiedzą nic o zmianie czasu, gdy doba ma 23 lub 25 godzin.

Praktyczne przykłady w Parku Jurajskim

1. System harmonogramu karmienia

Pierwszy harmonogram trzyma w Map konkretne godziny karmienia i liczy, ile czasu zostało do posiłku:

1class FeedingScheduler {
2  constructor() {
3    this.schedule = new Map();
4  }
5
6  scheduleFeed(dinoId, feedingTime) {
7    this.schedule.set(dinoId, {
8      scheduledTime: new Date(feedingTime),
9      status: 'pending'
10    });
11  }
12
13  getNextFeeding(dinoId) {
14    const feeding = this.schedule.get(dinoId);
15    if (!feeding) return null;
16
17    const now = new Date();
18    const timeUntil = feeding.scheduledTime - now;
19
20    if (timeUntil < 0) {
21      return { status: 'overdue', overdue: this.formatDuration(-timeUntil) };
22    }
23
24    return {
25      status: 'scheduled',
26      timeUntil: this.formatDuration(timeUntil),
27      scheduledTime: feeding.scheduledTime.toLocaleString('pl-PL')
28    };
29  }
30
31  formatDuration(ms) {
32    const hours = Math.floor(ms / (1000 * 60 * 60));
33    const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
34
35    if (hours > 0) {
36      return `${hours}h ${minutes}min`;
37    }
38    return `${minutes}min`;
39  }
40}
41
42const scheduler = new FeedingScheduler();
43scheduler.scheduleFeed('TREX-001', '2024-06-15T09:00:00');
44console.log(scheduler.getNextFeeding('TREX-001'));

Gdy termin minął, różnica dat jest ujemna - stąd status overdue, który zobaczysz przy dacie z przykładu. formatDuration() rozbija milisekundy na godziny i minuty operatorem reszty %.

2. Harmonogram co kilka godzin

Drugi wariant zna tylko odstęp między posiłkami. Następny termin to czas ostatniego karmienia plus odstęp w milisekundach:

1class FeedingScheduler {
2  constructor() {
3    this.schedules = new Map();
4  }
5
6  addSchedule(dinoId, feedingIntervalHours) {
7    this.schedules.set(dinoId, {
8      lastFed: new Date(),
9      interval: feedingIntervalHours,
10      nextFeeding: new Date(Date.now() + feedingIntervalHours * 3600000)
11    });
12  }
13
14  getNextFeeding(dinoId) {
15    const schedule = this.schedules.get(dinoId);
16    if (!schedule) return null;
17    return schedule.nextFeeding;
18  }
19
20  isOverdue(dinoId) {
21    const schedule = this.schedules.get(dinoId);
22    if (!schedule) return false;
23    return new Date() > schedule.nextFeeding;
24  }
25
26  getOverdueDinosaurs() {
27    const overdue = [];
28    const now = new Date();
29
30    for (const [dinoId, schedule] of this.schedules) {
31      if (now > schedule.nextFeeding) {
32        const overdueHours = (now - schedule.nextFeeding) / 3600000;
33        overdue.push({
34          dinoId,
35          overdueBy: `${overdueHours.toFixed(1)} godz.`,
36          nextFeeding: schedule.nextFeeding
37        });
38      }
39    }
40
41    // Najdłużej czekający dinozaur na początku listy
42    return overdue.sort((a, b) => a.nextFeeding - b.nextFeeding);
43  }
44
45  recordFeeding(dinoId) {
46    const schedule = this.schedules.get(dinoId);
47    if (!schedule) return false;
48
49    const now = new Date();
50    schedule.lastFed = now;
51    schedule.nextFeeding = new Date(now.getTime() + schedule.interval * 3600000);
52
53    console.log(`${dinoId} nakarmiony o ${now.toLocaleTimeString()}. Następne karmienie: ${schedule.nextFeeding.toLocaleTimeString()}`);
54    return true;
55  }
56}

isOverdue() porównuje daty zwykłym >, a getOverdueDinosaurs() przechodzi po mapie pętlą for...of i sortuje wynik od najdłużej czekających. recordFeeding() odlicza nowy odstęp od chwili karmienia.

Każdy gatunek dostaje własny rytm karmienia:

1// Użycie
2const scheduler = new FeedingScheduler();
3scheduler.addSchedule("TREX-001", 8);   // T-Rex co 8 godzin
4scheduler.addSchedule("RAPTOR-01", 6);  // Raptor co 6 godzin
5scheduler.addSchedule("BRACHIOSAURUS", 4); // Roślinożerca co 4 godziny
6
7const overdue = scheduler.getOverdueDinosaurs();
8if (overdue.length > 0) {
9  console.log("Spóźnione karmienia:", overdue);
10}

Zaraz po dodaniu nikt nie jest spóźniony, więc nic się nie wypisze - spóźnienia pojawią się, gdy minie odstęp bez recordFeeding().

3. Względne formatowanie czasu

Zwiedzających nie interesuje znacznik czasu, tylko komunikat w stylu "5 minut temu". Można go policzyć ręcznie albo oddać tę pracę Intl.RelativeTimeFormat:

1function formatRelativeTime(date) {
2  const now = new Date();
3  const diff = now - new Date(date);
4
5  const seconds = Math.floor(diff / 1000);
6  const minutes = Math.floor(seconds / 60);
7  const hours = Math.floor(minutes / 60);
8  const days = Math.floor(hours / 24);
9
10  if (seconds < 60) return 'przed chwilą';
11  if (minutes < 60) return `${minutes} min temu`;
12  if (hours < 24) return `${hours} godz. temu`;
13  if (days < 7) return `${days} dni temu`;
14
15  return new Date(date).toLocaleDateString('pl-PL');
16}
17
18// Lub z użyciem Intl.RelativeTimeFormat
19function formatRelativeTimeIntl(date) {
20  const rtf = new Intl.RelativeTimeFormat('pl', { numeric: 'auto' });
21  const diff = new Date(date) - new Date();
22
23  const seconds = Math.round(diff / 1000);
24  const minutes = Math.round(seconds / 60);
25  const hours = Math.round(minutes / 60);
26  const days = Math.round(hours / 24);
27
28  if (Math.abs(seconds) < 60) return rtf.format(seconds, 'second');
29  if (Math.abs(minutes) < 60) return rtf.format(minutes, 'minute');
30  if (Math.abs(hours) < 24) return rtf.format(hours, 'hour');
31  return rtf.format(days, 'day');
32}
33
34console.log(formatRelativeTime(new Date(Date.now() - 3600000))); // "1 godz. temu"
35
36// Sam formatter: numeric: 'auto' zamienia liczby na słowa
37const rtf = new Intl.RelativeTimeFormat('pl', { numeric: 'auto' });
38console.log(rtf.format(-1, 'day'));  // "wczoraj"
39console.log(rtf.format(-3, 'hour')); // "3 godziny temu"
40console.log(rtf.format(2, 'day'));   // "pojutrze"
41console.log(rtf.format(1, 'week'));  // "w przyszłym tygodniu"

Ręczna wersja zna tylko jeden język i sztywne końcówki, a Intl.RelativeTimeFormat sam odmienia słowa. Opcja numeric: 'auto' zamienia minus jeden dzień na "wczoraj", a plus dwa dni na "pojutrze".

Podsumowanie

Ray Arnold kończy szkolenie: "W systemach krytycznych jak Park Jurajski, precyzyjne zarządzanie czasem to kwestia życia i śmierci. Pamiętajcie:"

  1. Miesiące są od 0 - styczeń to 0, grudzień to 11
  2. Używajcie ISO 8601 do przechowywania dat ("2024-06-15T14:30:00Z")
  3. Intl.DateTimeFormat dla internacjonalizacji
  4. Unikajcie manipulacji stringami - używajcie metod Date
  5. Rozważcie biblioteki (date-fns, dayjs) dla złożonych operacji

Moja rada: zapisuj daty w UTC, a na czas lokalny zamieniaj je dopiero przy wyświetlaniu. Powstaje też nowe API Temporal, projektowane jako pełny następca Date - zanim po nie sięgniesz, sprawdź jego obsługę na MDN. W następnej lekcji zapiszesz dane w localStorage, skąd daty wracają jako zwykłe napisy.

Pamiętaj: zegar parku liczy miesiące od zera, a czas w milisekundach - pilnuj obu, a żaden dinozaur nie przegapi karmienia.

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co zwraca Date.now() w JavaScript?

  2. 2. Jaka jest ważna pułapka przy pracy z miesiącami w obiekcie Date?

Zadania praktyczne w grze

  • Edytor kodu

    Stwórz harmonogram karmienia z obliczaniem następnego czasu karmienia i sprawdzaniem opóźnień.

Przydatne artykuły