/* Zeitrechnung - a class library to determine calendar events Copyright (c) 1984-2023 Ulrich Hilger, http://uhilger.de This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package de.uhilger.zeitrechnung.kalender; import de.uhilger.zeitrechnung.ChinesischesDatum; import de.uhilger.zeitrechnung.Datum; import de.uhilger.zeitrechnung.Definition; import de.uhilger.zeitrechnung.Ort; /** * Der traditionelle chinesichen Kalender ist unterteilt in Mond- und Sonnenjahr. * Das Sonnenjahr (sui) beginnt am Tag der Wintersonnenwende ist weiter unterteilt * in solare Abschnitte und noch weiter unterteilt in solare Haupt- und Unterabschnitte. * * Die Bahn, die die Sonne innerhalb eines tropischen Jahres von 365,24 Tagen scheinbar * auf der Ekliptik durchläuft, wird in 24 Teile von je 15 Grad unterteilt. Dies sind die 24 * Stationen oder Jahreseinteilungen (節氣 / 节气, jiéqì). Jede zweite Station ist ein * Zhongqi (中氣 / 中气, zhōngqì – „zentrale/Haupt-Jahreseinteilung“), wobei die Sonnenwenden * und Tagundnacht­gleichen vier der zwölf Zhongqi sind. Der zeitliche Abstand von einem * Zhongqi zum nächsten beträgt im Mittel ein Zwölftel eines tropischen Jahres oder 30,44 Tage. * Er variiert leicht aufgrund der elliptischen Umlaufbahn der Erde um die Sonne. * * Jeweils sechs Jahreseinteilungen gehören zu einer Jahreszeit. Während aber im westlichen * Kalender die Jahreszeiten mit dem Tag der Sonnenwende bzw. der Tagundnacht­gleiche beginnen, * liegen im chinesischen Kalender diese Tage in der Mitte der jeweiligen Jahreszeit. Man zählt * die Stationen beginnend mit dem Frühlingsanfang – lichun, und mancherorts gilt der * Frühlingsanfang (und nicht die Wintersonnenwende) als Beginn des sui-Jahres. * * Eine Besonderheit des Chinesischen Kalenders ist, dass ein Chinesisches Datum neben * Tag, Monat und Jahr auch den Zyklus und die Angabe zum Schaltmonat benoetigt. Um * die Schnittstelle Wandler vollstaendig implementieren zu koennen, werden * die Methoden setZyklus und setSchaltmonat zusaetzlich eingebaut. * * Bei Nutzung der Wandler-Schnittstelle sollte im Falle des Chinesischen Kalenders * darauf geachtet werden, dass der Methode zuTagen() entweder ein Datum der * Klasse ChinesischesDatum uebergeben wird oder zuvor setZyklus und setSchaltmonat genutzt wird. * Auch vor Nutzung der Methode zuTagen(jahr, monat, tag) muss zuvor setZyklus und * setSchaltmonat genutzt oder stattdessen die Methode * zuTagen(zyklus, jahr, monat, schaltmonat, tag) verwendet werden. * * @author Ulrich Hilger */ public class ChinesischerKalender extends BasisKalender implements Wandler { /** * Der Start der chinesischen Zeitrechung laesst sich aus dem heutigen * chinesischen Datum errechnen. Das erste Jahr des ersten Zyklus faellt * so auf den 15. Februar -2636 im gregorianischen Kalender. */ public static final long STARTTAG = new ISOKalender().zuTagen(-2636, Definition.FEBRUAR, 15); private long zyklus; private boolean schaltmonat; /** * Den Zyklus angeben, in dem ein Jahr des chinesischen Kalenders liegt, das * mit der Methode zuTagen(jahr, monat, tag) in ein generisches Datum * umgerechnet werden soll. * * Diese Methode ist eine Hilfsfunktion, wenn nicht die Methode * zuTagen(zyklus, jahr, monat, schaltmonat, tag) genutzt werden kann. * * @param zyklus der Zyklus im chinesischen Ḱalender */ public void setZyklus(long zyklus) { this.zyklus = zyklus; } /** * Angeben, ob ein Monat eines Datums im chinesischen Kalender ein * Schaltmonat ist, das mit der Methode * zuTagen(jahr, monat, tag) in ein generisches Datum * umgerechnet werden soll. * * Diese Methode ist eine Hilfsfunktion, wenn nicht die Methode * zuTagen(zyklus, jahr, monat, schaltmonat, tag) genutzt werden kann. * * @param schaltmonat */ public void setSchaltmonat(boolean schaltmonat) { this.schaltmonat = schaltmonat; } /** * Die Anzahl der Tage ermitteln, die zwischen einem gegebenen Datum * des chinesischen Kalenders und dem Tag liegen, der im * Gregorianischen Kalender mit dem Datum 1. Januar 1 bezeichnet ist. * * @param zyklus der Zyklus des chinesischen Kalenders * @param jahr das Jahr des chinesischen Kalenders * @param monat der Monat des chinesischen Kalenders * @param schaltmonat true, wenn der Monat ein Schaltmonat ist, sonst false * @param tag Tag im Monat des chinesischen Kalenders * * @return Anzahl Tage, die zwischen dem gegebenen Datum * des chinesischen Kalenders und dem Tag liegen, der im * Gregorianischen Kalender mit dem Datum 1. Januar 1 bezeichnet ist. * Liegt das gegebene Datum vor dem 1. Januar 1 (gregorianisch), wird * eine negative Zahl zurueckgegeben. */ public long zuTagen(long zyklus, int jahr, int monat, boolean schaltmonat, int tag) { this.zyklus = zyklus; this.schaltmonat = schaltmonat; return zuTagen(jahr, monat, tag); } /** * Die Anzahl der Tage ermitteln, die zwischen einem gegebenen Datum * des chinesischen Kalenders und dem Tag liegen, der im * Gregorianischen Kalender mit dem Datum 1. Januar 1 bezeichnet ist. * * Bei Verwendung dieser Methode muessen zuvor die Methode setZyklus * und setSchaltmonat genutzt werden. * * @param jahr das Jahr im betreffenden Kalendersystem * @param monat der Monat im betreffenden Kalendersystem * @param tag der Tag im betreffenden Kalendersysem * * @return Anzahl Tage, die zwischen dem gegebenen Datum * des chinesischen Kalenders und dem Tag liegen, der im * Gregorianischen Kalender mit dem Datum 1. Januar 1 bezeichnet ist. * Liegt das gegebene Datum vor dem 1. Januar 1 (gregorianisch), wird * eine negative Zahl zurueckgegeben. */ @Override public long zuTagen(long jahr, int monat, int tag) { long midYear = (long)Math.floor(STARTTAG + ((zyklus - 1) * 60 + (jahr - 1) + .5) * MITTLERES_TROPISCHES_JAHR); long theNewYear = neujahrAmOderVor(midYear); long p = chinesischerNeumondAmOderNach(theNewYear + 29 * (monat - 1)); ChinesischesDatum d = vonTagen(p); long priorNewMoon = monat == d.getMonat() && schaltmonat == d.isSchaltmonat() ? p : chinesischerNeumondAmOderNach(p + 1); return priorNewMoon + tag - 1; } /** * Die Anzahl der Tage ermitteln, die zwischen einem gegebenen Datum * des chinesischen Kalenders und dem Tag liegen, der im * Gregorianischen Kalender mit dem Datum 1. Januar 1 bezeichnet ist. * * @param d das Datum im betreffenden Kalendersystem * * @return Anzahl Tage, die zwischen dem gegebenen Datum * des chinesischen Kalenders und dem Tag liegen, der im * Gregorianischen Kalender mit dem Datum 1. Januar 1 bezeichnet ist. * Liegt das gegebene Datum vor dem 1. Januar 1 (gregorianisch), wird * eine negative Zahl zurueckgegeben. */ @Override public long zuTagen(Datum d) { if(d instanceof ChinesischesDatum) { ChinesischesDatum cd = (ChinesischesDatum) d; return zuTagen(cd.getZyklus(), (int) cd.getJahr(), cd.getMonat(), cd.isSchaltmonat(), cd.getTag()); } else { return zuTagen(zyklus, (int) d.getJahr(), d.getMonat(), schaltmonat, d.getTag()); } } /** * Das Datum des chinesischen Kalenders fuer ein generisches Datum * ermitteln. * * @param tage Anzahl der Tage zwischen 1. Januar 1 im gregorianischen * Kalender und dem Tag, dessen Datum des chinesischen Kalenders * ermittelt werden soll * @return das Datum im chinesischen Kalender */ @Override public ChinesischesDatum vonTagen(long tage) { long s1 = winterSonnenwendeAmOderVor(tage); long s2 = winterSonnenwendeAmOderVor(s1 + 370); long m12 = chinesischerNeumondAmOderNach(s1 + 1); long nextM11 = chinesischerNeumondVor(s2 + 1); long m = chinesischerNeumondVor(tage + 1); boolean leapYear = Math.round((nextM11 - m12) / MITTLERER_SYNODISCHER_MONAT) == 12; int month = (int)moduloAngepasst( (long)Math.round((m - m12) / MITTLERER_SYNODISCHER_MONAT) - (leapYear && hatSchaltmonatVorher(m12, m) ? 1 : 0), 12); boolean leapMonth = leapYear && keinSolarerHauptabschnitt(m) && !hatSchaltmonatVorher(m12, chinesischerNeumondVor(m)); long elapsedYears = (long)Math.floor(1.5 - (month / 12d) + (tage - STARTTAG) / MITTLERES_TROPISCHES_JAHR); long cycle = ganzzahlQuotient(elapsedYears - 1, 60) + 1; int year = (int)moduloAngepasst(elapsedYears, 60); int day = (int)(tage - m + 1); return new ChinesischesDatum(cycle, year, month, leapMonth, day); } public long neujahr(long isoJahr) { ISOKalender g = new ISOKalender(); return neujahrAmOderVor(g.zuTagen(isoJahr, Definition.JULI, 1)); } public long qingMing(long isoJahr) { ISOKalender g = new ISOKalender(); return (long)Math.floor(solarerNebenabschnittAmOderNach(g.zuTagen(isoJahr, Definition.MAERZ, 30))); } /** * Das generische Datum des Tages mit einem gegebenen festen Datum * im chinesischen Kalender fuer ein gegebenes gregorianisches Jahr * ermitteln * * @param isoJahr Jahr im gregorischen Kalender, in das ein gesuchtes Ereignis faellt * @param cMonat Monat im chinesischen Kalender, an dem das Ereignis stattfindet * @param cTag Tag im Monat des chinesischen Kalenders, an dem das Ereignis stattfindet * @return generisches Datum des Ereignisses */ public long cDatumZuTagen(long isoJahr, int cMonat, int cTag) { ISOKalender g = new ISOKalender(); long elapsedYears = isoJahr - g.jahrVonTagen(STARTTAG) + 1; long cycle = ganzzahlQuotient(elapsedYears - 1, 60) + 1; int year = (int)moduloAngepasst(elapsedYears, 60); return zuTagen(cycle, year, cMonat, false, cTag); } /* ---- Hilfsfunktionen --------- */ public boolean hatSchaltmonatVorher(long mHaupt, long m) { return m >= mHaupt && (keinSolarerHauptabschnitt(m) || hatSchaltmonatVorher(mHaupt, chinesischerNeumondVor(m))); } public double mitternachtInChina(long tage) { return universalVonStandard(tage, chinesischerOrt(tage)); } public final Ort peking(double t) { ISOKalender g = new ISOKalender(); long year = g.jahrVonTagen((long)Math.floor(t)); return new Ort("Peking", (double) (39.55), winkel(116,25,0), (double) (43.5), year < 1929 ? 1397d/180 : 8); } public final Ort chinesischerOrt(double t) { return peking(t); } public long neujahrAmOderVor(long tage) { long neujahr = neujahrInSui(tage); return tage >= neujahr ? neujahr : neujahrInSui(tage - 180); } public long neujahrInSui(long tage) { long s1 = winterSonnenwendeAmOderVor(tage); long s2 = winterSonnenwendeAmOderVor(s1 + 370); long m12 = chinesischerNeumondAmOderNach(s1 + 1); long m13 = chinesischerNeumondAmOderNach(m12 + 1); long naechsterM11 = chinesischerNeumondVor(s2 + 1); if((Math.round((naechsterM11 - m12) / MITTLERER_SYNODISCHER_MONAT) == 12) && (keinSolarerHauptabschnitt(m12) || keinSolarerHauptabschnitt(m13))) { return chinesischerNeumondAmOderNach(m13 + 1); } else { return m13; } } public long winterSonnenwendeAmOderVor(long tage) { double approx = geschaetzteSolareLaengeVor(mitternachtInChina(tage + 1), Definition.WINTER); long i; for(i = (long)(Math.floor(approx) - 1); !(Definition.WINTER <= solareLaenge(mitternachtInChina(i + 1))); ++i); return i; } public boolean keinSolarerHauptabschnitt(long tage) { return aktuellerSolarerHauptabschnitt(tage) == aktuellerSolarerHauptabschnitt(chinesischerNeumondAmOderNach(tage + 1)); } public int aktuellerSolarerHauptabschnitt(long tage) { double s = solareLaenge(standardVonUniversal(tage, chinesischerOrt(tage))); return (int)moduloAngepasst(2 + ganzzahlQuotient(s, (double) (30)), 12); } public double solarerHauptabschnittAmOderNach(long tage) { double l = modulo(30 * Math.ceil(solareLaenge(mitternachtInChina(tage)) / 30), 360); return chinesischeSolareLaengeAmOderNach(tage, l); } public double solarerNebenabschnittAmOderNach(long tage) { double l = modulo(30 * Math.ceil((solareLaenge(mitternachtInChina(tage)) - (double) (15)) / 30) + (double) (15), 360); return chinesischeSolareLaengeAmOderNach(tage, l); } public double chinesischeSolareLaengeAmOderNach(long tage, double theta) { Ort peking = chinesischerOrt(tage); double t = solareLaengeNach(standardVonUniversal(tage, peking), theta); return standardVonUniversal(t, peking); } public int aktuellerSolarerUnterabschnitt(long tage) { double s = solareLaenge(mitternachtInChina(tage)); return (int)moduloAngepasst(3 + ganzzahlQuotient(s - (double) (15), (double) (30)), 12); } public long chinesischerNeumondAmOderNach(long tage) { double t = neumondNach(mitternachtInChina(tage)); return (long)Math.floor(standardVonUniversal(t, chinesischerOrt(t))); } public long chinesischerNeumondVor(long tage) { double t = neumondVor((long) mitternachtInChina(tage)); return (long)Math.floor(standardVonUniversal(t, chinesischerOrt(t))); } }