PHP Kalender Teil 4, Jahreszeiten

Nun kommen die etwas anspruchsvolleren Themen. Bisher gab es für alle Termine einen bestimmten Bezugspunkt, mit dem es sich leicht rechnen ließ. Bei den Jahreszeiten ist das leider nicht mehr so einfach, denn diese sind nicht von einem Datum abhängig, sondern vom Stand der Sonne. Der Frühling beispielsweise beginnt, wenn die scheinbare geozentrische Länge der Sonne 0° beträgt. Man nennt diesen Zeitpunkt auch Tag-und-Nacht-Gleiche, Äquinoktium oder noch präziser Primaräquinoktium, da es im Herbst ein weiteres Äquinoktium gibt…

Hö? Ähm…ja…sind soweit alle Klarheiten beseitigt?

Gut, dann können wir uns ja tiefer in das Thema vorwagen. Bevor hier irgendwas berechnet wird, sollte man sich erstmal Bezugsdaten holen, damit man die späteren Ergebnisse auch auf Fehlerfreiheit prüfen kann. Ich habe zu diesem Zweck 2 Seiten gefunden. Zum einen (mal wieder) den passenden Artikel im Wiki (Termine von 2006 – 2018) und zum anderen eine leicht umfangreichere Liste auf der Seite des Astronomical Applications Department of the U.S. Naval Observatory (Termine von 2000 – 2020).

Da PHP soweit mir bekannt keine Funktion beinhaltet, mit der man sich das Leben in diesem Falle leichter machen könnte, bleibt nur die Suche im Netz nach einer geeigeneten Formel. Alternativ hätte ich mir natürlich auch in den nächsten Monaten selbst das nötige Wissen anlesen können, aber Google erschien mir weniger aufwendig ^^

 
Die „einfache“ Grundformel

Bei der Suche stößt man auf das Buch Astronomical Algorithms von Jean Meeus. Darin enthalten ist eine oft zitierte Grundformel ohne Korrekturterme. Der Fehler soll bei unter 15 Minuten liegen.

Für Y = (Jahr-2000)/1000

März-Äquinoktium (Beginn des astronomischen Frühlings):
JDE = 2451623.80984 + 365242.37404*Y + 0.05169*Y^2 - 0.00411*Y^3 - 0.00057*Y^4

Juni-Solstitium (Beginn des astronomischen Sommers):
JDE = 2451716.56767 + 365241.62603*Y + 0.00325*Y^2 + 0.00888*Y^3 - 0.00030*Y^4

September-Äquinoktium (Beginn des astronomischen Herbstes)
JDE = 2451810.21715 + 365242.01767*Y - 0.11575*Y^2 + 0.00337*Y^3 + 0.00078*Y^4

Dezember-Solstitium (Beginn des astronomischen Winters):
JDE = 2451900.05952 + 365242.74049*Y - 0.06223*Y^2 - 0.00823*Y^3 + 0.00032*Y^4 

Im Forum von selfhtml hat der Benutzer EisFuX diese Grundformel in einer PHP-Klasse veröffentlicht. Leider hat die Klasse wie ich finde ein paar Schwachpunkte.

Hinzu kommt, dass der Abgleich mit den Referenzseiten einen falschen Winterbeginn 2014 offenbarte. Nicht ganz verwunderlich, denn der Winterbeginn ist um 0:03 Uhr MEZ und die Formel hat einen Fehler von 15 Minuten. Die Formel kann es somit gar nicht besser…

…also wieder auf die Suche.

 
Die komplexere Formel

Eine genauere Formel zur Berechnung der Jahreszeiten zu finden, ist schon eine etwas größere Herausforderung. Für jeden Mist bekommt man im Internet haufenweise vorgefertigte Funktionen, Klassen, Codeschnipsel oder zumindest besserwisserische Kommentare…aber hier beschleicht einen das Gefühl, dass vor einem noch niemand die Jahreszeiten selbst berechnen wollte.

Eigentlich gar nicht mal so schlecht, so erspart man sich schlichtes übernehmen und muss tatsächlich noch ein wenig Hirnschmalz aufbringen.

Die Suche führte mich letztlich auf die Seite des IMCCE (Institut de Mecanique Celeste et de Calcul des Ephemerides), welche eine deutlich komplexere Formel zur Berechnung der Jahreszeiten verwenden. Hier wird nicht nur auf den Tag, sondern auf die Minute genau gerechnet.

Freundlicherweise haben sie die Berechnung in JavaScript realisiert, so dass man sich auch den Quelltext anschauen kann.

So, bevor ich wieder lang aushole, erstmal die Fakten: die Formel des IMCCE hat nach ersten Proberechnungen nur einen Fehler von 1 Minute, eine größere Abweichung konnte ich zwischen 2000 und 2020 nicht feststellen. Dem Perfektionisten mag selbst diese eine Minute noch zu viel sein, mir reicht diese Genauigkeit jedoch vollkommen aus. Zu einem falschen Datum kann es jetzt nur noch kommen, wenn der Beginn einer Jahreszeit genau auf 23:59 oder 0:00 fällt und der Fehler in die „falsche Richtung“ geht. Sobald man das weiß, kann man händisch nachbessern, das sollte nicht allzu häufig vorkommen.

Hier nun die Formel des IMCCE als PHP-Klasse inkl. automatischer Umrechung des Ergebnisses in die Zeitzone des Servers, auf dem das Script ausgeführt wird:

/****************************************************************\
| Alle Funktionen und Formeln stammen von der Webseite des IMCCE |
| (Institut de Meécanique Celeste et de Calcul des Ephemerides)  |
|      http://www.imcce.fr/en/grandpublic/temps/saisons.php      |
\****************************************************************/
class jahreszeit {
  function datum() {
    $this->JJD = 0;
    $this->JAHR = 0;
    $this->MONAT = 0;
    $this->TAG =0;
    $this->JZ = array();
  }

  function trunc($x) {
    if($x>0.0)
      return floor($x);
    else
      return ceil($x);
  }

  function JJDATEJ(){
    $Z1 = $this->JJD+0.5;
    $Z = $this->trunc($Z1);
    $A = $Z;
    $B = $A+1524;
    $C = $this->$trunc(($B-122.1)/365.25);
    $D = $this->$trunc(365.25*$C);
    $E = $this->trunc(($B-$D)/30.6001);
    $this->TAG = $this->trunc($B-$D-$this->trunc(30.6001*$E));
    if($E<13.5)
      $this->MONAT = $this->trunc($E-1);
    else
      $this->MONAT = $this->trunc($E-13);
    if($this->MONAT >= 3)
      $this->JAHR = $this->trunc($C-4716);
    else
      $this->JAHR = $this->trunc($C-4715);
  }

  function JJDATE (){
    $Z1 = $this->JJD+0.5;
    $Z = $this->trunc($Z1);
    if($Z<2299161) 
      $A = $Z;
    else {
      $ALPHA = $this->trunc(($Z-1867216.25)/36524.25);
      $A = $Z+1+$ALPHA-$this->trunc($ALPHA/4);}
    $B = $A+1524;
    $C = $this->trunc(($B-122.1)/365.25);
    $D = $this->trunc(365.25*$C);
    $E = $this->trunc(($B-$D)/30.6001);
    $this->TAG = $this->trunc($B-$D-$this->trunc(30.6001*$E));
    if($E<13.5)
      $this->MONAT = $this->trunc($E-1);
    else
      $this->MONAT = $this->trunc($E-13);
    if($this->MONAT >= 3)
      $this->JAHR = $this->trunc($C-4716);
    else
      $this->JAHR = $this->trunc($C-4715);
  }

  function affsai($n) {
    $nomsai = Array("spring","summer", "autumn", "winter");
    $FDJ = ($this->JJD+0.5E0)-floor($this->JJD + 0.5E0);
    $HH = floor($FDJ*24);
    $FDJ-=$HH/24.0;
    $MM = floor($FDJ*1440);
    $temp = date_default_timezone_get();
    date_default_timezone_set("UTC");
    $this->JZ[$nomsai[$n]] = mktime($HH,$MM,0,$this->MONAT,$this->TAG,$this->JAHR);
    date_default_timezone_set($temp);
  }

  function saison($YY) {  
    $CODE1 = $YY;
    $nline = 1;
    $k = $YY-2000-1;
    for($n=0;$n<8;$n++) {
      $nn = $n%4;
      $dk = $k+0.25E0*$n;
      $T = 0.21451814e0+0.99997862442e0*$dk
          +0.00642125e0*sin( 1.580244e0+0.0001621008e0*$dk)                              
          +0.00310650e0*sin( 4.143931e0+6.2829005032e0*$dk)                              
          +0.00190024e0*sin( 5.604775e0+6.2829478479e0*$dk)                              
          +0.00178801e0*sin( 3.987335e0+6.2828291282e0*$dk)                              
          +0.00004981e0*sin( 1.507976e0+6.2831099520e0*$dk)                              
          +0.00006264e0*sin( 5.723365e0+6.2830626030e0*$dk)                              
          +0.00006262e0*sin( 5.702396e0+6.2827383999e0*$dk)                              
          +0.00003833e0*sin( 7.166906e0+6.2827857489e0*$dk)                              
          +0.00003616e0*sin( 5.581750e0+6.2829912245e0*$dk)                              
          +0.00003597e0*sin( 5.591081e0+6.2826670315e0*$dk)                              
          +0.00003744e0*sin( 4.3918e0  +12.56578830e0 *$dk)                                
          +0.00001827e0*sin( 8.3129e0  +12.56582984e0 *$dk)                                
          +0.00003482e0*sin( 8.1219e0  +12.56572963e0 *$dk)                                
          -0.00001327e0*sin(-2.1076e0  +0.33756278e0  *$dk)                                
          -0.00000557e0*sin( 5.549e0   +5.7532620e0   *$dk)                                 
          +0.00000537e0*sin( 1.255e0   +0.0033930e0   *$dk)                                 
          +0.00000486e0*sin(19.268e0   +77.7121103e0  *$dk)                                 
          -0.00000426e0*sin( 7.675e0   +7.8602511e0   *$dk)                                 
          -0.00000385e0*sin( 2.911e0   +0.0005412e0   *$dk)                                 
          -0.00000372e0*sin( 2.266e0   +3.9301258e0   *$dk)                                 
          -0.00000210e0*sin( 4.785e0   +11.5065238e0  *$dk)                                 
          +0.00000190e0*sin( 6.158e0   +1.5774000e0   *$dk)                                 
          +0.00000204e0*sin( 0.582e0   +0.5296557e0   *$dk)                                 
          -0.00000157e0*sin( 1.782e0   +5.8848012e0   *$dk)                                 
          +0.00000137e0*sin(-4.265e0   +0.3980615e0   *$dk)                                 
          -0.00000124e0*sin( 3.871e0   +5.2236573e0   *$dk)                                 
          +0.00000119e0*sin( 2.145e0   +5.5075293e0   *$dk)                                 
          +0.00000144e0*sin( 0.476e0   +0.0261074e0   *$dk)                                 
          +0.00000038e0*sin( 6.45e0    +18.848689e0   *$dk)                                  
          +0.00000078e0*sin( 2.80e0    +0.775638e0    *$dk)                                  
          -0.00000051e0*sin( 3.67e0    +11.790375e0   *$dk)                                  
          +0.00000045e0*sin(-5.79e0    +0.796122e0    *$dk)                                  
          +0.00000024e0*sin( 5.61e0    +0.213214e0    *$dk)                                  
          +0.00000043e0*sin( 7.39e0    +10.976868e0   *$dk)                                  
          -0.00000038e0*sin( 3.10e0    +5.486739e0    *$dk)                                  
          -0.00000033e0*sin( 0.64e0    +2.544339e0    *$dk)                                  
          +0.00000033e0*sin(-4.78e0    +5.573024e0    *$dk)                                  
          -0.00000032e0*sin( 5.33e0    +6.069644e0    *$dk)                                  
          -0.00000021e0*sin( 2.65e0    +0.020781e0    *$dk)                                  
          -0.00000021e0*sin( 5.61e0    +2.942400e0    *$dk)                                  
          +0.00000019e0*sin(-0.93e0    +0.000799e0    *$dk)                                  
          -0.00000016e0*sin( 3.22e0    +4.694014e0    *$dk)                                  
          +0.00000016e0*sin(-3.59e0    +0.006829e0    *$dk)                                  
          -0.00000016e0*sin( 1.96e0    +2.146279e0    *$dk)                                  
          -0.00000016e0*sin( 5.92e0    +15.720504e0   *$dk)                                  
          +0.00000115e0*sin(23.671e0   +83.9950108e0  *$dk)              
          +0.00000115e0*sin(17.845e0   +71.4292098e0  *$dk);
      $JJD = 2451545+$T*365.25e0;
      $D = $CODE1/100.0;
      $TETUJ = (32.23e0*($D-18.30e0)*($D-18.30e0)-15)/86400.e0;
      $JJD-=$TETUJ;
      $JJD+=0.0003472222e0;
      $this->JJD = $JJD;
      if($JJD<2299160.5e0)
        $this->JJDATEJ();
      else
        $this->JJDATE();
      if($this->JAHR == $CODE1)
        $this->affsai($nn);
    }
  return $this->JZ;
  }
}