> ## Content Index
> Fetch the complete content index at: https://lsdev.pl/llms.txt
> Use this file to discover other available public pages before exploring further.

# Przetwarzanie daty i czasu w języku Java i pułapki z tym związane
- URL: https://lsdev.pl/posts/przetwarzanie-daty-i-czasu-w-jezyku-java/
- Published: 2022-11-13T03:52:34.000Z
- Updated: 2022-11-14T01:11:09.000Z
- Author: Łukasz Sakowicz
- Tags: Java, Programming, #Import 2026-08-18 22:07

Przetwarzanie daty i czasu w aplikacjach zawsze sprawiało programistom wiele trudności gdyż trzeba wziąć wiele rzeczy pod uwagę. Wystarczy zerknąć na tę [stronę](https://yourcalendricalfallacyis.com/?ref=lsdev.pl) aby uzmysłowić sobie ile potencjalnych błędnych założeń możemy popełnić. Mnogość różnych formatów zapisu daty i czasu, strefy czasowe, zmiany czasu z letniego na zimowy i odwrotnie oraz inne tego typu aspekty powodują, że łatwo jest strzelić sobie w stopę. Na domiar złego wszystko komplikuje ekosystem Javy, która udostępnia kilka różnych interfejsów i mechanizmów operacji na datach, przez co szczególnie początkujący mogą poczuć się zagubieni i nieświadomi potencjalnych problemów.

W tym artykule wyjaśnię w jaki sposób przetwarzać, zapisywać i testować czas w języku Java przy okazji wspominając o potencjalnych problemach, które możemy napotkać.

## Legacy Api

W języku Java mamy do dyspozycji wiele klas i interfejsów wspierających przetwarzanie dat i czasu. Gdy Java wyszła na świat w pierwszej wersji otrzymaliśmy klasę *java.util.Date*, która reprezentuję punkt w czasie z milisekundową dokładnością.

```java
Date now = new Date();
Date tomorrow = new Date(now.getTime() + 1000 * 3600 * 24);
System.out.println(now.getMonth() + 1); // print month
System.out.println(now.getYear() + 1900); // print year
if (now.before(tomorrow)) {
	System.out.println("now is before tomorrow");
}
if (tomorrow.after(now)) {
	System.out.println("tomorrow is after now");
}
if (now.compareTo(now) == 0) {
	System.out.println("now is equal to now");
}

```

Jak widać powyżej API nie jest intuicyjne i nie należy do najprostszych. Wyciąganie takich składowych jak rok czy miesiąc lub formatowanie jest nieporęczne nie wspominając o internacjonalizacji czy strefach czasowych. Obecnie większość metod tej klasy jest oznaczona jako *@Deprecated* a sama klasa nie jest zalecana do użycia.

Rozwiązaniem powyższych problemów miała być klasa *Calendar*, którą otrzymaliśmy w wersji 1.1 wraz z klasami odpowiedzialnymi za formatowanie (*DateFormat*, *SimpleDateFormat*) i strefy czasowe (*TimeZone*, *SimpleTimeZone*).

```java
// Calendar with default timzone
Calendar calendar = Calendar.getInstance();

// Calendar with specific timezone
Calendar calendarWithZone = Calendar.getInstance(TimeZone.getTimeZone("Europe/Warsaw"));

// get date object
Date now = calendar.getTime();

// set time object
Date later = new Date();
calendar.setTime(later);

// get date components
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int millis = calendar.get(Calendar.MILLISECOND);

// add 24 hours
calendar.add(Calendar.HOUR, 24);

// Internationalization
Locale locale = new Locale("pl", "PL");
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.FULL, locale);
System.out.println(dateFormat.format(later)); // czwartek, 3 listopada 2022

```

API jest bardziej czytelne i intuicyjne jednak obecnie również niezalecane do wykorzystywania. Interfejs był projektowany gdy w specyfikacji języka nie było jeszcze typu wyliczeniowego więc łatwo o błąd w przypadku stałych numerycznych - nie możemy liczyć na błąd kompilacji. Co więcej klasa *Calendar* jest narzutem na wydajność i zużywa sporo pamięci ponieważ wewnętrznie przechowuje stan na dwa różne sposoby.

Kolejnym problemem dla omówionych powyżej API jest bezpieczeństwo w przypadku przetwarzania współbieżnego tj. na wielu wątkach. Z uwagi na to, że klasy *Date* jak i *Calendar* mogą zmieniać stan (mutable) łatwo o błąd gdy dojdzie do wyścigu w związku z tym niezbędna jest synchronizacja lub używanie mechanizmu [*ThreadLocal*](https://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html?ref=lsdev.pl).

Istnieją również klasy rozszerzające klasę *java.util.Date* (wrappery) m.in. *java.sql.Date*, *java.sql.Time*, *java.sql.Timestamp* stworzone na potrzeby [*jdbc-api*](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/?ref=lsdev.pl)*.* Mają one zastosowanie gdy integrujemy się z bazą danych w związku z tym nie powinniśmy ich używać w kodzie domenowym a jedynie do odczytu/zapisu czy też mapowania relacyjno-obiektowego.

## Nowe API od Java 8

W wersji 8 doczekaliśmy się nowego API związanego z obsługą dat i czasu kojarzone również jako [JSR-310](https://jcp.org/en/jsr/detail?id=310&ref=lsdev.pl). Nowa specyfikacja jest o wiele bardziej intuicyjna i pozwala zastąpić stare API omówione w poprzednim punkcie jak i inne zewnętrzne biblioteki usprawniające prace z czasem np.[ joda-time](https://www.joda.org/joda-time?ref=lsdev.pl) \- najczęściej używana przed wydaniem tej wersji.

Nowe API zostało usytuowane w pakiecie *java.time* i zawiera m.in. następujące klasy:

[**Insant**](https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html?ref=lsdev.pl) **\-** Reprezentuje określony punkt w czasie z nanosekundową precyzją.

[**Duration**](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html?ref=lsdev.pl) **\-** Odcinek czasu z nanosekundową precyzją np. 34.5 sekundy.

[**LocalDateTime**](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html?ref=lsdev.pl) **\-** Data i czas bez strefy czasowej w standardzie ISO-8601 z nanosekundową precyzją  
np. 2022-12-03T14:15:30.

[**LocalDate**](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html?ref=lsdev.pl) **\-** Data bez strefy czasowej w standardzie ISO-8601 np. 2022-10-03.

[**LocalTime**](https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html?ref=lsdev.pl) **\-** Czas bez strefy czasowej w standardzie ISO-8601 z nanosekundową precycją np. 12:30:45.

[**ZonedDateTime**](https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html?ref=lsdev.pl) **\-** Data i czas z strefą czasową w standardzie ISO-8601 z nanosekundową precyzją  
np. 2022-11-14T14:15:30+01:00 Europe/Warsaw.

[**ZoneId**](https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html?ref=lsdev.pl) **\-** Identyfikator strefy czasowej np. Europe/Warsaw.

[**Year**](https://docs.oracle.com/javase/8/docs/api/java/time/Year.html?ref=lsdev.pl) **\-** Rok w standardzie ISO-8601 np. 2022.

[**MonthDay**](https://docs.oracle.com/javase/8/docs/api/java/time/MonthDay.html?ref=lsdev.pl) **\-** Miesiąc i dzień w standardzie ISO-8601 np. 11-28.

[**YearMonth**](https://docs.oracle.com/javase/8/docs/api/java/time/YearMonth.html?ref=lsdev.pl) **\-** Rok i miesiąc w standardzie ISO-8601 np. 2022-12.

[**ZoneOffset**](https://docs.oracle.com/javase/8/docs/api/java/time/ZoneOffset.html?ref=lsdev.pl) **\-** Offset od czasu Greenwich/UTC z sekundową precyzją np. +02:00.

[**OffsetDateTime**](https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html?ref=lsdev.pl) **\-** Data i czas z przesunięciem od czasu Greenwich/UTC w standardzie ISO-8601  
z nanosekundową precyzją np. 2022-11-06T12:15:30+01:00.

[**OffsetTime**](https://docs.oracle.com/javase/8/docs/api/java/time/OffsetTime.html?ref=lsdev.pl) **\-** Czas z przesunięciem od czasu Greenwich/UTC w standardzie ISO-8601  
z nanosekundową precyzją np. 11:15:30+01:00.

[**Period**](https://docs.oracle.com/javase/8/docs/api/java/time/Period.html?ref=lsdev.pl) **\-** Odcinek czasu jako ilość lat, miesięcy i dni w standardzie ISO-8601 np.  
'2 years, 3 months and 4 days'.

[**Clock**](https://docs.oracle.com/javase/8/docs/api/java/time/Clock.html?ref=lsdev.pl) **\-** Pozwala na dostęp do aktualnego czasu i strefy czasowej.

Klas jest dość sporo więc na początku może być trudno się zorientować, których użyć w jakim przypadku natomiast niebagatelną zaletą tego jest to, że każda klasa ma swoje przeznaczenie zgodnie z zasadą [SRP](https://en.wikipedia.org/wiki/Single-responsibility%5Fprinciple?ref=lsdev.pl) w przeciwieństwie do omawianej wcześniej klasy *Date*. Warto również zwrócić uwagę na to, że każda z klas jest *immutable & thread safe* co z pewnością wpływa na bezpieczeństwo i jakość oprogramowania.

Zanim przejdę do omówienia najważniejszych klas bardziej szczegółowo warto zerknąć na obrazek poniżej, który jest pomocny w odnalezieniu się w tym gąszczu klas.

![](https://lsdev.pl/content/images/2022/11/javatime.webp)

## Instant

Reprezentuje określony moment w czasie. Początek został wyznaczony na północ 1 stycznia 1970 roku. Począwszy od tego punktu czas jest mierzony w sekundach po 86400 sekund na dobę w tył oraz w do przodu z dokładnością do nanosekund. Wartości klasy *Instant* mogą określać czas sięgający miliarda lat wstecz (*Instant.MIN*). *Instant.MAX* odpowiada dacie 31 grudnia 1 000 000 000, jeżeli przekroczymy tę wartość otrzymamy wyjątek *DateTimeException*.

```java
@Test
void testInstant() {
	Instant now = Instant.now();
	Instant later = now.plusSeconds(3600);
	System.out.println(now + ", " + later); // 2022-11-05T17:56:40.669420Z, 2022-11-05T18:56:40.669420Z
	assertTrue(later.isAfter(now));
}

```

*Instant* idealnie nadaje się do rejestrowania czasu różnego rodzaju zdarzeń lub logów.

## Duration

Reprezentuje odcinek czas pomiędzy dwoma punktami w sekundach z dokładnością do nanosekund gdzie 24h = 86400 sekund. Długość *Duration* możemy wygodnie pobierać używając takich metod jak *toNanos*, *toMillis*, *toSeconds*, *toMinutes*, *toHours*, *toDays*. Przydaje się w wyliczaniu różnego rodzaju odległości w czasie.

```java
@Test
void testDuration() {
	Instant now = Instant.now();
	Instant later = now.plus(4, ChronoUnit.HOURS);
	var duration = Duration.between(now, later);
	assertEquals(4, duration.toHours());
}

```

Używając *Duration* można również w łatwy sposób „skakać”   
w czasie definiując odpowiednią długość w czasie.

```java
Instant now = Instant.now();
Instant tomorrow = now.plus(Duration.ofDays(1));

```

Warto wiedzieć, że klasa Duration obsługuje parsowanie w standardzie ISO-8601 w związku z tym można przekazać do metody odpowiedni łańcuch znaków. Dzięki temu możemy w łatwy sposób wyrazić odległość w czasie za pomocą jednej zmiennej, która może być wyniesiona do jakiejś zewnętrznej konfiguracji.

```java
Duration duration = Duration.parse("P2DT3H4M"); // 2 days, 3 hours and 4 minutes

```

## LocalDateTime

Zawiera informację o dacie oraz czasie z nanosekundową precyzją natomiast nie udostępnia żadnych informacji na temat strefy czasowej. Z tego względu czas wyrażony z użyciem *LocalDateTime* nie może być rozpatrywany jako bezwględny ponieważ może być inny w zależności od tego gdzie się znajdujemy w przeciwieństwie do *Instant*, który jest wyliczany bez względu na strefę czasową.

```java
LocalDateTime now = LocalDateTime.now();

```

Domyślnie lokalna data jest wyliczana na podstawie domyślnej strefy czasowej ustawionej w systemie lub na maszynie wirtualnej. Warto pamiętać, że w przypadku *LocalDateTime* nie należy polegać na systemowej strefie czasowej  
gdyż wyniki mogą być różne w zależności od tego gdzie zostanie zainstalowana nasza aplikacja. Na szczęście mamy nad tym kontrolę i możemy do metody statycznej przekazać strefę czasową, na podstawie której zostanie wygenerowana data.

```java
LocalDateTime now = LocalDateTime.now(ZoneId.of("Europe/Warsaw"));

```

Jeżeli chcemy globalnie ustawić lokalną strefę czasową dla aplikacji możemy przekazać odpowiednią wartość do maszyny wirtualnej w następujący sposób.

```java
java -Duser.timezone="Europe/Warsaw" -jar app.jar

```

## LocalTime

Zawiera informację o lokalnym czasie z pominięciem strefy czasowej z nanosekundową precyzją i nie zwraca uwagi na podział AM/PM, tym zajmują się mechanizmy odpowiedzialne za formatowanie.

Utworzenie obiektu tej klasy sprowadza się do pobrania aktualnego czasu poprzez *LocalTime.now()* lub z góry ustalonego czasu *LocalTime.of(hour, minute, second, nanoOfSecond)* gdzie:

- *hour* \- liczba z zakresu 0-23
- *minute* \- liczba z zakresu 0-59
- *second* \- liczba z zakresu 0-59
- *nanoOfSecond* \- liczba z zakresu 0-999 999 999

```java
@Test
void localTimeTest() {
	LocalTime now = LocalTime.now(ZoneId.of("Europe/Warsaw"));
	LocalTime later = now.plusHours(4);
	assertTrue(later.isAfter(now));
	assertEquals(4, Duration.between(now, later).toHours());	
}

```

## LocalDate

Reprezentuje rok, miesiąc i dzień bez informacji o czasie i strefie czasowej. Idealnie nadaje się np. do zapisania daty urodzin lub innych wydarzeń gdzie czas nie ma znaczenia.

```java
LocalDate birthday = LocalDate.of(2022, 11, 14);

```

Aby obliczyć datę urodzin w przyszłym roku wystarczy użyć wyrażenia w postaci *birthday.plus(Period.ofYears(1))* lub po prostu *birthday.plusYears(1)*. Należy pamiętać, że w roku przestępnym wywołanie w postaci *birthday.plus(Period.ofDays(365))* zwróci nieprawidłowy wynik.

Warto również wspomnieć, że stworzenie daty 29 lutego w roku nieprzestępnym np. *LocalDate.of(2023, 2, 29)* objawi się wyjątkiem typu DateTimeException.

### Jak wyliczyć dzień programisty

Dzień programisty wypada na 256 dzień w roku. Jak go wyliczyć biorąc pod uwagę, że w roku przestępnym jest o jeden dzień więcej? Możemy to zrobić w następujący sposób.

```java
LocalDate programmersDayInCurrentYear = 
				LocalDate.now()
				.withMonth(1)
				.withDayOfMonth(1)
				.plusDays(255);

```

## ZonedDateTime

ZonedDateTime reprezentuje datę i czas wraz z strefą czasową z nanosekundową precyzją. Bazę danych wszystkich stref czasowych na całym świecie przechowuje agencja [IANA](http://www.iana.org/time-zones?ref=lsdev.pl) i aktualizuje ją kilka razy w roku. Java korzysta z tej bazy danych.

Każda strefa czasowa ma swój identyfikator np. *America/New\_York* czy *Europe/Berlin* i aktualizuje ją kilka razy w roku. Listę wszystkich dostępnych stref czasowych możemy pobrać przy użyciu *ZoneId.getAvailableZoneIds()*.

Dysponując tym identyfikatorem można przekształcić obiekt *LocalDateTime* na *ZonedDateTime.*

```java
LocalDateTime now = LocalDateTime.now();
ZonedDateTime znow = now.atZone(ZoneId.of("Europe/Warsaw"));
System.out.println(now); // 2022-11-13T02:38:03.934777
System.out.println(znow); // 2022-11-13T02:38:03.934777+01:00[Europe/Warsaw]

```

Możemy również pobrać aktualny czas na podstawie domyślnej strefy czasowej tj. *ZonedDateTime.now()* lub bezpośrednio przekazać identyfikator strefy *ZonedDateTime.now(ZoneId.of("Europe/Warsaw")).*

Wywołanie *LocalDateTime.atZone(...)* dodaję strefę czasową tworząc obiekt typu *ZonedDateTime* jednak nie zmienia czasu na ten wskazany przez strefę czasową (*LocalDateTime* nie ma informacji o strefie czasowej więc nawet nie byłoby to możliwe). Jeżeli naszym celem jest również zmiana czasu możemy zrobić to w następujący sposób.

```java
LocalDateTime someDate = LocalDateTime.parse("2022-11-10T18:40:00.000"); // time in Europe/Warsaw zone
ZonedDateTime convertedToSpecificZone = ZonedDateTime.ofInstant(
someDate.atZone(ZoneId.of("Europe/Warsaw")).toInstant(),
ZoneId.of("America/New_York"));

```

W przypadku konwersji *ZonedDateTime* \-> *ZonedDateTime* robimy podobnie.

```java
ZonedDateTime someDateWithZone = ZonedDateTime.now(ZoneId.of("Europe/Warsaw"));
ZonedDateTime newYorkTime = ZonedDateTime.ofInstant(someDateWithZone.toInstant(), ZoneId.of("America/New_York"));

```

lub zwięźlej

```java
ZonedDateTime newYorkTime = someDateWithZone.withZoneSameInstant(ZoneId.of("America/New_York"))

```

## OffsetDateTime

Reprezentuję datę i czas wraz z przesunięciem od czasu UTC/Greenwich z nanosekundową precyzją. Upraszczając *OffsetDateTime* jest to *Instant* \+ *ZoneOffset* i w taki też sposób możemy utworzyć obiekt tej klasy.

```java
OffsetDateTime offsetDateTime = Instant.now().atOffset(ZoneOffset.of("+01:00"));
// lub
OffsetDateTime now = OffsetDateTime.now();

```

W przeciwieństwie do klasy *ZonedDateTime* nie mamy tu informacji o strefie czasowej a tym samym o przybliżonej lokalizacji geograficznej, jedynie sztywno zdefiniowany *offset*. Warto o tym pamiętać szczególnie gdy następuje zmiana czasu, różnicę możemy zobaczyć w kodzie poniżej, gdzie w dzień zmiany czasu z letniego na zimowy przesunęliśmy się w czasie o 4 godziny.

```java
LocalDateTime dateTime = LocalDateTime.parse("2022-10-30T00:00:00.000");
ZonedDateTime zonedDateTime = dateTime.atZone(ZoneId.of("Europe/Warsaw")).plusHours(4);
OffsetDateTime offsetDateTime = dateTime.atOffset(ZoneOffset.of("+02:00")).plusHours(4);
System.out.println(zonedDateTime); // 2022-10-30T03:00+01:00[Europe/Warsaw]
System.out.println(offsetDateTime); // 2022-10-30T04:00+02:00

```

## Period

Klasa mierząca odcinek czasu jako ilość lat, miesięcy i dni w odróżnieniu od klasy *Duration* omówionej wcześniej, gdzie najmniejszą jednostką, która mierzy upływ czasu były sekundy/nanosekundy.

Z tego względu Period użyjemy raczej do wyliczenia różnicy między dwoma datami typu *LocalDate.*

```java
var now = LocalDate.parse("2022-11-10");
var later = now.plusDays(64);
var period = Period.between(now, later);
System.out.println(period.getMonths()); // 2
System.out.println(period.getDays()); // 3

```

Warto zwrócić uwagę, że mimo iż powyżej dodaliśmy 64 dni metoda *getDays()* zwraca nam 3 gdyż reszta dni została skonsolidowana do ilości miesięcy.

*Period* różni się także od *Duration* sposobem traktowania czasu letniego/zimowego w przypadku gdy dodajemy czas do *ZonedDateTime*. Jak wspomnieliśmy wcześniej *Duration* traktuje dzień zawsze jako 24h czyli 86400 sekund więc dokładnie o tyle przesuniemy czas w tył lub w przód, w przeciwieństwie do *Period* gdzie koncepcja dnia stara się zachować czas lokalny. Najłatwiej będzie zobaczyć to na przykładzie.

```java
var dateTime = ZonedDateTime.parse("2022-10-29T09:00+02:00[Europe/Warsaw]");
System.out.println(dateTime.plus(Duration.ofDays(7))); // 2022-11-05T08:00+01:00[Europe/Warsaw]
System.out.println(dateTime.plus(Period.ofDays(7))); // 2022-11-05T09:00+01:00[Europe/Warsaw]

```

Ustawiłem datę na dzień przed zmianą czasu z letniego na zimowy. Dodając równo 7 dni do tej daty używając *Duration* godzina również się zmieniła z 9 na 8\. Inaczej jest w przypadku *Period* gdzie godzina została zachowana (w przypadku *LocalDateTime* w obu przypadkach byłaby taka sama godzina tj. 9 gdyż ta klasa nie bierze pod uwagę zmiany czasu). Warto o tym pamiętać gdy np. chcemy zaplanować jakieś spotkanie w przyszłości.

## TimeAdjusters

Często mamy potrzebę wyliczenia pierwszego/ostatniego dnia miesiąca/roku/tygodnia lub innych tego typu wyliczeń. Z pomocą przychodzi klasa [*TemporalAdjusters*](https://docs.oracle.com/javase/8/docs/api/java/time/temporal/TemporalAdjusters.html?ref=lsdev.pl)*.* Przykładowo wyliczenie pierwszego dnia miesiąca może wyglądać następująco*.*

```java
LocalDate firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth());
// lub
LocalDate firstDayOfMonth =  (LocalDate) TemporalAdjusters.firstDayOfMonth().adjustInto(now);

```

Dostępnych jest wiele innych modyfikatorów m.in.

```java
TemporalAdjusters.firstDayOfMonth();
TemporalAdjusters.lastDayOfMonth();
TemporalAdjusters.firstDayOfYear();
TemporalAdjusters.firstDayOfNextMonth();
TemporalAdjusters.firstDayOfNextYear();
TemporalAdjusters.firstInMonth(DayOfWeek.SATURDAY);
TemporalAdjusters.previous(DayOfWeek.FRIDAY);

```

## Formatowanie, parsowanie i wyświetlanie

Aplikacje nad którymi pracujemy, często wymagają wyświetlenia a tym samym formatowania i parsowania daty i czasu na różne sposoby - czasami celem jest serializacja i przesłanie wartości przez sieć a czasem po prostu zwykłe wyświetlenie.

W najprostszym przypadku możemy skorzystać z domyślnego formatu tj. zgodnego z standardem ISO-8601 wywołując metodę *toString()* na obiekcie daty.

```java
String now = LocalDate.now().toString();
System.out.println(now); // 2022-11-11
LocalDate localDate = LocalDate.parse(now);

```

Jednak najczęściej to nam nie wystarczy. Chcielibyśmy mieć większą kontrolę nad formatowaniem i parsowaniem czasu. Z pomocą przychodzi klasa *DateTimeFormatter*, która pozwala bardzo precyzyjnie to wyrazić.

Zdefiniowanie formatu sprowadza się do stworzenia   
obiektu tej klasy na podstawie odpowiedniego wzorca.

```java
LocalDate localDate = LocalDate.parse("2022-10-14");
var formatter = DateTimeFormatter.ofPattern("dd LLLL, yyyy");
String formatted = localDate.format(formatter); // lub formatter.format(localDate);
System.out.println(formatted); // 14 October, 2022

```

W celu wyświetlenia daty w rodzimym języku podczas   
tworzenia *formattera* należy przekazać odpowiedni *Locale*.

```java
LocalDate localDate = LocalDate.parse("2022-10-14");
Locale pl = new Locale("pl", "PL");
var formatter = DateTimeFormatter.ofPattern("dd LLLL, yyyy", pl);
System.out.println(localDate.format(formatter)); // 14 październik, 2022

```

Istnieje również wiele predefiniowanych formatów zgodnych   
z standardem ISO-8601, które możemy wykorzystać m.in.

```java
DateTimeFormatter.ISO_LOCAL_DATE
DateTimeFormatter.ISO_LOCAL_DATE_TIME
DateTimeFormatter.ISO_INSTANT
...

```

## Testowanie

Aby napisane przez nas oprogramowanie było solidne warto zadbać o dobre testy jednostkowe i integracyjne. 

W przypadku gdy testowany przez nas kod pobiera aktualny czas (np. *Instant.now()*) i na jego podstawie wykonuje pewną logikę nasz kod staje się trudno testowalny, gdyż nie mamy kontroli nad upływającym czasem, który za każdym uruchomieniem testu będzie inny.

Z pomocą przychodzi klasa [*Clock*](https://docs.oracle.com/javase/8/docs/api/java/time/Clock.html?ref=lsdev.pl) i jej pochodne, dzięki którym możemy w prosty sposób skonfigurować źródło, na podstawie którego czas będzie pobierany.

```java
Clock clock = Clock.system(ZoneId.of("Europe/Warsaw"));
Instant instant = Instant.now(clock);

```

Jeżeli korzystamy z mechanizmu wstrzykiwania zależności np. w Spring'u możemy pójść o krok dalej i stworzyć komponent, który będzie wstrzykiwany tam gdzie nastąpi potrzeba pobierania czasu systemowego.

```java
@Configuration
class AppConfig {

	@Bean
	public Clock clock() {
		return Clock.system(ZoneId.of("Europe/Warsaw"));
	}
}

@Service
class SomeService {

	private final Clock clock;

	public SomeService(Clock clock) {
		this.clock = clock;
	}

	public String bar() {
        return "Hello world " + Instant.now(clock);
    }
}

```

Następnie możemy przejść do przetestowania funkcjonalności podmieniając implementację *Clock*'a aby odzyskać kontrolę w testach.

```java
@Test
void barTest() {
    // given
    Clock clock = Clock.fixed(Instant.parse("2022-11-12T10:39:33.307871Z"), ZoneId.of("Europe/Warsaw"));
    var subject = new SomeService(clock);

    // when
    String result = subject.bar();

    // then
    assertEquals("Hello world 2022-11-12T10:39:33.307871Z", result);
}

```

## Pułapki

### Planowanie spotkań

Załóżmy, że chcemy zaplanować spotkanie co tydzień o 14:30 czasu w strefie Europe/Warsaw. W tym celu musimy dodać do ostatniego czasu strefowego 7 dni czyli 7 \* 4 \* 3600 sekund, jednak jeśli przy tym przekroczymy granice zmiany czasu z letniego na zimowy okaże się, że spotkanie wypadnie o godzinę za wcześnie lub za późno.

```java
ZonedDateTime dateTime = ZonedDateTime.of(LocalDateTime.of(2022, 10, 29, 14, 30), ZoneId.of("Europe/Warsaw"));
System.out.println(dateTime.plusSeconds(7 * 24 * 3600)); // 13:30 !!!

```

Z tego powodu projektanci API zalecają aby godzin z informacjami o strefie czasowej używać jedynie wtedy gdy posługiwanie się bezwzględnymi momentami czasu jest absolutnie niezbędne. Urodziny, święta, zaplanowane spotkania najlepiej zapisywać przy użyciu lokalnych dat i godzin.

```java
LocalDateTime localDateTime = LocalDateTime.of(2022, 10, 29, 14, 30);
System.out.println(localDateTime.plusSeconds(7 * 24 * 3600)); // 14:30 OK

```

### Używanie *LocalDateTime* gdy musimy mieć więcej informacji

Jak wiemy klasa *LocalDateTime* nie posiada informacji o żadnym *offsecie* od czasu UTC a tym samym o strefie czasowej. Jeżeli przykładowo tworzymy aplikację, która ma za zadanie przypominać użytkownika o różnych wydarzeniach i datę rozpoczęcia wydarzenia zapiszemy w postaci *LocalDateTime* tak naprawdę utracimy informację o faktycznym czasie rozpoczęcia (chyba, że zakładamy, że używamy tylko jednej globalnej strefy czasowej). Z tego względu warto mieć w takim przypadku dodatkową informację tj. strefę czasową w jakiej operuje użytkownik.

### Błędy związane z formatowaniem

- Używanie "mm" dla miesięcy i "MM" dla minut i odwrotnie
- Używanie "DD" jako dzień miesiąca zamiast roku, w rzeczywistości dzień miesiąca to "dd"
- Używanie "hh" (1-12) jako godzina w danym dniu zamiast "HH" (0-23)
- Używanie "YYYY" do formatowania roku zamiast "yyyy"

## Podsumowanie

Java 8 przyniosła liczne i długo wyczekiwane zmiany w kontekście *date & time* API. Nowe API jest bezpieczniejsze i prostsze w użyciu, z pewnością więc warto jest nim zastąpić poprzednie implementacje. Na koniec kilka dobrych praktyk, które mogą Ci się przydać przy pracy z datami i czasem:

- Unikaj używania konstrukcji daty i czasu, które są zbyt szczegółowe lub zbyt ogólne
- Nie polegaj na systemowej strefie czasowej lub tej ustawionej w bazie danych
- Używaj *Clock* w celu konfiguracji źródła, na podstawie którego czas będzie pobierany
- Staraj się używać formatu w standardzie ISO-8601 w API, które oferujesz
- Używaj *ZonedDateTime* jeśli musisz uwzględniać strefy czasowe i zmiany czasu
- Używaj *Instant* jeśli chcesz zapisać czas wystąpienia jakiegoś zdarzenia jako punkt na osi czasu
- Przechowuj czas w UTC i konwertuj na czas lokalny po stronie frontendu jeżeli musisz wyświetlać daty z uwzględnieniem różnych stref czasowych
- Unikaj używania legacy *date-time* API w Java na rzecz nowego API
- Pamiętaj o walidacji pól wpisywanych przez użytkownika
- Pamiętaj o testach jednostkowych szczególnie przypadków brzegowych tj.  
\- początek/koniec miesiąca/roku  
\- dni powszednie i weekendy  
\- 29 lutego  
\- strefy czasowe, czas letni

Po więcej szczegółowych informacji zachęcam do przejrzenia [dokumentacji](https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html?ref=lsdev.pl) a także do zerknięcia na świetną [prezentację](https://youtu.be/zsfEWLGgsEY?ref=lsdev.pl) autorstwa Tomasza Nurkiewicza. A tymczasem zostawiam Cię z utworem i do następnego 👋

[Manual Music](https://soundcloud.com/manual-music?ref=lsdev.pl "Manual Music") · [FREE DOWNLOAD: Stil Corners - The Trip (Bruno Andrada Unofficial Remix)](https://soundcloud.com/manual-music/stil-corners-the-trip-bruno-andrada-unofficial-remix?ref=lsdev.pl "FREE DOWNLOAD: Stil Corners - The Trip (Bruno Andrada Unofficial Remix)")