Przejdź do treści

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.

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

  1. Zapisz kod jako KalendarzRoboczy.java — dokładnie pod nazwą klasy publicznej — i uruchom poleceniem java KalendarzRoboczy.java. Program obliczy Wielkanoc i Boże Ciało, policzy dni robocze oraz wypisze harmonogram terminów.
  2. Święta danego roku pobierzesz przez swietaPolskie(rok) — mapa zawiera daty i czytelne nazwy w porządku chronologicznym, a wielkanoc(rok) zwraca samą datę Niedzieli Wielkanocnej.
  3. 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 puste Set.of(), nigdy null.
  4. Termin po określonej liczbie dni wyznacz przez dodajDniRobocze, a całą serię przez harmonogram. Obie metody omijają weekendy, święta i daty dodatkowe — z jednym wyjątkiem: przy zerowej liczbie dni dodajDniRobocze oddaje 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ędnia od, ale nie uwzględnia doDnia. Taka konwencja pasuje do datesUntil i 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.
#java.time #localdate #dni robocze #święta

Podobne skrypty