Christian Seiler: Datums- und Zeitberechnung mit PHP, Schaltjahre, Zeitzonen

Beitrag lesen

Hallo Dodwin,

Ich habe nun mit dem Wochentag angefangen und habe das so probiert:
$weekday = date('w',strtotime($year.'-'.$month.'-'.$day));

Das ist - wie Dir bereits gesagt wurde - nicht die effizienteste Methode. Ein

$weekday = idate ('w', mktime (0, 0, 0, $monats_nummer, $tag, $jahr));

wäre schonmal effizienter. Dann kannst Du ja - weil Du nur Datumsberechnungen machst und Dich die Zeit nicht interessiert auf die gm-Funktionen setzen, d.h.

$weekday = gmdate ('w', gmmktime (0, 0, 0, $monats_nummer, $tag, $jahr));

Das wäre die effizienteste in PHP eingebaute Methode.

Bei 100.000 Durchläufen brauchte die obengenannte Funktion im Durchschnitt 20.694 sec.

Die Methode über idate() + mktime() ist bei mir nur leicht schneller als die Methode über date() + strtotime() - allerdings ist die Methode über gmdate() + gmmktime() bei mir nur noch 2 Mal so langsam, wie Deine Funktion. Zu den Hintergründen: Siehe unten.

Die nachgebaute Funktion brauchte bei gleicher Anzahl an Durchläufen lediglich 0.514 sec im Durchschnitt.
Das bedeutet die nachgebaute Funktion ist ca 40x so schnell.

Leider ist Deine nachgebaute Funktion zum einen falsch und zum anderen nicht die effizienteste Implementierung selbst innerhalb des Wertebereichs, den Deine Funktion akzeptiert.

Warum ist die Funktion falsch? Weil sie Schaltjahre nicht korrekt berücksichtigt. Du nimmst an, dass alle 4 Jahre in Schaltjahr ist. Dies ist nicht korrekt! Dies war im julianischen Kalender so, im gregorianischen Kalender (den wir aktuell verwenden) ist die Schaltjahresregel anders, nämlich: Alle 4 Jahre sind ein Schaltjahr, alle 100 Jahre wieder nicht, alle 400 Jahre dann doch wieder. Da das Jahr 2000 gerade in die 400-Jahre-Regelung fällt, stimmt Deine Funktion da zufälligerweise wieder, im Jahr 2100 stimmt sie dagegen nicht mehr.

Zudem: Warum schränkst Du den Wertebereich der Funktion künstlich ein? Für Daten von vor 1990 liefert sie schlichtweg ein falsches Ergebnis, ohne es abzufangen und einen Fehler zurückzugeben.

Wenn Du etwas selbst programmiertes nutzen willst:

Eine Funktion zum Berechnen des Wochentags für den greorianischen und julianischen Kalender hatte ich bereits im Forum gepostet. Diese funktioniert für alle Daten ab dem 01.01.4713 v.u.Z. JC. Sie ist einen winzigen Tick ineffizienter als Deine Funktion, weil sie 4x die PHP-Funktion floor() aufruft - obwohl das nicht überall nötig wäre. Folgender Code nutzt nur noch (int) statt floor() - und gibt eine Zahl (0 für Montag etc.) statt eines Monatsnamens zurück. Ich habe die Kommentare zu den Berechnungen der Kürze wegen entfernt, die findest Du in dem obigen Archivposting:

function wochentag ($jahr, $monat, $tag, $g = true) {  
 if ($monat <= 2) {  
  $jahr--;  
  $monat += 12;  
 }  
 if ($g) {  
  $jahrhundert = floor ($jahr / 100);  
  $korrektur = 2 - $jahrhundert + floor ($jahrhundert / 4);  
 } else {  
  $korrektur = 0;  
 }  
 $jul_tag_zahl = (int) (365.25 * ($jahr + 4716));  
 $jul_tag_zahl += (int) (30.6001 * ($monat + 1));  
 $jul_tag_zahl += $tag;  
 $jul_tag_zahl += $korrektur;  
 $jul_tag_zahl -= 1524;  
 return $jul_tag_zahl % 7;  
}

Oder (einen Tick effizienter) als fast geschlossener Ausdruck:

function wochentag ($jahr, $monat, $tag, $g = true) {  
 if ($monat <= 2) {  
  $jahr--;  
  $monat += 12;  
 }  
 if ($g) {  
  $jahrhundert = floor ($jahr / 100);  
  $korrektur = 2 - $jahrhundert + floor ($jahrhundert / 4);  
 } else {  
  $korrektur = 0;  
 }  
 $jul_tag_zahl = (int) (365.25 * ($jahr + 4716))+ (int) (30.6001 * ($monat + 1)) + $tag + $korrektur - 1524;  
 return $jul_tag_zahl % 7;  
}

Oder man kann auf floor() komplett verzichten, ohne sich den Wertebereich kaputt zu machen (für negative Jahre ist (int) != floor!):

function wochentag ($jahr, $monat, $tag, $g = true) {  
 if ($monat <= 2) {  
  $jahr--;  
  $monat += 12;  
 }  
 if ($g) {  
  $jahrhundert = (int) (($jahr + 4800) / 100) - 48;  
  $korrektur = 2 - $jahrhundert + (int) (($jahrhundert + 48) / 4) - 12;  
 } else {  
  $korrektur = 0;  
 }  
 $jul_tag_zahl = (int) (365.25 * ($jahr + 4716))+ (int) (30.6001 * ($monat + 1)) + $tag + $korrektur - 1524;  
 return $jul_tag_zahl % 7;  
}

Dies dürfte auch die effizienteste korrekte Implementierung in PHP sein, ist etwa 1,3x so schnell wie Deine Lösung, unterstützt sowohl den gregorianischen als auch den julianischen Kalender und ist korrekt für alle Daten ab dem 1. Januar 4713 v.U.Z. (das war vor etwa 6720 Jahren).

Wenn Du also möglichst effizient lediglich die Wochentage berechnen willst, dann empfehle ich Dir die letzte Implementierung. Ich stelle mir allerdings auch hier wieder die Frage: Wozu? Wenn Du sowieso nur einen limitierten Zeitraum abbilden willst, dann reichen gmdate() + gmmktime() völlig aus - die sind zwar etwa 2,5x so langsam wie meine letzte Funktion, aber ich kann mir nicht vorstellen, dass das irgend etwas ausmacht. Wie oft berechnest Du denn Wochentage im Script?

Die Timestamp-Berechnung dauert ebenfalls sehr lange.

Was heißt für Dich "lange"? Ich wiederhole mich zwar, aber selbst Deine ursprünglichen Zahlen für die Wochentagsberechnung mit date() + strtotime() implizieren 0,21ms pro Berechnung - ich kann mir ehrlich nicht vorstellen, dass das der Flaschenhals in Deinem Script ist.

Nun habe ich auch diese Funktion (mktime oder wahlweise strtotime) versucht nachzubauen und siehe da sie war wieder schneller.

Leider musste ich aber enttäuscht feststellen, dass meine selbst erstellten timestamps teilweise um eine Stunde von den wirklichen Ergebnissen abwichen. Ich hatte die Sommer/Winter-Zeit vergessen.

Das macht die Sache leider kompliziert

Ja, Zeitzonen sowie Sommer- und Winterzeit sind eine ziemlich komplexe Angelegenheit. Zum Beispiel ist die Uhrzeit 02:30:00 am 30. März 2008 in unserer Zeitzone nicht erlaubt. Sie darf schlichtweg nicht vorkommen. Warum? Weil die Uhr am 30. März von 01:59:59 auf 03:00:00 umspringt. Genauso gibt es die Uhrzeit 02:30:00 am 28. Oktober 2007 gleich zwei Mal mit einer Stunde Abstand - laut Gesetz ist die erste Stunde als "A" und die zweite Stunde als "B" zu bezeichnen. Zudem kommt noch hinzu, dass es in anderen Ländern andere Vorschriften zur Umstellung gibt und diese wurden z.B. in den USA erst kürzlich geändert, d.h. vor 2006 galt die eine Regelung, danach die andere.

Es gibt im Internet unter http://www.twinsun.com/tz/tz-link.htm eine Datenbank, die Public Domain ist, die erschöpfende Informationen über Zeitzonen und Sommer-/Winterzeit enthält. Wenn Du die wirklich verwendent willst, wird es allerdings schnell SEHR kompliziert.

PHP macht das aber bereits für Dich, Du brauchst Dich nicht darum kümmern. Deswegen ist gmdate() / gmmktime() so viel schneller als date() / strtotime() - PHP muss bei GMT nicht schauen, welche Zeitzone / Sommer+Winterzeit da gilt oder nicht, GMT hat sowas per Definition nicht.

und nun habe ich mir überlegt wäre es nicht einfacher sich seine "eigenen" timestamps zu berechnen.
Die würden dann: [gekürzt]

  • Die Zeit seit 1990 (nicht seit 1970) berechnen.
  • Die Sommer-/Winterzeit ignorieren.
    Was meint ihr zu der Idee?

Naja, wenn Du Sommer-/Winterzeit ignorieren willst, nimm gmdate() und gmmktime(). Die sind schon ausprogrammiert und definitiv nicht viel langsamer (evtl. sogar schneller [1]) als alle Funktionen, die Du selbst programmieren kannst. Ein eigenes Timestamp-Format einzuführen, das einen noch eingeschränkteren Wertebereich als das ursprüngliche hat, ist dagegen ziemlicher Quatsch - mit den Werten wird nämlich keiner umzugehen wissen.

Wenn Du nur das Datum brauchst und Dir gm* immer noch zu langsam ist, dann könntest Du eine julianische Tageszahl [2] nehmen - das ist zumindest einen gängige Konvention. Wie Du eine julianische Tageszahl berechnest, siehst Du ja in der Funktion für den Wochentag (einfach das % 7 weglassen), das wieder Rückgängig zu machen ist ja nicht allzu schwierig (siehe Wikipedia).

Andererseits (ja ich weiß, ich wiederhole mich) frage ich mich, was Du eigentlich im Script anstellst, um zu glauben, dass die normalen PHP-Datumsfunktionen ein Performanceproblem darstellen. Denn für ALLES, was ich bisher gesehen habe (und ich habe eine MENGE PHP-Code gesehen), waren die Funktionen LOCKER schnell genug.

Viele Grüße,
Christian

[1] Bei der Wochentagsberechnung waren sie deswegen langsamer, weil sie dennoch die Uhrzeit mit berücksichtigten, obwohl das hier nicht nötig war.

[2] Oder, um die Wikipedia-Terminologie zu nutzen: »Chronologisches Julianisches Datum« (das "normale" »julianische Datum« nutzt Zeit als Bruchanteil der Zahl - für reine Datumsberechnungen brauchst Du das aber nicht).