Wieczny kalendarz – skąd się biorą lata przestępne i jak obliczyć dzień tygodnia
„Wieczny kalendarz” to tablica albo algorytm, który dla dowolnej daty – sprzed stu lat czy za dwieście lat – podaje dzień tygodnia. Żeby taki kalendarz zbudować, trzeba zrozumieć dwie rzeczy: skąd biorą się lata przestępne i dlaczego kalendarz gregoriański w ogóle wygląda tak, jak wygląda. A po drodze trafimy na najdziwniejsze daty w historii: dni, które nigdy nie istniały, i jedyny w dziejach 30 lutego.
Wpis dzieli się na pół: pierwsza połowa to teoria i historia, druga – algorytmy z implementacjami, a na końcu gotowe funkcje wbudowane w popularne języki. Praktyczną stronę tematu – budowę interaktywnego kalendarza miesięcznego w przeglądarce – opisałem osobno we wpisie o kalendarzu w czystym JavaScript.
Problem: rok nie ma okrągłej liczby dni
Cała trudność kalendarza bierze się z jednego niewygodnego faktu astronomicznego: Ziemia okrąża Słońce w czasie, który nie jest wielokrotnością doby. Rok zwrotnikowy (od równonocy do równonocy) trwa około 365,2422 dnia – czyli 365 dni, 5 godzin, 48 minut i 45 sekund.
Gdybyśmy przyjęli po prostu 365 dni, kalendarz spóźniałby się o ~6 godzin rocznie – po 100 latach czerwiec wypadałby tam, gdzie wcześniej maj, a po ~700 latach obchodzilibyśmy Boże Narodzenie w środku lata. Kalendarz musi więc co jakiś czas „dosypać” dzień, żeby nadążyć za Słońcem. Pytanie tylko: jak często?
Kalendarz juliański: co 4 lata (za dużo)
Kalendarz juliański, wprowadzony przez Juliusza Cezara w 45 r. p.n.e., przyjmuje średnią długość roku 365,25 dnia: trzy lata zwykłe i czwarty przestępny. To bardzo blisko – ale 365,25 to o ~11 minut za dużo rocznie. Drobiazg? Po 128 latach z tych minut robi się cały dzień nadwyżki.
Przez szesnaście wieków ta nadwyżka urosła do 10 dni. Równonoc wiosenna, która w czasach soboru nicejskiego (325 r.) wypadała 21 marca, w XVI wieku wypadała już 11 marca – a od niej liczy się datę Wielkanocy, więc Kościół miał realny problem.
Reforma gregoriańska: reguła 4/100/400
W 1582 roku papież Grzegorz XIII bullą Inter gravissimas wprowadził dwie zmiany:
- Jednorazowo wycięto 10 dni, żeby cofnąć narosły błąd: po czwartku 4 października 1582 nastąpił od razu piątek 15 października 1582. Daty od 5 do 14 października 1582 nie istnieją.
- Poprawiono regułę lat przestępnych, żeby błąd nie narastał od nowa. Rok jest przestępny, gdy:
- dzieli się przez 4,
- chyba że dzieli się przez 100 – wtedy nie jest przestępny,
- chyba że dzieli się przez 400 – wtedy jednak jest.
Stąd rok 2000 był przestępny (dzieli się przez 400), ale 1900 nie był i 2100 nie będzie (dzielą się przez 100, ale nie przez 400). Na 400 lat wypada więc nie 100, a 97 lat przestępnych, co daje średnią długość roku 365,2425 dnia – od prawdziwej różni się już tylko o ~26 sekund rocznie, czyli o 1 dzień na ~3300 lat. Wystarczy na długo.
Dlaczego „wieczny" kalendarz jest w ogóle możliwy? 400 lat gregoriańskich to 400 × 365 + 97 = 146 097 dni, a 146 097 dzieli się przez 7 bez reszty (20 871 tygodni). Kalendarz gregoriański powtarza się więc dokładnie co 400 lat – 28 lipca 2426 wypadnie w ten sam dzień tygodnia co 28 lipca 2026. Cała „wieczność" mieści się w skończonej, 400‑letniej tabeli.
Dni, które nigdy nie istniały
Reforma z 1582 roku obowiązywała od razu tylko tam, gdzie sięgała władza papieża. Reszta Europy przechodziła na nowy kalendarz przez kolejne 340 lat – a każdy kraj, który się przesiadał, musiał wyciąć ze swojego kalendarza narosłą różnicę. Im później, tym więcej dni trzeba było skreślić.
| Kiedy | Kto | Wycięte dni |
|---|---|---|
| październik 1582 | Polska (z Litwą), Hiszpania, Portugalia, Włochy | 10 (4 X → 15 X) |
| grudzień 1582 | Francja | 10 |
| 1583–1700 | kraje niemieckie, Niderlandy, Dania, Norwegia… | 10–11 |
| wrzesień 1752 | Wielka Brytania z koloniami (w tym przyszłe USA) | 11 (2 IX → 14 IX) |
| 1753 | Szwecja | 11 |
| 1918 | Rosja | 13 (31 I → 14 II) |
| 1923 | Grecja (ostatnia w Europie) | 13 |
Polska przyjęła nowy kalendarz od razu, w 1582 roku, jako jedno z pierwszych państw na świecie.
Wielka Brytania, 1752: „Oddajcie nam nasze 11 dni!”
Anglia trzymała się kalendarza juliańskiego jeszcze 170 lat po reformie. Skutek: list wysłany z Londynu do Paryża przychodził „11 dni przed” datą nadania. Dopiero Calendar (New Style) Act 1750 uporządkował sprawę: po środzie 2 września 1752 nastąpił od razu czwartek 14 września 1752. Ta sama ustawa przesunęła też początek roku prawnego z… 25 marca na 1 stycznia – wcześniej w Anglii „nowy rok” urzędowo zaczynał się wiosną.
Z tą zmianą wiąże się anegdota o tłumach protestujących pod hasłem „Give us our eleven days!” („Oddajcie nam nasze jedenaście dni!”) – ludzie mieli wierzyć, że rząd skrócił im życie. Historycy dziś wątpią, czy takie zamieszki naprawdę wybuchły (hasło znamy głównie z satyrycznego obrazu Hogartha), ale realne pretensje były: pytanie, czy czynsz za wrzesień 1752 należy się za cały miesiąc, wcale nie było głupie.
Ślad tej reformy nosi każdy Unix i Linux. Polecenie cal domyślnie przyjmuje brytyjską datę reformy, więc wrzesień 1752 wygląda tak:
$ cal 9 1752
September 1752
Su Mo Tu We Th Fr Sa
1 2 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30To nie błąd programu – tych dni naprawdę (w Anglii) nie było.
Szwecja, 1712: jedyny w historii 30 lutego
Najbarwniej pogubiła się Szwecja. W 1700 roku postanowiła przejść na kalendarz gregoriański stopniowo: zamiast wyciąć 11 dni naraz, planowała przez 40 lat pomijać wszystkie dni przestępne. Pominięto luty 1700… a potem wybuchła wielka wojna północna i o planie zapomniano – lata 1704 i 1708 były przestępne, jak gdyby nigdy nic. Efekt: kalendarz szwedzki nie zgadzał się już ani z juliańskim, ani z gregoriańskim – Szwecja żyła o jeden dzień obok całego świata.
Karol XII uciął to w najprostszy możliwy sposób: wracamy do kalendarza juliańskiego, a zgubiony dzień oddajemy. Dlatego luty 1712 dostał w Szwecji dwa dni przestępne – i tak powstał 30 lutego 1712, jedyna taka data w historii. Na gregoriański Szwecja przeszła ostatecznie w 1753 roku, już klasycznie: po 17 lutego nastąpił 1 marca.
Rosja, 1918: rewolucja październikowa w listopadzie
Rosja carska używała kalendarza juliańskiego aż do rewolucji – dlatego rewolucja październikowa (25 października według starego stylu) wypada według naszego kalendarza 7 listopada. Rosja radziecka wycięła 13 dni w lutym 1918: po 31 stycznia nastąpił od razu 14 lutego.
Algorytmy
Tyle historii – czas na kod. Wszystkie poniższe algorytmy dotyczą kalendarza gregoriańskiego.
Rok przestępny: reguła 4/100/400 w kodzie
Reguła przekłada się na jedno wyrażenie logiczne. Kolejność warunków ma znaczenie – najczęstszy błąd początkujących to sprawdzenie samego rok % 4:
// JavaScript
const przestepny = (rok) =>
(rok % 4 === 0 && rok % 100 !== 0) || rok % 400 === 0;
przestepny(2024); // true (dzieli się przez 4, nie przez 100)
przestepny(2026); // false
przestepny(2100); // false (dzieli się przez 100, nie przez 400)
przestepny(2000); // true (dzieli się przez 400)# Python
def przestepny(rok: int) -> bool:
return rok % 4 == 0 and (rok % 100 != 0 or rok % 400 == 0)<?php
// PHP
function przestepny(int $rok): bool
{
return ($rok % 4 === 0 && $rok % 100 !== 0) || $rok % 400 === 0;
}Obie postaci (JS/PHP kontra Python) są równoważne – to ta sama reguła zapisana raz przez alternatywę, raz przez koniunkcję. Wykazanie tej równoważności to zresztą ładne ćwiczenie z praw De Morgana.
Kongruencja Zellera: dzień tygodnia ze wzoru
Klasyczny algorytm wiecznego kalendarza to kongruencja Zellera, opublikowana przez Christiana Zellera w 1882 roku. Dla kalendarza gregoriańskiego:
h = ( q + ⌊13(m + 1) / 5⌋ + K + ⌊K / 4⌋ + ⌊J / 4⌋ + 5J ) mod 7gdzie q to dzień miesiąca, m – numer miesiąca, K = rok mod 100, J = ⌊rok / 100⌋ (stulecie), a h to dzień tygodnia w nietypowej numeracji: 0 = sobota, 1 = niedziela, …, 6 = piątek.
Najciekawszy jest trik z miesiącami: Zeller liczy rok od marca, więc m przyjmuje wartości od 3 (marzec) do 14, przy czym styczeń i luty traktuje się jako 13. i 14. miesiąc roku poprzedniego. Dzięki temu dzień przestępny (29 lutego) wypada na samym końcu tak przesuniętego roku i nie psuje wzoru w środku – to dokładnie ten sam powód, dla którego rzymski kalendarz dokładał dzień przestępny w lutym: luty był kiedyś ostatnim miesiącem roku. Wyrażenie ⌊13(m+1)/5⌋ to z kolei sprytna aproksymacja nieregularnych długości miesięcy (31, 30, 31, 30, 31…) funkcją liniową z podłogą.
def dzien_tygodnia(rok: int, miesiac: int, dzien: int) -> str:
"""Kongruencja Zellera dla kalendarza gregoriańskiego."""
if miesiac < 3: # styczeń i luty = 13. i 14. miesiąc
miesiac += 12 # poprzedniego roku
rok -= 1
K, J = rok % 100, rok // 100
h = (dzien + (13 * (miesiac + 1)) // 5 + K + K // 4 + J // 4 + 5 * J) % 7
return ("sobota", "niedziela", "poniedziałek", "wtorek",
"środa", "czwartek", "piątek")[h]
dzien_tygodnia(1582, 10, 15) # 'piątek' – pierwszy dzień kalendarza gregoriańskiego
dzien_tygodnia(2026, 7, 28) # 'wtorek' – data publikacji tego wpisuMetoda Sakamoto: to samo w trzech linijkach
W latach 90. Tomohiko Sakamoto opublikował w grupie dyskusyjnej comp.lang.c wersję, która stała się kanonem konkursów programistycznych – tablica 12 stałych zastępuje całą arytmetykę miesięcy:
/* C – oryginał krąży w tej postaci od lat 90. */
int dzien_tygodnia(int y, int m, int d)
{
static const int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
if (m < 3)
y -= 1;
return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7; /* 0 = niedziela */
}Idea jest ta sama co u Zellera – styczeń i luty „należą” do poprzedniego roku (y -= 1), a składnik y + y/4 - y/100 + y/400 to po prostu liczba lat plus liczba dni przestępnych do roku y, czyli wprost reguła 4/100/400. Tablica t koduje przesunięcie początku każdego miesiąca względem początku roku, modulo 7. W JavaScript to jednolinijkowiec:
const dzienTygodnia = (y, m, d) =>
((m < 3 ? --y : y) + Math.trunc(y / 4) - Math.trunc(y / 100) +
Math.trunc(y / 400) + [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4][m - 1] + d) % 7;
dzienTygodnia(2026, 7, 28); // 2 – wtorek (0 = niedziela)Doomsday: wieczny kalendarz w pamięci
Algorytm Doomsday Johna Conwaya (tego od „gry w życie”) jest przeznaczony nie dla komputera, lecz do liczenia w głowie – Conway potrafił nim podawać dzień tygodnia w ~2 sekundy. Opiera się na obserwacji, że kilka łatwych do zapamiętania dat zawsze wypada w ten sam dzień tygodnia danego roku (tzw. doomsday):
- 4.04, 6.06, 8.08, 10.10, 12.12 – „parzysty/parzysty”,
- 9.05, 5.09, 7.11, 11.07 – mnemonik: „9 do 5 w 7‑Eleven”,
- ostatni dzień lutego oraz 3.01 (4.01 w roku przestępnym).
Wystarczy więc wyznaczyć doomsday danego roku i doliczyć różnicę do najbliższej z tych dat. Dla lat 2000–2099 kotwicą stulecia jest wtorek; doomsday roku to kotwica + (RR + ⌊RR/4⌋) mod 7, gdzie RR to dwie ostatnie cyfry roku.
Przykład dla 2026: 26 + ⌊26/4⌋ = 32, 32 mod 7 = 4, wtorek + 4 = sobota. Sprawdźmy: w jaki dzień wypadnie Boże Narodzenie 2026? Doomsday to 12.12 (sobota), a 25.12 jest 13 dni później; 13 mod 7 = 6, sobota + 6 = piątek. Zgadza się – i policzyliśmy to bez kartki.
Gotowe funkcje w językach programowania
Algorytmy powyżej warto napisać raz – dla zrozumienia. W realnym kodzie używa się bibliotek standardowych, bo daty to pole minowe (strefy czasowe, lokalizacja, walidacja):
# Python – moduł calendar i datetime
import calendar
calendar.isleap(2026) # False
calendar.leapdays(1582, 2026) # 108 – lata przestępne w przedziale
calendar.monthrange(2026, 7) # (2, 31) – lipiec zaczyna się w środę (0 = pon.), ma 31 dni
calendar.weekday(1582, 10, 15) # 4 – piątek
print(calendar.month(2026, 7)) # gotowy miesiąc tekstem, jak uniksowy cal<?php
// PHP
checkdate(2, 29, 2100); // false – walidacja daty z regułą przestępności
(new DateTime('1582-10-15'))->format('N'); // "5" – piątek (1 = poniedziałek, ISO-8601)
cal_days_in_month(CAL_GREGORIAN, 2, 2024); // 29
(bool) date('L'); // czy bieżący rok jest przestępny// JavaScript – klasyczny Date…
new Date(1582, 9, 15).getDay(); // 5 – piątek (0 = niedziela; miesiące od zera!)
new Intl.DateTimeFormat('pl-PL', { weekday: 'long' })
.format(new Date(1582, 9, 15)); // 'piątek'
// …oraz nowe Temporal (ES2026)
const d = Temporal.PlainDate.from('1582-10-15');
d.dayOfWeek; // 5 – piątek (1 = poniedziałek, jak w ISO)
d.inLeapYear; // false
d.daysInMonth; // 31Temporal – następca kulawego Date – jest już częścią standardu (ES2026) i działa natywnie w Firefoksie (od 139) i Chrome (od 144); na pełną dostępność czekamy jeszcze na Safari. W Javie odpowiednikiem jest java.time (Year.of(2026).isLeap(), LocalDate.getDayOfWeek()), w C# – DateTime.IsLeapYear() i właściwość DayOfWeek.
Pułapka: kalendarz proleptyczny. Niemal wszystkie te API „udają", że kalendarz gregoriański obowiązywał zawsze (to tzw. kalendarz proleptyczny) – zapytane o 3 września 1752 grzecznie policzą dzień tygodnia, choć w Anglii taki dzień nie istniał, a data z angielskiego dokumentu sprzed 1752 roku nie przystaje do ich wyniku. Wyjątkiem jest stare javowe java.util.GregorianCalendar, które naprawdę przełącza się na kalendarz juliański przed 15 października 1582 – i np. datę „10 października 1582" po cichu przesunie na 20 października. Nowe java.time jest już, jak wszyscy, proleptyczne.
Podsumowanie
Wieczny kalendarz to wdzięczny temat, bo w jednym zadaniu spotykają się astronomia, historia i arytmetyka modularna:
- rok zwrotnikowy ma 365,2422 dnia – i z tej końcówki bierze się cała reszta;
- juliańskie „co 4 lata” myli się o dzień na 128 lat; gregoriańska reguła 4/100/400 – o dzień na ~3300 lat;
- przejście na nowy kalendarz kosztowało Europę wycinane dni: 10 w 1582 (m.in. Polska), 11 w Anglii w 1752 (
cal 9 1752!), 13 w Rosji w 1918 – a Szwecja po drodze zafundowała sobie 30 lutego 1712; - kalendarz gregoriański powtarza się co 400 lat (146 097 dni = równo 20 871 tygodni) – dlatego skończona tablica może być „wieczna”;
- dzień tygodnia policzysz kongruencją Zellera, tablicową metodą Sakamoto albo – w pamięci – Doomsdayem Conwaya;
- w produkcji korzystaj z bibliotek standardowych (
calendar,DateTime,Temporal,java.time) – ale dopiero po napisaniu Zellera samodzielnie wiesz, co one liczą.
Jeśli po teorii masz ochotę na praktykę front-endową, w osobnym wpisie buduję kalendarz miesięczny w czystym JavaScript – bez żadnych zależności, z Intl zamiast tablic nazw miesięcy.