Przejdź do treści

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.

JAVA StrefyCzasowe.java 130 linii Pobierz .java
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

  1. Wklej kod do pliku StrefyCzasowe.java i odpal java StrefyCzasowe.java. W trybie jednoplikowym launcher nie sprawdza, czy nazwa pliku zgadza się z klasą publiczną — dopiero javac tego 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.
  2. 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 kolumna timestamptz albo TIMESTAMP zapisywany w UTC.
  3. Godzinę wpisaną przez użytkownika sprawdź, zanim ją zapiszesz. W kodzie rozstrzygnie to strefa.getRules().getTransition(lokalna) — wynik inny niż null oznacza lukę albo nakładkę, więc poproś o doprecyzowanie zamiast zgadywać. zbadajGodzine układa z tego samego sprawdzenia gotowy komunikat dla człowieka, a parę „czas plus przesunięcie” przysłaną przez API zweryfikujesz metodą czyPrzesunieciePasuje.
  4. Do rozliczania czasu pracy używaj czasPracy, która zwraca dokładny Duration mię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.between na samych LocalDateTime policzy zaś to, co pokazywała tarcza, a nie faktycznie przepracowany czas.

Warto wiedzieć

  • W nieistniejącej godzinie ZonedDateTime.of nie 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żyj ZonedDateTime.ofStrict(lokalna, przesuniecie, strefa) — przy niepasującym przesunięciu rzuca DateTimeException.
  • W jesiennej nakładce ZonedDateTime.of wybiera zawsze pierwsze wystąpienie godziny, czyli to z letnim przesunięciem; do drugiego dostaniesz się przez withLaterOffsetAtOverlap(). Konsekwencja jest praktyczna: dwa zdarzenia zapisane jako „02:30” dzieli w rzeczywistości godzina, a jeśli w bazie masz sam LocalDateTime bez 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 rzuca ZoneRulesException, a mapowanie ZoneId.SHORT_IDS nie jest stabilnym kontraktem między wydaniami: JDK 17 tłumaczy EST na stałe -05:00, natomiast JDK 25 na America/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 kolei CET to osobna, uproszczona strefa, a nie synonim Europe/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 LocalDateTime plus ZoneId i przeliczaj przy każdym wyświetleniu; zapisany dziś Instant zamraż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: tam Instant jest jedyną sensowną formą zapisu.
#java.time #strefy czasowe #localdate #formatowanie #walidacja

Podobne skrypty