Zabawa z datami to odwieczny problem programistów. Nie inaczej ma to miejsce u mnie. Ostatnio musiałem się z tym zmierzyć w ramach swojego projektu Parking Domain. Problem dotyczył klasy ZonedDateTime oraz Instant, a dokładniej utworzenia Instant z określoną godziną. Jak wiemy Instant.now() reprezentuje punkt w czasie określony przez liczbę nanosekund liczoną od 1970-01-01T00:00:00Z. Z tego zapisu najbardziej istotna jest literka Z na końcu tej daty. Oznacza ona nic innego jak Zulu time. Korzystając z API klasy Instant poruszamy się w strefie czasowej UTC±00:00 tak naprawdę o tym nie wiedząc.

Jeśli uruchomimy Instant.now() w Polsce o godzinie 13:11 16 czerwca 2024 to otrzymamy - 2024-06-13T11:11:30.218825200Z. Jest on przedstawiony w strefie UTC uwzględniając przesunięcie.

Utrudnieniem staje się fakt, że na klasie Instant nie ma możliwości zmiany np. godziny. Musimy zrobić pewne obejście wykorzystując do tego ZonedDateTime.

1
2
3
4
5
6
ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())
    .withHour(12)
    .withMinute(0)
    .withSecond(0)
    .withNano(0)
    .toInstant();

Będąc w Polsce spodziewamy się dostać w wyniku godzinę 12 czasu Europe/Warsaw zmienioną na godzinę 10 czasu UTC. I tak się właśnie dzieje - 2024-06-13T10:00:00Z.

Załóżmy teraz, że jesteśmy w strefie UTC i chcemy wykonać tą samą operację.

1
2
3
4
5
6
ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC"))
    .withHour(12)
    .withMinute(0)
    .withSecond(0)
    .withNano(0)
    .toInstant();

Bez niespodzianek, otrzymaliśmy 2024-06-13T12:00:00Z. Czyli dwie godziny później niż to miało miejsce w poprzednim przypadku, ponieważ ustawiliśmy godzinę będąc już w strefie czasowej Zulu. Jednak co jeśli chcielibyśmy w tym drugim przypadku przeskoczyć do strefy czasowej CET? Musimy skorzystać z metody withZoneSameInstant.

1
2
3
4
5
6
7
ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC"))
    .withHour(12)
    .withMinute(0)
    .withSecond(0)
    .withNano(0)
    .withZoneSameInstant(ZoneId.systemDefault())
    .toInstant();

Znowu otrzymaliśmy 2024-06-13T12:00:00Z. Dlaczego? Ponieważ linijka withZoneSameInstant(ZoneId.systemDefault()) zmieniła nam ZonedDateTime wykorzystując czas środkowoeuropejski, ale później skorzystaliśmy z metody toInstant, która ponownie zmieniła nam nasz czas uwzględniając strefę Zulu. Łatwo się w tym pogubić, ja właśnie tak miałem.

Nie ułatwia nam jeszcze kwestia tego, że do dyspozycji mamy również metodę withZoneSameLocal, która zmienia strefę zachowując czas.

1
2
3
4
5
6
7
ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC"))
    .withHour(12)
    .withMinute(0)
    .withSecond(0)
    .withNano(0)
    .withZoneSameLocal(ZoneId.systemDefault())
    .toInstant();

Dla takiego przypadku dostajemy oczywiście 2024-06-13T10:00:00Z. W ten sposób uzmysłowiłem sobie jak ważne jest przekazywanie odpowiedniej daty w samej aplikacji oraz pomiędzy systemami. Warto w odpowiedni sposób taką datę przekonwertować do czasu Greenwich i informować o tym klienty naszej aplikacji, aby nie było żadnych nieporozumień.