Strefy czasowe i zmiana czasu
- Data i czas
- poziom: średni
- aktualizacja: 16 sierpnia 2026
Instant to punkt na osi czasu — ten sam dla wszystkich na świecie. LocalDateTime to sam napis „1 lipca 2026, 10:00”, który bez strefy nie oznacza żadnej konkretnej chwili: w Warszawie i w Nowym Jorku wypada w dwóch momentach oddalonych o sześć godzin. Dopiero ZoneId łączy jedno z drugim. Dwa razy w roku ta układanka pokazuje zęby — przy wiosennej zmianie czasu jedna godzina w ogóle nie istnieje, a jesienią inna pada dwukrotnie. To nie ciekawostka: nocna zmiana rozliczona z tarczy zegara gubi wtedy godzinę pracy. Skrypt wylicza daty przełomów z ZoneRules zamiast wpisywać je na sztywno i pokazuje na żywo, co Java robi z takimi godzinami, gdy jej o nie nie zapytasz.
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneRules;
import java.time.zone.ZoneRulesException;
import java.util.Locale;
/**
* Strefy czasowe i zmiana czasu — skryptyJAVA.pl
* Zapisz jako: StrefyCzasowe.java
*/
public class StrefyCzasowe {
private static final Locale PL = Locale.forLanguageTag("pl-PL");
private static final ZoneId WARSZAWA = ZoneId.of("Europe/Warsaw");
private static final ZoneId NOWY_JORK = ZoneId.of("America/New_York");
/** Wzorzec „xxx” wypisuje przesunięcie (+02:00), a „VV” pełny identyfikator strefy. */
private static final DateTimeFormatter PELNY =
DateTimeFormatter.ofPattern("dd.MM.uuuu HH:mm xxx '('VV')'", PL);
/** Zamienia znacznik zapisany w UTC na czas ścienny wskazanej strefy. */
public static String naZegarze(Instant chwila, ZoneId strefa) {
return chwila.atZone(strefa).format(PELNY);
}
/** Zwraca pierwszą zmianę czasu następującą po podanej chwili; null, gdy strefa ich nie ma. */
public static ZoneOffsetTransition kolejnaZmiana(ZoneId strefa, Instant od) {
return strefa.getRules().nextTransition(od);
}
/** Ile realnych chwil kryje napis daty i godziny: zero w luce, dwie w nakładce, poza tym jedną. */
public static String zbadajGodzine(LocalDateTime lokalna, ZoneId strefa) {
ZoneRules zasady = strefa.getRules();
ZoneOffsetTransition zmiana = zasady.getTransition(lokalna);
if (zmiana == null) {
return "zwykła godzina, jedna chwila, przesunięcie " + zasady.getOffset(lokalna);
}
if (zmiana.isGap()) {
return "ta godzina NIE ISTNIEJE — zegar skacze z "
+ zmiana.getDateTimeBefore().toLocalTime() + " na "
+ zmiana.getDateTimeAfter().toLocalTime();
}
return "ta godzina pada DWA RAZY — najpierw z " + zmiana.getOffsetBefore()
+ ", potem z " + zmiana.getOffsetAfter();
}
/** Sprawdza, czy para „czas lokalny + przesunięcie” opisuje w tej strefie realną chwilę. */
public static boolean czyPrzesunieciePasuje(LocalDateTime lokalna, ZoneOffset przesuniecie,
ZoneId strefa) {
return strefa.getRules().isValidOffset(lokalna, przesuniecie);
}
/** Zwraca dokładny czas pracy po osi czasu, bez obcinania części godziny. */
public static Duration czasPracy(ZonedDateTime poczatek, ZonedDateTime koniec) {
return Duration.between(poczatek.toInstant(), koniec.toInstant());
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) {
// 1. LocalDateTime to tylko napis — dopiero strefa robi z niego punkt na osi czasu.
LocalDateTime napis = LocalDateTime.of(2026, 7, 1, 10, 0);
Instant jakoWarszawa = napis.atZone(WARSZAWA).toInstant();
Instant jakoNowyJork = napis.atZone(NOWY_JORK).toInstant();
System.out.println("Napis bez strefy: " + napis);
System.out.println(" jako Warszawa: " + jakoWarszawa);
System.out.println(" jako Nowy Jork: " + jakoNowyJork);
System.out.println(" dzieli je: "
+ Duration.between(jakoWarszawa, jakoNowyJork).toHours() + " h");
// 2. Jedna chwila widziana z dwóch zegarów.
ZonedDateTime spotkanie = napis.atZone(WARSZAWA);
System.out.println("Spotkanie: " + spotkanie.format(PELNY));
System.out.println(" widziane z NY: " + spotkanie.withZoneSameInstant(NOWY_JORK).format(PELNY));
System.out.println(" w bazie (UTC): " + spotkanie.toInstant());
// 3. Daty zmiany czasu bierzemy z reguł strefy, a nie z pamięci.
Instant poczatekRoku = LocalDateTime.of(2026, 1, 1, 0, 0).atZone(WARSZAWA).toInstant();
ZoneOffsetTransition wiosna = kolejnaZmiana(WARSZAWA, poczatekRoku);
ZoneOffsetTransition jesien = kolejnaZmiana(WARSZAWA, wiosna.getInstant());
LocalDateTime luka = wiosna.getDateTimeBefore().plusMinutes(30);
System.out.println("Wiosna: " + wiosna.getDateTimeBefore() + " -> " + wiosna.getDateTimeAfter());
System.out.println(" " + luka.toLocalTime() + ": " + zbadajGodzine(luka, WARSZAWA));
System.out.println(" of() przesuwa ją: " + ZonedDateTime.of(luka, WARSZAWA).format(PELNY));
System.out.println(" czy istnieje z " + wiosna.getOffsetBefore() + ": "
+ czyPrzesunieciePasuje(luka, wiosna.getOffsetBefore(), WARSZAWA));
// 4. Jesienią ten sam napis oznacza dwie różne chwile.
LocalDateTime nakladka = jesien.getDateTimeAfter().plusMinutes(30);
ZonedDateTime pierwsze = ZonedDateTime.of(nakladka, WARSZAWA);
ZonedDateTime drugie = pierwsze.withLaterOffsetAtOverlap();
System.out.println("Jesień: " + jesien.getDateTimeBefore() + " -> " + jesien.getDateTimeAfter());
System.out.println(" " + nakladka.toLocalTime() + ": " + zbadajGodzine(nakladka, WARSZAWA));
System.out.println(" domyślny wybór: " + pierwsze.format(PELNY));
System.out.println(" drugie przejście: " + drugie.format(PELNY));
System.out.println(" dzieli je: " + Duration.between(pierwsze, drugie).toMinutes() + " min");
// 5. Nocna zmiana 22:00-06:00 w noc cofnięcia zegarów trwa dłużej, niż widać na tarczy.
LocalDateTime poNakladce = jesien.getDateTimeAfter();
ZonedDateTime start = ZonedDateTime.of(poNakladce.withHour(22).minusDays(1), WARSZAWA);
ZonedDateTime koniec = ZonedDateTime.of(poNakladce.withHour(6), WARSZAWA);
long wedlugZegara = ChronoUnit.HOURS.between(start.toLocalDateTime(), koniec.toLocalDateTime());
System.out.println("Nocna zmiana: " + start.format(PELNY) + " -> " + koniec.format(PELNY));
System.out.println(" zegar pokazuje: " + wedlugZegara + " h");
System.out.println(" do wypłaty: " + czasPracy(start, koniec).toMinutes() + " min");
// 6. Skróty stref kłamią — używaj identyfikatorów region/miasto.
try {
ZoneId.of("EST");
} catch (ZoneRulesException blad) {
System.out.println("ZoneId.of(\"EST\"): " + blad.getMessage());
}
Instant lipiec = LocalDateTime.of(2026, 7, 1, 12, 0).toInstant(ZoneOffset.UTC);
System.out.println("Południe UTC w Nowym Jorku: " + naZegarze(lipiec, NOWY_JORK));
System.out.println(" to samo przez skrót EST: "
+ naZegarze(lipiec, ZoneId.of("EST", ZoneId.SHORT_IDS)));
LocalDateTime archiwum = LocalDateTime.of(1964, 6, 1, 12, 0);
System.out.println("Archiwum w Europe/Warsaw: " + ZonedDateTime.of(archiwum, WARSZAWA).format(PELNY));
System.out.println(" to samo w skrócie CET: "
+ ZonedDateTime.of(archiwum, ZoneId.of("CET")).format(PELNY));
}
}
Jak użyć tego skryptu
- Wklej kod do pliku
StrefyCzasowe.javai odpaljava StrefyCzasowe.java. W trybie jednoplikowym launcher nie sprawdza, czy nazwa pliku zgadza się z klasą publiczną — dopierojavactego pilnuje, więc nadaj ją poprawnie od razu. Zobaczysz, jak 2:30 znika pod koniec marca i jak ta sama 2:30 wraca w październiku po raz drugi. - W swoim projekcie trzymaj każdy znacznik czasu jako
Instant(czyli UTC), a strefę doklejaj dopiero przy wyświetlaniu:StrefyCzasowe.naZegarze(chwila, ZoneId.of("Europe/Warsaw")). W bazie odpowiada temu kolumnatimestamptzalboTIMESTAMPzapisywany w UTC. - Godzinę wpisaną przez użytkownika sprawdź, zanim ją zapiszesz. W kodzie rozstrzygnie to
strefa.getRules().getTransition(lokalna)— wynik inny niżnulloznacza lukę albo nakładkę, więc poproś o doprecyzowanie zamiast zgadywać.zbadajGodzineukłada z tego samego sprawdzenia gotowy komunikat dla człowieka, a parę „czas plus przesunięcie” przysłaną przez API zweryfikujesz metodączyPrzesunieciePasuje. - Do rozliczania czasu pracy używaj
czasPracy, która zwraca dokładnyDurationmiędzy chwilami. Dopiero kod z regułami płacowymi powinien zamienić wynik na minuty lub zaokrąglone godziny;toHours()obcięłoby każdą niepełną godzinę.ChronoUnit.HOURS.betweenna samychLocalDateTimepoliczy zaś to, co pokazywała tarcza, a nie faktycznie przepracowany czas.
Warto wiedzieć
- W nieistniejącej godzinie
ZonedDateTime.ofnie zgłasza żadnego błędu — po cichu przesuwa czas o długość luki, więc z 02:30 robi 03:30. Zadanie zaplanowane na 2:30 w noc wiosennej zmiany wykona się o godzinę później, a raport „o 2:30 nic nie zrobiliśmy” będzie prawdziwy tylko pozornie. Jeśli wolisz wyjątek zamiast cichej korekty, użyjZonedDateTime.ofStrict(lokalna, przesuniecie, strefa)— przy niepasującym przesunięciu rzucaDateTimeException. - W jesiennej nakładce
ZonedDateTime.ofwybiera zawsze pierwsze wystąpienie godziny, czyli to z letnim przesunięciem; do drugiego dostaniesz się przezwithLaterOffsetAtOverlap(). Konsekwencja jest praktyczna: dwa zdarzenia zapisane jako „02:30” dzieli w rzeczywistości godzina, a jeśli w bazie masz samLocalDateTimebez przesunięcia, tej różnicy nie odtworzysz już nigdy — zamówienia, logi i stawki nocne z tej jednej godziny stają się nierozróżnialne. - Skróty stref to mina.
ZoneId.of("EST")w ogóle nie działa i rzucaZoneRulesException, a mapowanieZoneId.SHORT_IDSnie jest stabilnym kontraktem między wydaniami: JDK 17 tłumaczyESTna stałe-05:00, natomiast JDK 25 naAmerica/Panama. W obu przypadkach nie dostajesz reguł Nowego Jorku i latem wynik jest o godzinę wcześniejszy; dokumentacja JDK 25 zastrzega też, że mapa może zmieniać się w aktualizacjach. Z koleiCETto osobna, uproszczona strefa, a nie synonimEurope/Warsaw: z polskimi regułami pokrywa się dopiero od 1965 roku, a wcześniej rozjeżdża się z nimi o godzinę (czerwiec 1964 z demonstracji), a w latach 1918–1922 nawet o dwie. Podawaj identyfikatory w postaci region/miasto. - Reguły stref to dane, nie kod — baza tzdb siedzi w JDK i starzeje się razem z nim, a kraje potrafią zmienić zasady z kilkutygodniowym wyprzedzeniem. Dlatego przyszłe terminy przechowuj jako
LocalDateTimeplusZoneIdi przeliczaj przy każdym wyświetleniu; zapisany dziśInstantzamraża dzisiejsze przesunięcie i po zmianie przepisów wskaże inną godzinę na zegarze, niż umówiłeś z klientem. Dla zdarzeń przeszłych jest odwrotnie: tamInstantjest jedyną sensowną formą zapisu.
Podobne skrypty
Data i czas po polsku (java.time)
Zestaw gotowych metod na bazie java.time: formatowanie dat po polsku, bezpieczne parsowanie tekstu, obliczanie wieku, różnicy dni, dni roboczych bez weekendów oraz strefy czasowe.
początkujący Zobacz kod → Data i czasStoper i precyzyjny pomiar czasu
Stoper na System.nanoTime z pauzą i wznowieniem, pomiar operacji razem z jej wynikiem oraz formatowanie Duration w jednostce dobranej do skali — od nanosekund po minuty.
średni Zobacz kod → Data i czasDni robocze, harmonogram i Wielkanoc
Kalendarz polskich dni roboczych: data Wielkanocy z algorytmu Meeusa, święta stałe i ruchome, liczenie dni w przedziale, wyznaczanie terminów oraz gotowe harmonogramy.
zaawansowany Zobacz kod →