Przejdź do treści

Od dni do sekund – uniksowy timestamp, problem roku 2038 i strefy czasowe

W dwóch poprzednich wpisach – o wiecznym kalendarzu i o dacie Wielkanocy – zajmowaliśmy się dniami: latami przestępnymi, dniami tygodnia, ruchomymi świętami. Ale komputer, na którym to czytasz, wcale nie myśli w dniach. Pod spodem nie ma żadnego „29 lipca” ani „środy” – jest jedna wielka liczba sekund. I z tej prostoty wynika zaskakująco dużo problemów: data z limitem ważności w 2038 roku, sekundy dopisywane do doby na ostatniej pozycji zegara i strefy czasowe, które trzeba aktualizować jak oprogramowanie.

Uniksowy timestamp: jedna liczba zamiast daty

Czas uniksowy to liczba sekund, które upłynęły od 1 stycznia 1970 roku, 00:00:00 UTC – tzw. epoki Uniksa. Ta konwencja, przyjęta w latach 70. dla wygody jednego systemu operacyjnego, stała się lingua franca całej informatyki:

$ date +%s
1785326400        # ≈ południe UTC, 29 lipca 2026
Date.now();                    // 1785326400000 – uwaga: JS liczy w MILIsekundach
Math.trunc(Date.now() / 1000); // klasyczny timestamp w sekundach
import time
time.time()        # 1785326400.514 – float: sekundy z ułamkiem
<?php
time();            // 1785326400

Zalety są ogromne. Porównanie dwóch momentów to porównanie dwóch liczb, odstęp między nimi to zwykłe odejmowanie, a jeden moment ma jedną wartość na całym świecie – timestamp nie wie nic o strefach czasowych, bo liczy się względem UTC. Cała arytmetyka kalendarzowa z poprzednich wpisów (lata przestępne, długości miesięcy) jest potrzebna dopiero na samym końcu – przy zamianie liczby na napis „29 lipca 2026” dla człowieka.

Jest jeszcze jedna, mniej oczywista konwencja: w czasie uniksowym doba ma zawsze dokładnie 86 400 sekund. Zapamiętaj to zdanie – wrócimy do niego przy sekundach przestępnych, bo fizyczna doba taka równa nie jest.

Problem roku 2038

Sekundy od 1970 roku trzeba w czymś przechowywać. Historycznie typem time_t w C był 32-bitowy int ze znakiem, a jego maksimum to 2³¹ − 1 = 2 147 483 647. Policzmy, kiedy licznik się skończy:

from datetime import datetime, timezone

datetime.fromtimestamp(2**31 - 1, tz=timezone.utc)
# datetime(2038, 1, 19, 3, 14, 7, ...) – ostatnia sekunda 32-bitowego świata

19 stycznia 2038, 03:14:07 UTC. Sekundę później licznik przekręca się na −2 147 483 648, czyli… 13 grudnia 1901 roku. To problem roku 2038 – młodszy brat słynnej pluskwy milenijnej, tyle że wynikający nie z zapisu „dwóch cyfr roku”, lecz z twardego limitu typu danych.

Kogo to naprawdę dotknie? Twój laptop i telefon są od dawna 64-bitowe – 64-bitowy time_t wystarczy na ~292 miliardy lat, więc temat nie istnieje. Ryzyko siedzi gdzie indziej:

  • Systemy wbudowane: sterowniki, liczniki, urządzenia przemysłowe i IoT. Sprzęt sprzedawany dziś z 32-bitowym firmware’em będzie w 2038 roku miał kilkanaście lat – czyli dokładnie tyle, ile zwykle żyje.
  • Formaty danych i bazy: MySQL-owy typ TIMESTAMP do dziś ma zakres kończący się na 2038-01-19 03:14:07 – daty urodzin, terminy kredytów czy ważność dokumentów lepiej trzymać w DATETIME, który limitu nie ma.
  • Programy liczące w przyszłość: na błąd 2038 nie trzeba czekać do 2038. Soft wyliczający harmonogram 30-letniego kredytu albo termin ważności długiego certyfikatu przekracza granicę już teraz – pierwsze takie awarie raportowano dekady temu.

Świat open source odrobił lekcję zawczasu: Linux od wersji 5.6 (2020) ma 64-bitowe wywołania czasu także dla systemów 32-bitowych, glibc udostępnia _TIME_BITS=64, a Debian 13 „trixie” (2025) przeprowadził wielką migrację t64 – przebudowę tysięcy pakietów na 64-bitowy time_t również na architekturach 32-bitowych. JavaScript jest bezpieczny niejako przypadkiem: Date trzyma milisekundy w liczbie zmiennoprzecinkowej, co daje zakres do roku 275760.

A najlepsze na koniec: pamiętasz z wpisu o Wielkanocy, że rok 2038 to także najpóźniejsza możliwa Wielkanoc (25 kwietnia) i granica PHP-owego easter_date()? Rok 2038 zbiera nam się w całkiem osobny wpis.

Sekundy przestępne: doba nie jest równa

Wróćmy do konwencji „doba = 86 400 sekund”. Sekunda jest dziś zdefiniowana atomowo (przez częstotliwość przejścia w cezie‑133), a obrót Ziemi – niestety – atomowo równy nie jest: pływy księżycowe go hamują, a procesy we wnętrzu planety potrafią go chwilowo przyspieszać. Astronomiczna doba różni się od 86 400 atomowych sekund o pojedyncze milisekundy – i te milisekundy się kumulują.

Rozwiązanie z 1972 roku to sekunda przestępna: gdy rozjazd zbliża się do 0,9 s, do UTC dokłada się dodatkową sekundę – zegar pokazuje wtedy egzotyczne 23:59:60. Od 1972 roku zrobiono to 27 razy (ostatnio 31 grudnia 2016). Stąd bierze się rodzina skal czasu, które warto umieć rozróżnić:

SkalaCo robi z sekundami przestępnymiStan dziś
TAI (atomowa)nie zna ich – płynie jednostajniewzorzec
UTC (cywilna)wstawia 23:59:60TAI − 37 s
GPSzamrożona kopia UTC z 1980 r.TAI − 19 s
czas uniksowyudaje, że nie istnieją~UTC bez 23:59:60

Czas uniksowy sekundy przestępnej nie umie zapisać – 23:59:60 nie ma swojego timestampa. Systemy radzą sobie różnie: klasycznie kernel powtarza ostatnią sekundę doby, a wielkie chmury stosują leap smear – rozsmarowują dodatkową sekundę mikroskopijnymi porcjami na całą dobę, żeby żaden proces nie zobaczył czasu stojącego w miejscu (sekunda z 30 czerwca 2012 położyła wtedy sporo dużych serwisów – od tamtej pory branża woli dmuchać na zimne).

Ten rozdział historii właśnie się kończy: 27. Generalna Konferencja Miar (2022) postanowiła zrezygnować z sekund przestępnych najpóźniej do 2035 roku – rozjazd z obrotem Ziemi będzie się kumulował i zostanie skorygowany kiedyś, większym skokiem. Jest i pikanteria: Ziemia od kilku lat kręci się odrobinę szybciej, więc zanim reforma wejdzie, może być potrzebna pierwsza w historii ujemna sekunda przestępna (23:59:58 → 00:00:00) – scenariusz, którego żaden produkcyjny system nigdy nie przećwiczył.

Strefy czasowe: baza danych, nie matematyka

Timestamp mówi, który to moment; strefa czasowa mówi, jak ten moment nazywa się na czyimś zegarze ściennym. I tu kończy się matematyka, a zaczyna polityka. Offset strefy nie wynika z żadnego wzoru – to decyzja administracyjna, którą państwa potrafią zmienić z tygodniowym wyprzedzeniem. Dlatego strefy czasowe są utrzymywane jako baza danych: tz database od IANA (tzdata), z identyfikatorami w rodzaju Europe/Warsaw, pełną historią zmian od 1970 roku i kilkoma wydaniami rocznie – którą Twój system aktualizuje jak każdy inny pakiet.

Jak bardzo bywa to płynne, pokazują przykłady z ostatnich lat:

  • Samoa, 30 grudnia 2011 – dzień, którego nie było. Kraj przeskoczył na zachodnią stronę linii zmiany daty (żeby handlować w rytmie Australii, nie USA): po czwartku 29 grudnia nastąpiła od razu sobota 31 grudnia. Dokładnie ten sam manewr, co wycinanie dni w 1582 i 1752 roku z wpisu o kalendarzu – tyle że współcześnie i z powodów ekonomicznych.
  • Liban, marzec 2023: rząd w ostatniej chwili przesunął zmianę czasu, część instytucji się nie podporządkowała – i przez kilka dni w jednym kraju obowiązywały dwa czasy naraz.
  • Unia Europejska: projekt zniesienia sezonowej zmiany czasu wisi od 2018 roku i wciąż nie wszedł w życie – co oznacza, że reguły dla Europe/Warsaw mogą się zmienić w każdej chwili.

Sezonowa zmiana czasu funduje nam zresztą lokalne odpowiedniki kalendarzowych ciekawostek: w Polsce 29 marca 2026 godzina 2:30 nie istnieje (zegary skaczą z 2:00 na 3:00), a 25 października 2026 godzina 2:30 zdarzy się dwa razy. Biblioteki standardowe muszą to jakoś rozstrzygać – Python robi to parametrem fold:

from datetime import datetime
from zoneinfo import ZoneInfo   # baza tzdata w bibliotece standardowej (Python 3.9+)

warsaw = ZoneInfo("Europe/Warsaw")
datetime(2026, 7, 29, 12, 0, tzinfo=warsaw).utcoffset()   # 2 h – CEST (czas letni)
datetime(2026, 12, 29, 12, 0, tzinfo=warsaw).utcoffset()  # 1 h – CET

# 25 X 2026, 2:30 istnieje dwa razy – fold wybiera, o którą nam chodzi:
datetime(2026, 10, 25, 2, 30, tzinfo=warsaw).utcoffset()          # 2 h – pierwsze 2:30 (CEST)
datetime(2026, 10, 25, 2, 30, fold=1, tzinfo=warsaw).utcoffset()  # 1 h – drugie 2:30 (CET)

W JavaScript przeglądarka zna strefę użytkownika (Intl.DateTimeFormat().resolvedOptions().timeZone"Europe/Warsaw"), a nadchodzące Temporal ma pełnoprawny typ ZonedDateTime; w PHP analogiczną rolę pełni DateTimeZone.

Zasada przechowywania dat. Momenty z przeszłości zapisuj jako timestamp/UTC – to fakty, nic ich nie zmieni. Ale przyszłe wydarzenia lokalne (spotkanie „we wtorek o 9:00", lot, wizyta) przechowuj jako czas lokalny + identyfikator strefy (2027-03-30 09:00, Europe/Warsaw), nie jako timestamp. Timestamp zamraża dzisiejsze reguły strefy – jeśli państwo zmieni przepisy między zapisem a terminem (a widzieliśmy, że zmienia), Twoje „9:00" przestanie być dziewiątą.

Podsumowanie

  • Komputery liczą sekundy od 1 stycznia 1970 UTC – daty, miesiące i dni tygodnia to tylko warstwa prezentacji nad jedną liczbą (JS: uwaga na milisekundy).
  • Problem roku 2038 to limit 32-bitowego time_t (2³¹ − 1 s → 19 I 2038, 03:14:07 UTC). Desktopy są bezpieczne; ryzyko to embedded/IoT, formaty danych (MySQL TIMESTAMP!) i każdy soft liczący daty w przyszłość – tam błędy zdarzają się już dziś.
  • Sekundy przestępne łatają różnicę między sekundą atomową a obrotem Ziemi (27 wstawek od 1972 r.; TAI − UTC = 37 s). Czas uniksowy je ignoruje, chmury je „rozsmarowują”, a od 2035 roku mają zniknąć – być może po pierwszej w historii sekundzie ujemnej.
  • Strefy czasowe to baza danych IANA, nie wzór: aktualizowana jak oprogramowanie, pełna dni, których nie było (Samoa 2011), i godzin, które bywają dwa razy (25 X, 2:30). Przyszłe wydarzenia lokalne przechowuj jako czas lokalny + strefa.

Tak domyka się mały cykl o czasie: wieczny kalendarz policzył dni, computus – święta, a dziś zeszliśmy do sekund, na których to wszystko stoi.