package de.duehl.basics.datetime.date;

/*
 * Copyright 2025 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.util.List;

import de.duehl.basics.text.xml.own.OwnXmlHelper;
import de.duehl.basics.text.xml.own.XmlStorable;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.regex.RegexHelper;

import static de.duehl.basics.datetime.date.DateCalculations.*;

/**
 * Diese Klasse stellt ein nicht veränderbares Datum dar und bietet Methoden
 * rund um dieses an.                                                            <br><br>
 *
 * Im Gegensatz zu MySimpleDate ist der Zustand dieser Klasse unveränderlich
 * (oder immutual, d.h. alle Variablen sind final). Daher geben Methoden zum
 * Hinzufügen von Tagen neue Objekte zurück, ähnlich wie bei der Klasse String.
 *
 * @version 1.01     2025-12-18
 * @author Christian Dühl
 */

public class ImmutualDate implements Comparable<ImmutualDate>, XmlStorable {

    public static final String XML_IMMUTUAL_DATE_TAG = "immutual-date";

    /** Deutschsprachige Monatsnamen. */
    public static final List<String> GERMAN_MONTH_NAMES = CollectionsHelper.buildListFrom("Januar",
            "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober",
            "November", "Dezember");

    /** Kurze Deutschsprachige Monatsnamen. */
    public static final List<String> SHORT_GERMAN_MONTH_NAMES = CollectionsHelper.buildListFrom(
            "Jan.", "Feb.", "März", "Mär.", "Apr.", "Mai", "Jun.", "Jul.", "Aug.", "Sep.", "Sept.",
            "Okt.", "Nov.", "Dez.");

    /** Ein regulärer Ausdruck der auf einen der deutschsprachigen Monatsnamen passt. */
    public static final String GERMAN_MONTH_NAMES_REGEX =
            RegexHelper.buildOrRegexFromList(GERMAN_MONTH_NAMES);

    /** Ein regulärer Ausdruck der auf einen der kurzen deutschsprachigen Monatsnamen passt. */
    public static final String SHORT_GERMAN_MONTH_NAMES_REGEX =
            RegexHelper.buildOrRegexFromList(SHORT_GERMAN_MONTH_NAMES);



    public static final String YEAR_REGEX = "\\d{4}";

    /** Ein regulärer Ausdruck für ein Datum der Art "DD.MM.YYYY" oder auch "D.MM.YYYY". */
    public static final String DATE_REGEX = "\\d{1,2}\\.\\d{1,2}\\." + YEAR_REGEX;

    /** Ein regulärer Ausdruck für ein Datum der Art "DD.MM.YYYY" oder auch "D.MM.YYYY". */
    private static final String DATE_REGEX_WITH_BLANKS = "\\d{1,2}\\. ?\\d{1,2}\\. ?" + YEAR_REGEX;

    /** Ein regulärer Ausdruck für ein Datum in der Form "DD.MM. / DD.MM.YYYY". */
    public static final String COMPLICATED_DATE_REGEX = ""
            + "(\\d{2}\\.\\d{2}\\.)"
            + " ?(?:/|und) ?"
            + "\\d{2}\\.\\d{2}\\.(" + YEAR_REGEX + ")";

    /** Ein regulärer Ausdruck für ein Datum der Art "Januar YYYY". */
    public static final String DATE_WITH_GERMAN_MONTH_NAMES_WITHOUT_DAY_REGEX =
            GERMAN_MONTH_NAMES_REGEX + " " + YEAR_REGEX;

    /** Ein regulärer Ausdruck für ein Datum der Art "DD. Januar YYYY" oder auch "D. Januar YYYY". */
    public static final String DATE_WITH_GERMAN_MONTH_NAMES_REGEX =
            "\\d{1,2}\\. " + DATE_WITH_GERMAN_MONTH_NAMES_WITHOUT_DAY_REGEX;

    /** Ein regulärer Ausdruck für ein Datum der Art "Jan. YYYY". */
    public static final String DATE_WITH_SHORT_GERMAN_MONTH_MONTH_NAMES_WITHOUT_DAY_REGEX =
            SHORT_GERMAN_MONTH_NAMES_REGEX + " " + YEAR_REGEX;

    /** Ein regulärer Ausdruck für ein Datum der Art "DD. Jan. YYYY" oder auch "D. Jan. YYYY". */
    public static final String DATE_WITH_SHORT_GERMAN_MONTH_NAMES_REGEX =
            "\\d{1,2}\\. " + DATE_WITH_SHORT_GERMAN_MONTH_MONTH_NAMES_WITHOUT_DAY_REGEX;


    /**
     * Ein regulärer Ausdruck für ein vollständiges Datum irgendeiner Art, wenn man die gefangenen
     * Gruppen nicht verwenden will. Diese sind hier nicht verlässlich.
     */
    public static final String ANY_FULL_DATE_REGEX = ""
            + "(?:"
            + COMPLICATED_DATE_REGEX
            + "|"
            + DATE_REGEX
            + "|"
            + DATE_REGEX_WITH_BLANKS
            + "|"
            + DATE_WITH_GERMAN_MONTH_NAMES_REGEX
            + "|"
            + DATE_WITH_SHORT_GERMAN_MONTH_NAMES_REGEX
            + ")"
            ;

    /**
     * Ein regulärer Ausdruck für ein Datum irgendeiner Art, wenn man die gefangenen Gruppen nicht
     * verwenden will. Diese sind hier nicht verlässlich.
     */
    public static final String ANY_DATE_REGEX = ""
            + "(?:"
            + COMPLICATED_DATE_REGEX
            + "|"
            + DATE_REGEX
            + "|"
            + DATE_REGEX_WITH_BLANKS
            + "|"
            + DATE_WITH_GERMAN_MONTH_NAMES_REGEX
            + "|"
            + DATE_WITH_SHORT_GERMAN_MONTH_NAMES_REGEX
            + "|"
            + DATE_WITH_GERMAN_MONTH_NAMES_WITHOUT_DAY_REGEX
            + "|"
            + YEAR_REGEX
            + ")"
            ;

    /** Tag des Monats von 1 bis 31. */
    private final int day;

    /** Monat von 1 (Januar) bis 12 (Dezember). */
    private final int month;

    /** Jahr als vierstellige Zahl (2012). */
    private final int year;

    /**
     * Konstruktor
     *
     * @param day
     *            Tag des Monats von 1 bis 31.
     * @param month
     *            Monat von 1 (Januar) bis 12 (Dezember).
     * @param year
     *            Jahr als vierstellige Zahl (2012).
     */
    public ImmutualDate(int day, int month, int year) {
        this.day   = day;
        this.month = month;
        this.year  = year;
    }

    /**
     * Konstruktor
     *
     * @param date
     *            Datum in bestimmtem String-Format.
     */
    public ImmutualDate(String date) {
        this(parseDate(date));
    }

    /** Konstruktor, erzeugt Datumsobjekt mit dem heutigen Datum. */
    public ImmutualDate() {
        this(parseDate(new java.util.Date().toString()));
    }

    /**
     * Konstruktor.
     *
     * @param that
     *            Objekt, dessen Datum zu übernehmen ist.
     */
    private ImmutualDate(ImmutualDate that) {
        this.day = that.day;
        this.month = that.month;
        this.year = that.year;
    }

    /** Getter für den Tag des Monats von 1 bis 31. */
    public int getDay() {
        return day;
    }

    /** Getter für den Monat von 1 (Januar) bis 12 (Dezember). */
    public int getMonth() {
        return month;
    }

    /** Getter für das Jahr als vierstellige Zahl (2012). */
    public int getYear() {
        return year;
    }

    /**
     * Testet, ob das Datum zulässig ist. Das Jahr muss zwischen 1000 und 9999 liegen, der Monat
     * zwischen 1 und 12 und der Tag zwischen 1 und dem zum Monat und Jahr passenden Anzahl an
     * Tagen des Monats.
     *
     * @return Wahrheitswert: true genau dann, wenn das Datum nach den obigen Kriterien valide ist.
     */
    public boolean isValid() {
        /* Jahr ist zu klein oder zu groß: */
        if (year < 1000 || year > 9999) {
            return false;
        }

        /* Monat ist zu klein oder zu groß: */
        if (month < 1 || month > 12) {
            return false;
        }

        /* Tag ist ist zu klein oder zu groß: */
        if (day < 1 || day > monthDays(month, year)) {
            return false;
        }

        return true;
    }

    /**
     * Testet, ob das Datum zulässig ist. Das Jahr muss zwischen 0 und 9999 liegen, der Monat
     * zwischen 1 und 12 und der Tag zwischen 1 und dem zum Monat und Jahr passenden Anzahl an
     * Tagen des Monats.
     *
     * @return Wahrheitswert: true genau dann, wenn das Datum nach den obigen Kriterien valide ist.
     */
    public boolean isValidWithYearZero() {
        /* Jahr ist zu klein oder zu groß: */
        if (year < 0 || year > 9999) {
            return false;
        }

        /* Monat ist zu klein oder zu groß: */
        if (month < 1 || month > 12) {
            return false;
        }

        /* Tag ist ist zu klein oder zu groß: */
        if (day < 1 || day > monthDays(month, year)) {
            return false;
        }

        return true;
    }

    /**
     * Testet, ob das Datum in einem Schaltjahr liegt.
     */
    public boolean isLeapYear() {
        return isLeapYear(year);
    }

    /**
     * Testet, ob das übergeben Jahr ein Schaltjahr ist.
     *
     * @param year
     *            Zu testendes Jahr.
     */
    private static boolean isLeapYear(int year) {
        return DateCalculations.isLeapYear(year);
    }

    /**
     * Berechnet den Abstand zweier Datumswerte in Tagen.
     *
     * Wenn der andere Zeitpunkt später ist, ist die Differenz positiv, ist der andere Zeitpunkt
     * früher, ist sie negativ.
     *
     * @param that
     *            Anderes Datum als MySimpleDate-Objekt, zu dem der Abstand berechnet werden soll.
     */
    public int difference(ImmutualDate that) {
        return calculateDayDifference(that);
    }

    /**
     * Berechnet den Abstand zweier Datumswerte in Tagen.
     *
     * Wenn der andere Zeitpunkt später ist, ist die Differenz positiv, ist der andere Zeitpunkt
     * früher, ist sie negativ.
     *
     * @param that
     *            Anderes Datum als MySimpleDate-Objekt, zu dem der Abstand berechnet werden soll.
     */
    int calculateDayDifference(ImmutualDate that) {
        int thisDay   = this.getDay();
        int thatDay   = that.getDay();
        int thisMonth = this.getMonth();
        int thatMonth = that.getMonth();
        int thisYear  = this.getYear();
        int thatYear  = that.getYear();

        int abstand   = 0;


        /*
         *  Tagesdifferenz:
         */
        abstand = thisDay - thatDay;
        thisDay = thatDay;

        /*
         *  Monatsdifferenz:
         */
        while (thisMonth > thatMonth) {
            abstand += monthDays(thatMonth, thatYear);
            ++thatMonth;
        }
        while (thisMonth < thatMonth) {
            abstand -= monthDays(thisMonth, thisYear);
            ++thisMonth;
        }

        /*
         *  Jahresdifferenz:
         */
        while (thisYear > thatYear) {
            if (isLeapYear(thatYear)   && thisMonth <  3 ||
                isLeapYear(thatYear+1) && thisMonth >= 3)
            {
                abstand += 366;
            }
            else {
                abstand += 365;
            }
            ++thatYear;
        }
        while (thisYear < thatYear) {
            if (isLeapYear(thisYear)   && thisMonth <  3 ||
                isLeapYear(thisYear+1) && thisMonth >= 3)
            {
                abstand -= 366;
            }
            else {
                abstand -= 365;
            }
            ++thisYear;
        }

        return -abstand;
    }

    /**
     * Addiert (oder subtrahiert bei negativen Zahlen) die angegebene Anzahl Tage zum Datum.
     *
     * @param delta
     *            Anzahl Tage, die addiert (oder bei negativer Anzahl subtrahiert) werden sollen.
     * @return Nach Addition oder Subtraktion normalisiertes Datum.
     */
    public ImmutualDate addDays(int delta) {
        return normalise(day + delta, month, year);
    }

    /**
     * Addiert (oder subtrahiert bei negativen Zahlen) die angegebene Anzahl Monate zum Datum.
     *
     * @param numberOfMonths
     *            Anzahl Monate, die addiert (oder bei negativer Anzahl subtrahiert) werden sollen.
     * @return Nach Addition oder Subtraktion normalisiertes Datum.
     */
    public ImmutualDate addMonths(int numberOfMonths) {
        boolean subtract = false;

        int editedNumberOfMonth = numberOfMonths;
        if (editedNumberOfMonth < 0) {
            subtract = true;
            editedNumberOfMonth = -editedNumberOfMonth;
        }

        int newDay = day;
        int tempMonth = month;
        int tempYear = year;

        for (int i = 0; i < editedNumberOfMonth; ++i) {
            int numberOfDays = monthDays(tempMonth, tempYear);
            if (subtract) {
                newDay -= numberOfDays;
                --tempMonth;
                if (tempMonth == 0) {
                    tempMonth = 12;
                    --tempYear;
                }
            }
            else {
                newDay += numberOfDays;
                ++tempMonth;
                if (tempMonth == 13) {
                    tempMonth = 1;
                    ++tempYear;
                }
            }
        }

        return normalise(newDay, month, year);
    }

    /**
     * Addiert (oder subtrahiert bei negativen Zahlen) die angegebene Anzahl Jahre zum Datum.
     *
     * @param numberOfYears
     *            Anzahl Jahre, die addiert (oder bei negativer Anzahl subtrahiert) werden sollen.
     * @return Nach Addition oder Subtraktion normalisiertes Datum.
     */
    public ImmutualDate addYears(int numberOfYears) {
        return normalise(day, month, year + numberOfYears);
    }

    /**
     * Normalisiert das Datum, zu kleine Tage oder zu große Tage werden auf andere Monate oder
     * Jahre umberechnet.
     */
    public ImmutualDate normalise() {
        return normalise(this);
    }

    /**
     * Normalisiert das Datum, zu kleine Tage oder zu große Tage werden auf andere Monate oder
     * Jahre umberechnet.
     *
     * @param immutualDate
     *            Das Datum, das normalisiert werden soll.
     */
    public static ImmutualDate normalise(ImmutualDate immutualDate) {
        int day = immutualDate.getDay();
        int month = immutualDate.getMonth();
        int year = immutualDate.getYear();

        return normalise(day, month, year);
    }

    /**
     * Normalisiert das Datum, zu kleine Tage oder zu große Tage werden auf andere Monate oder
     * Jahre umberechnet.
     *
     * @param day
     *            Tag des Monats von 1 bis 31. Abweichende Werte werden normalisiert.
     * @param month
     *            Monat von 1 (Januar) bis 12 (Dezember). Abweichende Werte werden normalisiert.
     * @param year
     *            Jahr als vierstellige Zahl (2012).
     */
    public static ImmutualDate normalise(int day, int month, int year) {
        int normalisedDay = day;
        int normalisedMonth = month;
        int normalisedYear = year;

        /* negatives Delta an Monaten: */
        while (normalisedMonth < 1) {
            normalisedMonth += 12;
            --normalisedYear;
        }
        // 12.00.2011 -> 12.12.2010
        // 23.-1.2012 -> 23.11.2011

        /* positives Delta an Monaten: */
        while (normalisedMonth > 12) {
            normalisedMonth -= 12;
            ++normalisedYear;
        }
        // 03.13.2011 -> 03.01.2012

        /* negatives Delta an Tagen: */
        while (normalisedDay < 1) {
            --normalisedMonth;
            if (normalisedMonth == 0) {
                normalisedMonth = 12;
                --normalisedYear;
            }
            normalisedDay += monthDays(normalisedMonth, normalisedYear);
        }
        // 00.05.2003 -> 30.04.2003
        // -3.05.2003 -> 27.04.2003

        /* positives Delta: */
        while (normalisedDay > monthDays(normalisedMonth, normalisedYear)) {
            normalisedDay -= monthDays(normalisedMonth, normalisedYear);
            ++normalisedMonth;
            if (normalisedMonth == 13) {
                normalisedMonth = 1;
                ++normalisedYear;
            }
        }
        // 32.05.2003 -> 01.06.2003

        return new ImmutualDate(normalisedDay, normalisedMonth, normalisedYear);
    }

    /**
     * Berechnet den Wochentag des Datums.
     *
     * @return Wochentag
     */
    public Weekday dayOfTheWeek() {
        return DateCalculations.dayOfTheWeek(day, month, year);
    }

    /**
     * Ermittelt, ob das Datum ein Arbeitstag ist.
     *
     * @return Wahrheitswert
     */
    public boolean isWorkDay() {
        return !isWeekend() && !isHoliday();
    }

    /**
     * Ermittelt, ob das Datum am Wochenende liegt, also ein Samstag oder Sonntag ist.
     *
     * @return Wahrheitswert
     */
    public boolean isWeekend() {
        return isWeekend(dayOfTheWeek());
    }

    /**
     * Ermittelt, ob der Wochentag am Wochenende liegt, also ein Samstag oder Sonntag ist.
     *
     * @return Wahrheitswert
     */
    public static boolean isWeekend(Weekday weekday) {
        return weekday == Weekday.SATURDAY || weekday == Weekday.SUNDAY;
    }

    /**
     * Ermittelt, ob das Datum ein Feiertag ist.
     *
     * @return Wahrheitswert
     */
    public boolean isHoliday() {
        /*
         * Fixe Feiertage:
         */
        if (
            day ==  1 && month ==  1 || // Neujahr
          //day ==  6 && month ==  1 || // Dreikönigstag
            day ==  1 && month ==  5 || // Tag der Arbeit
          //day == 19 && month ==  6 || // Fronleichnam ist nicht fest, sondern beweglich!
            day == 15 && month ==  8 || // Mariae Himmelfahrt
            day ==  3 && month == 10 || // Tag der deutschen Einheit
            day ==  1 && month == 11 || // Allerheiligen
            day == 24 && month == 12 || // Weihnachten
            day == 25 && month == 12 || // Erster Weihnachtstag
            day == 26 && month == 12 || // Zweiter Weihnachtstag
            day == 31 && month == 12    // Silvester
        ) {
            return true;
        }

        /*
         * Bewegliche Feiertage:
         */

        /* Ostersonntag: */
        ImmutualDate easter = calculateEasterSunday(year);

        /* Ostermontag: */
        ImmutualDate easterMonday = easter.addDays(1);

        /* Karfreitag: */
        ImmutualDate goodFriday = easter.addDays(-2);

        /* Christi Himmelfahrt: */
        ImmutualDate ascensionDay = easter.addDays(39);

        /* Pfingstmontag: */
        ImmutualDate whitMonday = easter.addDays(50);

        /* Fronleichnam: */
        ImmutualDate corpusChristi = easter.addDays(60);

        if (this.equals(easter)
                || this.equals(easterMonday)
                || this.equals(goodFriday)
                || this.equals(ascensionDay)
                || this.equals(whitMonday)
                || this.equals(corpusChristi)
                ) {
            return true;
        }

        return false;
    }

    /**
     * Ermittelt, ob das Datum vor dem angegebenen Datum liegt.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false zurückgegeben.
     */
    public boolean before(ImmutualDate that) {
        if (this.year < that.year)
            return true;
        if (this.year > that.year)
            return false;

        if (this.month < that.month)
            return true;
        if (this.month > that.month)
            return false;

        if (this.day < that.day)
            return true;
        return false;
    }

    /**
     * Ermittelt, ob das Datum vor dem angegebenen Datum liegt oder diesem gleicht.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false zurückgegeben.
     */
    public boolean beforeOrEqual(ImmutualDate that) {
        if (this.year < that.year)
            return true;
        if (this.year > that.year)
            return false;

        if (this.month < that.month)
            return true;
        if (this.month > that.month)
            return false;

        if (this.day <= that.day)
            return true;
        return false;
    }

    /**
     * Ermittelt, ob das Datum nach dem angegebenen Datum liegt.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false zurückgegeben.
     */
    public boolean after(ImmutualDate that) {
        return that.before(this);
    }

    /**
     * Ermittelt, ob das Datum nach dem angegebenen Datum liegt oder diesem gleicht.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false zurückgegeben.
     */
    public boolean afterOrEqual(ImmutualDate that) {
        return that.beforeOrEqual(this);
    }

    /**
     * Ermittelt, ob das Datum im Bereich der beiden angegebenen Datumswerte liegt.
     *
     * @param first
     *            Erster erlaubter Datumswert.
     * @param last
     *            Letzter erlaubter Datumswert.
     * @return Wahrheitswert.
     */
    public boolean between(ImmutualDate first, ImmutualDate last) {
        if (!first.beforeOrEqual(last)) {
            throw new IllegalArgumentException("Der Übergebene Datumsbereich "
                    + "von " + first + " bis " + last
                    + " ist kein gültiger Bereich.");
        }
        return afterOrEqual(first) && beforeOrEqual(last);
    }


    /**
     * Berechnet den Abstand zu dem übergebenen Datum in Tagen. Der Abstand zu einem Datum, das
     * weiter in der Zukunft liegt als dieses, ist positiv, der Abstand zu einem Datum in der
     * Vergangenheit ist negativ.
     *
     * @param that
     *            Das Datum mit dem verglichen wird.
     * @return Differenz in Tagen.
     */
    int calculateDayDistanceTo(ImmutualDate that) {
        /* Vorarbeit: Bestimmung des früheren und späteren Datums */
        int earlyYear;
        int earlyMonth;
        int earlyDay;
        int lateYear;
        int lateMonth;
        int lateDay;
        boolean switchedDates;
        if (before(that)) {
            earlyYear  = this.getYear();
            earlyMonth = this.getMonth();
            earlyDay   = this.getDay();
            lateYear   = that.getYear();
            lateMonth  = that.getMonth();
            lateDay    = that.getDay();
            switchedDates = false;
        }
        else {
            earlyYear  = that.getYear();
            earlyMonth = that.getMonth();
            earlyDay   = that.getDay();
            lateYear   = this.getYear();
            lateMonth  = this.getMonth();
            lateDay    = this.getDay();
            switchedDates = true;
        }

        int distance = 0;

        /* 1. beide Datumswerte auf den 1. des Monats bringen: */
        distance -= (earlyDay - 1);
        earlyDay = 1;
        distance += (lateDay - 1);
        lateDay = 1;

        /* 2. beide Datumswerte auf Januar bringen: */
        while (earlyMonth > 1) {
            --earlyMonth;
            distance -= monthDays(earlyMonth, earlyYear);
        }
        while (lateMonth > 1) {
            --lateMonth;
            distance += monthDays(lateMonth, lateYear);
        }

        /* 3. früheres Jahr auf das spätere bringen: */
        while (earlyYear < lateYear) {
            if (isLeapYear(earlyYear)) {
                distance += 366;
            }
            else {
                distance += 365;
            }
            ++earlyYear;
        }

        /*
         * 4. Falls die Datumswerte am Anfang vertauscht wurden, distance mit
         * -1 multiplizieren:
         */
        if (switchedDates) {
            distance *= -1;
        }

        return distance;
    }

    /** Stringrepräsentation im Format "DD.MM.YYYY". */
    @Override
    public String toString() {
        return String.format("%02d.%02d.%04d", day, month, year);
    }

    /** Interne Stringrepräsentation im Format "YYYY/MM/DD". */
    public String toStringInternational() {
        return String.format("%04d/%02d/%02d", year, month, day);
    }

    /** Stringrepräsentation im Format 20130722. */
    public String asYyyyMmDd() {
        return String.format("%04d%02d%02d", year, month, day);
    }

    /** Stringrepräsentation im Format 2013-07-22. */
    public String asYyyyMinusMmMinusDd() {
        return String.format("%04d-%02d-%02d", year, month, day);
    }

    /** Stringrepräsentation im Format 22072013. */
    public String asDdMmYyyy() {
        return String.format("%02d%02d%04d", day, month, year);
    }

    /** Gibt den Monat als die ersten drei Buchstaben des englischen Monatsnamens zurück. */
    public String getMonthAsFirstThreeLettersOfEnglishName() {
        switch (month) {
            case  1 : return "JAN";
            case  2 : return "FEB";
            case  3 : return "MAR";
            case  4 : return "APR";
            case  5 : return "MAY";
            case  6 : return "JUN";
            case  7 : return "JUL";
            case  8 : return "AUG";
            case  9 : return "SEP";
            case 10 : return "OCT";
            case 11 : return "NOV";
            case 12 : return "DEC";
            default: throw new RuntimeException("Monat " + month + " im falschen Bereich!");
        }
    }

    /** Stringrepräsentation im Format 17JAN2020 für z.B. Sas. */
    public String toSasFormat() {
        return String.format("%d%s%d", day, getMonthAsFirstThreeLettersOfEnglishName(), year);
    }

    /** Berechnung des Hash-Codes. */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + day;
        result = prime * result + month;
        result = prime * result + year;
        return result;
    }

    /** Bestimmt, wann zwei Objekte als gleich gelten sollen. */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ImmutualDate other = (ImmutualDate) obj;
        if (day != other.day)
            return false;
        if (month != other.month)
            return false;
        if (year != other.year)
            return false;
        return true;
    }

    /**
     * Vergleicht ein Datum mit einem anderen Datum
     *
     * @param that
     * @return Negative Zahl, wenn dieses Datum vor dem anderen Datum liegt, 0 wenn beide
     *         Datumswerte den gleichen Tag bezeichnen und positive Zahl, wenn dieses Datum nach
     *         dem anderen Datum liegt.
     */
    @Override
    public int compareTo(ImmutualDate that) {
        return -calculateDayDistanceTo(that);
    }

    /** Erzeugt den nächsten Arbeitstag nach diesem Datum. */
    public ImmutualDate generateNextWorkDay() {
        ImmutualDate nextWorkDay = addDays(1);
        while (!nextWorkDay.isWorkDay()) {
            nextWorkDay = nextWorkDay.addDays(1);
        }
        return nextWorkDay;
    }

    /** Erzeugt eine Darstellung im XML für die Persistenz. */
    @Override
    public String toXml() {
        return "<" + XML_IMMUTUAL_DATE_TAG + ">" + toString() + "</" + XML_IMMUTUAL_DATE_TAG + ">";
    }

    /** Erzeugt einen Datumswert aus einer oder mehrere Zeilen aus einer XML-Datei. */
    public static ImmutualDate createFromXml(String line) {
        String date = OwnXmlHelper.readContentOfSingleUniqueXmlElementInXmlLine(
                XML_IMMUTUAL_DATE_TAG, line);
        return new ImmutualDate(date);
    }

    /**
     * Gibt den letzten übergebenen Wochentag vor dem Datum zurück.
     *
     * Beim gleichen Wochentag bekommt man das Datum vor einer Woche zurück.
     *
     * Startet man mit dem 01.12.2021, einem Mittwoch, und übergibt MONDAY, so wird der 28.11.2021
     * zurückgeliefert.
     *
     * @param weekday
     *            Letzter Wochentag vor dem Datum.
     * @return Datum des letzten übergebenen Wochentages vor dem Datum.
     */
    public ImmutualDate getWeekdayBefore(Weekday weekday) {
        Weekday actualWeekday = dayOfTheWeek();
        int actualWeekdayCode = actualWeekday.getDayCode();
        int wantedWeekdayCode = weekday.getDayCode();

        int dayDifference;
        if (wantedWeekdayCode < actualWeekdayCode) {
            dayDifference = actualWeekdayCode - wantedWeekdayCode;
        }
        else {
            dayDifference = 7 + actualWeekdayCode - wantedWeekdayCode;
        }

        int minusDays = -dayDifference;
        return addDays(minusDays);
    }

    /** Gibt zurück, ob das Datum das Datum von heute ist. */
    public boolean isToday() {
        return this.equals(new ImmutualDate());
    }

    /** Gibt den Monat als deutschen Monatsnamens zurück. */
    public String getMonthAsGermanWord() {
        if (month < 1 || month > 12) {
            throw new RuntimeException("Monat " + month + " im falschen Bereich!");
        }
        return GERMAN_MONTH_NAMES.get(month - 1);
    }

    /*

    Achtung wir haben eine doppelte Methode...
        calculateDayDistanceTo()
    und
        calculateDayDifference()
    !

    Nach außen aber nur eine: difference()

    */

}
