Dni robocze, harmonogram i Wielkanoc
- Data i czas
- poziom: zaawansowany
- aktualizacja: 16 sierpnia 2026
Odejmiesz soboty i niedziele — i prędzej czy później wyślesz klientowi termin płatności na Boże Ciało. Świąt ruchomych nie wpiszesz na sztywno: chodzą po kalendarzu razem z Wielkanocą, a ta co roku wypada gdzie indziej. Poniższa klasa liczy niedzielę wielkanocną algorytmem gregoriańskim, wyprowadza z niej Poniedziałek Wielkanocny, Zielone Świątki i Boże Ciało, a potem pozwala Ci liczyć dni robocze, wyznaczać terminy i budować całe harmonogramy. W kalendarzu jest też Wigilia, w Polsce ustawowo wolna od 2025 roku, a firmowe dni wolne dorzucisz własnym zbiorem.
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Polskie dni robocze i święta ruchome — skryptyJAVA.pl
* Zapisz jako: KalendarzRoboczy.java
*/
public final class KalendarzRoboczy {
private static final Map<Integer, Map<LocalDate, String>> PAMIEC = new ConcurrentHashMap<>();
private KalendarzRoboczy() {} // Klasa zawiera wyłącznie metody statyczne.
/** Oblicza datę Wielkanocy w kalendarzu gregoriańskim; zmienne a–m jak w algorytmie Meeusa. */
public static LocalDate wielkanoc(int rok) {
if (rok < 1583) {
throw new IllegalArgumentException("Algorytm wymaga roku 1583 lub późniejszego.");
}
int a = rok % 19, b = rok / 100, c = rok % 100;
int d = b / 4, e = b % 4, f = (b + 8) / 25;
int g = (b - f + 1) / 3, i = c / 4, k = c % 4;
int h = (19 * a + b - d - g + 15) % 30;
int l = (32 + 2 * e + 2 * i - h - k) % 7;
int m = (a + 11 * h + 22 * l) / 451;
int dzienRoku = h + l - 7 * m + 114;
return LocalDate.of(rok, dzienRoku / 31, dzienRoku % 31 + 1);
}
/** Zwraca ustawowe polskie święta stałe i ruchome obowiązujące od 2025 roku. */
public static Map<LocalDate, String> swietaPolskie(int rok) {
if (rok < 2025) {
throw new IllegalArgumentException("Kalendarz obowiązuje od 2025 roku.");
}
LocalDate wielkanoc = wielkanoc(rok);
Map<LocalDate, String> swieta = new LinkedHashMap<>();
swieta.put(LocalDate.of(rok, 1, 1), "Nowy Rok");
swieta.put(LocalDate.of(rok, 1, 6), "Święto Trzech Króli");
swieta.put(wielkanoc, "Wielkanoc");
swieta.put(wielkanoc.plusDays(1), "Poniedziałek Wielkanocny");
swieta.put(LocalDate.of(rok, 5, 1), "Święto Pracy");
swieta.put(LocalDate.of(rok, 5, 3), "Święto Narodowe Trzeciego Maja");
swieta.put(wielkanoc.plusDays(49), "Zielone Świątki");
swieta.put(wielkanoc.plusDays(60), "Boże Ciało");
swieta.put(LocalDate.of(rok, 8, 15), "Wniebowzięcie NMP");
swieta.put(LocalDate.of(rok, 11, 1), "Wszystkich Świętych");
swieta.put(LocalDate.of(rok, 11, 11), "Narodowe Święto Niepodległości");
swieta.put(LocalDate.of(rok, 12, 24), "Wigilia Bożego Narodzenia");
swieta.put(LocalDate.of(rok, 12, 25), "Boże Narodzenie");
swieta.put(LocalDate.of(rok, 12, 26), "Drugi dzień Bożego Narodzenia");
return swieta;
}
/** Ten sam kalendarz, ale liczony raz na rok — inaczej płaci za niego każdy sprawdzany dzień. */
private static Map<LocalDate, String> swietaZPamieci(int rok) {
return PAMIEC.computeIfAbsent(rok, KalendarzRoboczy::swietaPolskie);
}
/** Sprawdza weekendy (6 i 7 to sobota i niedziela), święta oraz dni wolne firmy. */
public static boolean czyDzienRoboczy(LocalDate data, Set<LocalDate> dodatkoweWolne) {
Objects.requireNonNull(dodatkoweWolne, "Podaj zbiór dni wolnych, choćby pusty Set.of().");
return data.getDayOfWeek().getValue() < 6
&& !swietaZPamieci(data.getYear()).containsKey(data)
&& !dodatkoweWolne.contains(data);
}
/** Liczy dni robocze w przedziale od daty początkowej włącznie do końcowej wyłącznie. */
public static long liczDniRobocze(LocalDate od, LocalDate doDnia, Set<LocalDate> dodatkoweWolne) {
Objects.requireNonNull(dodatkoweWolne, "Podaj zbiór dni wolnych, choćby pusty Set.of().");
if (doDnia.isBefore(od)) {
throw new IllegalArgumentException("Data końcowa nie może być wcześniejsza.");
}
Set<LocalDate> wolne = new HashSet<>(dodatkoweWolne);
for (int rok = od.getYear(); rok <= doDnia.getYear(); rok++) {
wolne.addAll(swietaZPamieci(rok).keySet());
}
return od.datesUntil(doDnia)
.filter(data -> data.getDayOfWeek().getValue() < 6)
.filter(data -> !wolne.contains(data))
.count();
}
/** Przesuwa datę w przód o dni robocze; przy ile = 0 oddaje datę startową bez sprawdzania. */
public static LocalDate dodajDniRobocze(LocalDate data, int ile, Set<LocalDate> dodatkoweWolne) {
swietaZPamieci(data.getYear()); // sprawdza zakres także wtedy, gdy ile wynosi zero
if (ile < 0) {
throw new IllegalArgumentException("Liczba dni nie może być ujemna.");
}
LocalDate wynik = data;
int pozostalo = ile;
while (pozostalo > 0) {
wynik = wynik.plusDays(1);
if (czyDzienRoboczy(wynik, dodatkoweWolne)) {
pozostalo--;
}
}
return wynik;
}
/** Tworzy kolejne terminy oddalone o stałą liczbę dni roboczych. */
public static List<LocalDate> harmonogram(LocalDate start, int ileTerminow, int odstep,
Set<LocalDate> dodatkoweWolne) {
swietaZPamieci(start.getYear()); // sprawdza zakres również dla pustego harmonogramu
if (ileTerminow < 0 || odstep < 1) {
throw new IllegalArgumentException("Podaj nieujemną liczbę terminów i dodatni odstęp.");
}
List<LocalDate> terminy = new ArrayList<>();
LocalDate termin = start;
for (int i = 0; i < ileTerminow; i++) {
termin = dodajDniRobocze(termin, odstep, dodatkoweWolne);
terminy.add(termin);
}
return terminy;
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) {
LocalDate wielkanoc = wielkanoc(2026);
System.out.println("Wielkanoc 2026: " + wielkanoc + ", Boże Ciało: " + wielkanoc.plusDays(60));
System.out.println("Dni robocze 21.12.2026 – 5.01.2027: " + liczDniRobocze(
LocalDate.of(2026, 12, 21), LocalDate.of(2027, 1, 5), Set.of(LocalDate.of(2026, 12, 31))));
System.out.println("Harmonogram: " + harmonogram(LocalDate.of(2026, 3, 30), 4, 5, Set.of()));
}
}
Jak użyć tego skryptu
- Zapisz kod jako
KalendarzRoboczy.java— dokładnie pod nazwą klasy publicznej — i uruchom poleceniemjava KalendarzRoboczy.java. Program obliczy Wielkanoc i Boże Ciało, policzy dni robocze oraz wypisze harmonogram terminów. - Święta danego roku pobierzesz przez
swietaPolskie(rok)— mapa zawiera daty i czytelne nazwy w porządku chronologicznym, awielkanoc(rok)zwraca samą datę Niedzieli Wielkanocnej. - Do liczenia przedziału wywołaj
liczDniRobocze(od, doDnia, dodatkoweWolne). Początek jest wliczany, koniec nie. Trzeci argument to Twoje własne dni wolne — urlopy, przerwa świąteczna w firmie; jeśli żadnych nie masz, podaj pusteSet.of(), nigdynull. - Termin po określonej liczbie dni wyznacz przez
dodajDniRobocze, a całą serię przezharmonogram. Obie metody omijają weekendy, święta i daty dodatkowe — z jednym wyjątkiem: przy zerowej liczbie dnidodajDniRoboczeoddaje datę startową taką, jaką dostało.
Warto wiedzieć
- Algorytm Wielkanocy dotyczy kalendarza gregoriańskiego i kod świadomie odrzuca lata wcześniejsze niż 1583. Daty świąt prawosławnych oraz kalendarz juliański wymagają innego algorytmu.
- Lista odpowiada polskim dniom ustawowo wolnym obowiązującym od 2025 roku, w tym wolnej Wigilii 24 grudnia, dlatego metody kalendarza odrzucają wcześniejsze daty zamiast zwracać historycznie błędny wynik. Przepisy mogą się zmienić — w systemie biznesowym trzymaj kalendarz w konfiguracji i aktualizuj go niezależnie od wdrożenia kodu.
- Święto przypadające w sobotę może powodować obowiązek wyznaczenia przez pracodawcę innego dnia wolnego. Tego dnia nie da się wyliczyć z samej ustawy i daty święta (dotyczy to np. 15 sierpnia i 26 grudnia 2026 roku), dlatego przekaż go w zbiorze
dodatkoweWolne. - Przedziały są półotwarte:
liczDniRobocze(od, doDnia, ...)uwzględniaod, ale nie uwzględniadoDnia. Taka konwencja pasuje dodatesUntili zapobiega podwójnemu liczeniu sąsiednich okresów. Uważaj przy tym na zero:dodajDniRobocze(data, 0, ...)oddaje datę startową bez sprawdzania, więc jeśli podasz sobotę, sobotę dostaniesz z powrotem.
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 czasStrefy czasowe i zmiana czasu
Przeliczanie chwili między strefami na ZonedDateTime, wykrywanie regułami ZoneRules godziny, której przy wiosennej zmianie czasu nie ma, i tej padającej jesienią dwa razy, oraz pułapka skrótów CET i EST.
średni Zobacz kod →