Kalenderspielereien mit Java

„Nichts ist getan, wenn noch etwas zu tun übrig ist.“

Carl Friedrich Gauß

Hin und wieder muss ein Entwickler überprüfen, ob ein bestimmtes Datum ein Feiertag ist oder nicht. Erstaunlicherweise bieten die Java Standardbibliotheken zum Thema Feiertage recht wenig. Nicht einmal eine Methode zur Osterberechnung ist zu finden.

Dabei wäre es doch sehr angenehm ein Datum wie im folgenden Beispiel dargestellt auf einen möglichen Feiertag zu prüfen.

Holidays.in(Locale.GERMANY).isHoliday(LocalDate.of(2020, 4, 10));

Da der Karfreitag auf den 10. April 2020 fällt, liefert die Methode isHoliday hier den Wert true zurück.

Unter den Feiertagen gibt es die angenehmen Gesellen, die immer auf ein festes Datum fallen und die unangenehmen, meist religiösen Burschen, die Jahr für Jahr im Kalender herumrutschen.

Feiertage mit einem festen Datum lassen sich in Java mit der Klasse MonthDay darstellen. Folgende Zeilen prüfen ein Datum date vom Typ LocalDate gegen Neujahr.

MonthDay.parse("--01-01").atYear(date.getYear()).equals(date);

Mit der parse Methode wird eine neue MonthDay Instanz für den 1. Januar erzeugt. Aus der Instanz wird mit der Jahresangabe im date Parameter eine neue LocalDate Instanz erzeugt und die beiden dann auf Gleichheit geprüft.

Einige Feiertage hängen vom ersten Sonntag nach dem ersten Vollmond im Frühling ab. Dies ist der Ostersonntag und relativ zu ihm liegen Christi Himmelfahrt, Pfingsten und Fronleichnam.

Die von Carl Friedrich Gauß ersonnene Osterformel ist vermutlich allen Informatik-Schülern und -Studenten bekannt.

static LocalDate gauss(int year) {
  int a = year % 19;
  int b = year % 4;
  int c = year % 7;
  int H1 = year / 100;
  int H2 = year / 400;
  int N = 4 + H1 - H2;
  int M = 15 + H1 - H2 - (8 * H1 + 13) / 25;
  int d = (19 * a + M) % 30;
  int e = (2 * b + 4 * c + 6 * d + N) % 7;
  int f = (c + 11 * d + 22 * e) / 451;
  int tage = 22 + d + e - 7 * f;
  return LocalDate.of(year, 3, 1).plus(tage - 1, DAYS);
}

Interessant ist hier der Einsatz der Methode plus der Klasse LocalDate. Häufig wird geprüft, ob der Wert tage größer als 31 ist, da dann der Ostertermin, statt im März im April liegt. Beim Einsatz der LocalDate Klasse wird die Anzahl der Tage um 1 reduziert auf den 1. März addiert um den Ostersonntag zu erhalten. Bei Werten über 30 liegt das Datum dann automatisch im April.

Die Feiertage, die relativ zu Ostersonntag liegen, prüft man durch das addieren ihres Tages Offsets auf den Ostersonntag und den Vergleich mit dem zu prüfenden Datum.

gauss(date.getYear()).plus(offset, DAYS).equals(date);

Um alle Feiertagsberechnungen für eine Region zentral zu speichern, werden sie als Function Objekte in einer Map gespeichert.

Map<String, Function<Integer, LocalDate>> holidays = new HashMap<>();
holidays.put("Neujahr", MonthDay.parse("--01-01").atYear(year));
holidays.put("Karfreitag", year -> gauss(year).plus(-2, DAYS));
holidays.put("Ostermonntag", year -> gauss(year).plus(1, DAYS));
holidays.put("Himmelfahrt", year -> gauss(year).plus(39, DAYS));
holidays.put("Pfingsten", year -> gauss(year).plus(49, DAYS));
holidays.put("1. Mai", MonthDay.parse("--05-01").atYear(year));
holidays.put("1. Weihnachtstag", MonthDay.parse("--12-25").atYear(year));
holidays.put("2. Weihnachtstag", MonthDay.parse("--12-26").atYear(year));

Mit Hilfe der obigen Map können nun alle gesamtdeutschen Feiertage für ein spezielles Jahr berechnet werden.

public Map<LocalDate, String> getHolydays(int year) {
  return functions.entrySet().stream()
    .collect(toMap(e -> e.getValue().apply(year), e -> e.getKey()));
}

Die recht triviale Umsetzung der Methode isHoliday ist die Prüfung, ob das Datum in der berechneten Map enthalten ist.

public boolean isHoliday(LocalDate date) {
  return getHolidays(date.getYear()).containsKey(date);
}

Leider gelten nicht alle Feiertage für das ganze Land. Wie man den Framework entsprechend anpassen kann, folgt in einem weiteren Beitrag.