Kalenderspielereien mit Java (3)

Bevor in diesem Beitrag die Initialisierung der Feiertagsklassen aus Teil 1 und Teil 2 besprochen werden, wird noch eine fehlende Feiertagsberechnung nachgereicht.

Im Saarland ist der Buß und Bettag ein gesetzlicher Feiertag und er fällt jedes Jahr auf den Mittwoch vor dem 23. November. Die Berechnung erscheint im ersten Moment kompliziert, mit der Java Time API benötigen wir jedoch nur drei Zeilen Code.

LocalDate base = MonthDate.of(11, 22).atYear(year);
LocalDate day = base.with(DayOfWeek.WEDNESDAY);
return day.isAfter(base) ? Day.minus(7, DAYS) : day;

Die with Methode verschiebt das Datum so, dass der gewünschte Wochentag erreicht wird. Ist der 22. November ein Mittwoch, dann gibt es keine Änderung und das gewünschte Resultat steht bereit. Ansonsten wird das Datum vor oder hinter den 22. November geschoben. Liegt das Datum vor den 22. November, dann gibt es nichts weiter zu tun. Nur nach dem 22. November sind wir eine Woche zu weit und müssen sieben Tage abziehen.

Nachdem nun auch der Buß und Bettag zu den Feiertagen hinzugefügt wurde, geht es nun an die Initialisierung der Holiday Klasse.

In der ersten Implementierung wurden die Map Instanzen mit den Feiertagsberechnungen bei jedem Aufruf in der HolidayKlasse erstellt. Das ist nicht nur schlecht zu erweitern, auch die Performance leidet darunter.

Um diesen Missstand zu beseitigen wird der Code zur Generierung der deutschen Feiertags-Formeln in eine eigene Klasse GermanHolidaysProvider ausgegliedert. Um auch die Feiertage anderer Länder zu generieren, implementiert die neue Klasse das Interface HolidaysProvider.

public interface HolidaysProvider {
  Map<String, Function<Integer, LocalDate>> create(Locale locale);

  Map<String, Function<Integer, LocalDate>> create(SubDivision locale);

  Map<String, Function<Integer, LocalDate>> create(Locode locale);
}

Die drei Methoden erstellen jeweils eine Map für die, auf der jeweiligen Ebene definierten Feiertage. Bei einem Locale also nur die landesweiten Feiertagen, bei einer SubDivison nur die Feiertage zu Provinz/Bundesland/Kanton ohne die jeweiligen landesweiten Feiertage. Bei dem Locode Parameter sind es die Feiertage ohne Feiertage der beiden übergeordneten Strukturen.

Die Map Strukturen werden von zwei Unterklassen der Holidays Klasse verwendet um die jeweils alle aktuellen Feiertage für eine Region zu ermitteln. Die MapHolidays ist ein einfacher Wrapper, der aus der Mapund dem übergebenen Jahr die Feiertage berechnet.

<code>public Map<LocalDate, String> getHolidays(int year)</code>
  Map<LocalDate, String> result = new HashMap<>();
  for (Entry<String, Function<Integer, LocalDate>> entry :  functions.entrySet()) {
    LocalDate key = entry.getValue().apply(year);
    if (key != null) {
      result.put(key, entry.getKey());
    }
  }
  return result;
}

Die BackedHolidays Klasse ist eine Erweiterung der MapHolidays Klasse und prüft gegen die selbstberechneten Feiertage oder gegen die Feiertage einer anderen MapHolidays Instanz.

public boolean isHoliday(LocalDate date) {
  return super.isHoliday(date) || backedBy.isHoliday(date);
}

public Optional&lt;String&gt; getHoliday(LocalDate date) {
  Optional<String> result = Optional.ofNullable(super.getHolidays(date.getYear()).get(date));
  return result.isPresent() ? result : backedBy.getHoliday(date);
}

Die andere Instanz ist dabei jeweils die der übergeordneten Struktur. Für eine Gemeinde, die des Bundeslandes und für ein Bundesland, die des Landes. Diese Instanzen werden zusätzlich in einer Map gespeichert, damit nachfolgende Aufrufe der Holidays Klasse diese nicht immer und immer wieder erzeugen müssen. Die Regeln für die landesweiten Feiertage werden also nur ein einziges Mal erstellt und dann den Feiertagen für Bundesländer und Gemeinden hinzugefügt.

Eine weitere kleine Ergänzung ist die vorberechnung der Ostertage. Die Implementierung dieser kleinen Optimierung wurde bei den Number Klassen abgeschaut. Bei Zahlen zwischen -128 und 128 werden mit valueOf nicht immer neue Instanzen erzeugt, sondern Instanzen aus einem Cache verwendet.

Der verwendete EasterCache berechnet 100 Ostertage und speichert sie in einem Array.

class EasterCache {
  static final int MIN = 2000;
  static final int MAX = 2100;

  static final LocalDate[] cache;
  static {
    cache = IntStream.range(MIN, MAX).mapToObj(year -&gt; gauss2(year, 5, 24)).toArray(LocalDate[]::new);
  }

  static LocalDate gauss(int year) {
    if (year &lt; EasterCache.MIN || year &gt;= EasterCache.MAX) {
      return EasterCache.gauss2(year);
    }
    return EasterCache.cache[year - EasterCache.MIN];
  }

  static LocalDate gauss2(int year) {
    int h1 = year / 100;
    int h2 = year / 400;
    int n = 4 + h1 - h2;
    int m = 15 + h1 - h2 - (8 * h1 + 13) / 25;
    return gauss2(year, n, m);
  }

  static LocalDate gauss2(int year, int n, int m) {
    int a = year % 19;
    int b = year % 4;
    int c = year % 7;
    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);
  }
}

Die 100 Ostertage wurden u.a. auch so gewählt, weil die Konstanten N und M der Gauß-Formel nicht berechnet werden müssen sondern immer 5 und 24 sind. Es hätte auch die gesamte Berechnung entfallen können, in dem für die Ostertage der Epoch Day (Offset zum 1. Januar 1970) zu Berechnung bereitgestellt wird.

static {
  cache = IntStream.range(EPOCH_DAYS).mapToObj(LocalDate::ofEpochDate).toArray(LocalDate[]::new);
}

Bislang werden die meisten Feiertage aus Deutschland und Österreich unterstützt. Märiä Himmelfahr in Bayern und Fronleichnam in Sachsen und Thüringen werden nicht unterstützt. Bei ersteren ist die Pflege der statistisch-katholischen Gemeinden ein Hindernis und bei den letzteren sind einige Gemeinden nur mit Stadtteilen vertreten.

Das Framework kann in eigenen Projekten um zusätzliche Feiertage ergänzt werden, in dem HolidaysProvider z.B. für polnische oder belgische Feiertage implementiert werden. Da der Java ServiceLoader verwendet wird um die HolidayProvider zu laden, genügt der vollständige Klassenname der Implementierung in der Datei META-INF/services/de.schegge.holidays.HolidaysProvider im eigenen Projekt.

Die aktuellen Sourcen zum Framework sind im Projekt Holiday auf gitlab zu finden.

Nachtrag

Im Zuge der Ankündigung von JFrog, ihr Repository JCenter zu schließen, ist die Bibliothek als Java 11 Binary nach Maven Central migriert.

<dependency>
  <groupId>de.schegge</groupId>
  <artifactId>holidays</artifactId>
  <version>0.1.7</version>
</dependency>