PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Die lieben Zeitzonen

with 12 comments

Gastartikel von Arno Hollosi.

Kurz zu meiner Person: Ich arbeite seit über 13 Jahren mit PHP, betreibe ein recht erfolgreiches Wiki (Sensei’s Library) und habe in den letzten Jahren zwei Bücher veröffentlicht: PHP programmieren unter Windows (auch ins Englische übersetzt) und  das vor kurzem hier verloste Buch Leistungsstarke PHP-Anwendungen.

Wenn Ihr genaue Uhrzeiten in eurer Applikation verwalten müsst, merkt ihr bald, dass das Thema komplexer als angenommen ist: Nicht nur, dass die Erde eine Kugel ist, nein, zudem hat sich Anfang des vergangenen Jahrhunderts die Idee der Sommerzeit durchgesetzt und auch Regierungen ändern gelegentlich die Zeit ihres Landes.

Sommerzeit

Egal was ihr von der Sommerzeit haltet, in den PHP-Anwendungen kann man sich als Programmierer vor der Sommerzeit nicht drücken. Leider ist das genaue Datum der Umstellung von Land zu Land und teilweise sogar von Jahr zu Jahr unterschiedlich. Zudem gibt es Zonen, in denen der Unterschied zwischen Sommer- und Winterzeit nicht eine Stunde beträgt, auf der Lord-Howe-Insel in Australien wird z.B. die Uhr nur um eine halbe Stunde verstellt. In PHP erhält man die aktuellen Angaben mit DateTimeZone::getTransitions():

 $tz = new DateTimeZone('Europe/Berlin');
$transitions = $tz->getTransitions(time());
print_r($transitions[0]); // Angaben zur aktuellen Zeit
print_r($transitions[1]); // Nächster Wechsel
print_r($transitions[2]); // Übernächster Wechsel etc.

Array ( [ts] => 1346686784 [time] => 2012-09-03T15:39:44+0000
        [offset] => 7200 [isdst] => 1 [abbr] => CEST )

Array ( [ts] => 1351386000 [time] => 2012-10-28T01:00:00+0000
        [offset] => 3600 [isdst] => [abbr] => CET )

Array ( [ts] => 1364691600 [time] => 2013-03-31T01:00:00+0000
        [offset] => 7200 [isdst] => 1 [abbr] => CEST )

Wie spät war es doch gleich?

Die Grenzen der Zeitzonen und auch die Zugehörigkeit von Staaten zu Zeitzonen verändern sich immer wieder. Portugal beispielsweise wechselte 1966 von der Westeuropäischen zur Mitteleuropäischen Zeit, 1976 retour zur Westeuropäischen Zeit, 1992 zur Mitteleuropäischen Zeit und ist seit 1996 wieder in der Westeuropäischen Zeitzone. Alleine in Europa fanden seit den 1960er Jahren knapp 100 solcher Zeitzonenwechsel statt. Auch ob die Sommerzeit angewandt wird und wann sie startet und endet ändert sich gelegentlich. Kuba hat beispielsweise heuer den Start kurzfristig auf den 1. April verschoben ;o)

Olson-Datenbank

Die Zeitzonendatenbank (auch nach ihrem Gründer Olson-Datenbank genannt) enthält alle aktuellen und historischen Angaben zu den Zeitzonen. Von dieser Datenbank stammen auch die Kennungen Europe/Berlin oder America/Mexico_City. Die Datenbank enthält ca. 400 verschiedene Zeitzonen, da in der Definition der Datenbank eine Zeitzone eine Region ist, in der die Uhrzeiten seit 1970 übereinstimmen. Die Datenbank führt historische Daten teilweise bis zurück ins 19. Jahrhundert.

Eric Muller: The TZ timezones of the world

Eric Muller: The TZ timezones of the world (http://efele.net/maps/tz/world/)

Die Zeitzonendatenbank ist Bestandteil von PHP, muss also nicht extra installiert werden. Das hat aber auch einen Nachteil: die in PHP integrierte Datenbank wird nur aktualisiert, wenn ihr eine neue PHP-Version installiert. Da die Datenbank aber mehrmals pro Jahr aktualisiert wird, seid ihr möglicherweise nicht immer am aktuellsten Stand. Mithilfe von PECL kann man sich aber die aktuelle Version als Erweiterung installieren, die automatisch von den PHP-Datums- und -Zeitfunktionen verwendet wird. Mit pecl install timezonedb lässt sich die Erweiterung einfach installieren. Um auf dem aktuellen Stand zu bleiben, muss nur in Folge regelmäßig die PECL-Erweiterung aktualisiert werden: pecl upgrade timezonedb.

Zeitstempel

UTC-Zeitstempel, meist als Unix-Zeitstempel angegeben, eignen sich gut für das Festhalten von absoluten Zeitpunkten:

  • Als (Server-)Uhrzeit für interne Protokollierung von Ereignissen
  • Berechnung der Dauer seit einem Ereignis (z.B. Alter einer Nachricht, Zeit seit letztem Login)
  • Berechnung von Zeitpunkten ausgehend von einem gegebenen UTC-Zeitpunkt und absoluten Intervallen (z.B. in 60 Minuten, vor 24 Stunden)

Für andere Aufgaben sind UTC-Zeitstempel wegen der Zeitzonen, Sommerzeit und politischen Änderungen nicht geeignet. Wenn eure Benutzer Termine in ihrer Ortszeit angeben können, solltet ihr von der UTC-Zeit im Regelfall die Finger lassen.

Zudem werden Unix-Zeitstempel als vorzeichenbehaftete 32-Bit-Zahl der Sekunden seit 1.1.1970 gespeichert, weshalb der mögliche Zeitraum nur von 1901-12-13 bis 2038-01-19 reicht. Wenn das Betriebssystem 64-Bit-Zeitstempel unterstützt, tut dies der PHP-Zeitstempel aber ebenfalls. Damit umfasst der Zeitraum ±292 Billionen Jahre, was für jede Anwendung ausreichend sein sollte 😉

DateTime

PHP bietet mit DateTime und den zugehörigen Klassen DateTimeZone, DateInterval und DatePeriod ausgereifte Werkzeuge zur Speicherung und Verarbeitung von Ortszeiten an. Intern arbeiten die Klassen mit 64 Bit, der mögliche Zeitraum ist also praktisch unbeschränkt.

Einen Zeitpunkt inklusive Zeitzone erzeugt man wie hier zu sehen, mit format() kann man die gewünschte Ausgabe erzeugen:

$tz = new DateTimeZone('Europe/Berlin');
$termin = new DateTime('2012-10-01 08:15:00', $tz);
printf("%s (%+.1f)", $termin->format('Y-m-d H:i'), $termin->getOffset()/3600);

2012-10-01 08:15 (+2.0) // Europe/Berlin

Um den Termin in eine andere Zeitzone umzurechnen, kann man mit setTimezone() die Zone ändern:

$tz2 = new DateTimeZone('Australia/Lord_Howe');
$termin->setTimezone($tz2);
printf("%s (%+.1f)", $termin->format('Y-m-d H:i'), $termin->getOffset()/3600);

2012-10-01 16:45 (+10.5)  // umgerechnet in Australia/Lord_Howe

Will man den Termin beispielsweise um zwei Wochen verschieben, verwendet man DateInterval. Das Intervall wird im ISO8601-Format angegeben:

$zweiWochen = new DateInterval('P2W');
$termin->add($zweiWochen);
printf("%s (%+.1f)", $termin->format('Y-m-d H:i'), $termin->getOffset()/3600);
$termin->setTimezone($tz); // Wieder Europe/Berlin
printf("%s (%+.1f)", $termin->format('Y-m-d H:i'), $termin->getOffset()/3600);

2012-10-15 16:45 (+11.0) // + 2 Wochen (in Australia/Lord_Howe)

2012-10-15 07:45 (+2.0) // zurück in Europe/Berlin : 30 Minuten Unterschied!

Wie an der Ausgabe zu sehen, sind Berechnungen abhängig von der Zeitzone. Die Uhrzeit hat sich gegenüber der Ausgangszeit in Europe/Berlin verändert, da die Sommerzeitgrenze genau in die Periode der zwei Wochen fiel. Bei zeitzonenübergreifende Terminserien kann sich demnach die lokale Uhrzeit ändern! In meinen Anwendungen ist eine Terminserie daher immer an die Ortszeit des Terminorganisators gebunden. Die Zeiten für Teilnehmer in anderen Zeitzonen ändern sich daher manchmal. Alternativ könnten die Termine auch an die UTC gebunden werden. Benutzer kommen damit – meiner Erfahrung nach – aber damit nicht so klar, da sich die Uhrzeit auch dann ändern kann, selbst wenn alle Teilnehmer in derselben Zeitzone sind.

Und MySQL?

MySQL bietet auch einige Zeitfunktionen, meine Empfehlung aber: berechnet alles in PHP. MySQL bietet zwar auch Optionen zur Umrechnung und Berechnung, ihr erspart euch aber, die kleinen Unterschiede zwischen PHP & MySQL zu kennen und damit eine weitere Fehlerquelle. MySQL rechnet intern übrigens nur mit UTC-Zeitstempeln.

Für die Ortszeit muss in der Datenbank neben dem üblichen TIMESTAMP- oder DATETIME-Feld auch eine eigene Spalte für die Kennung der Zeitzone angelegt werden. Nur den Offset zu speichern ist zu wenig, da sich der verändern kann (Sommerzeit!) bzw. unterschiedliche Zeitzonen teilweise denselben Offset haben. Nur mit der Kennung der Zeitzone hat man alle notwendigen Informationen für die Ortszeit. Für SQL-Abfragen arbeite ich in der Datenbank meist noch mit einem zusätzlichen UTC-Feld. Die SQL-Abfrage formuliere ich unscharf (Grenzen um 2-3 Stunden erweitert), die Detailauswertung erfolgt dann in PHP, zugeschnitten auf die Zeitzone des Benutzers.

Fazit

Wäre die Erde eine Scheibe, würde vieles einfacher sein. Die Sommerzeit hätte man sich dann wahrscheinlich auch gespart. So bleibt einem nur, sich mit den vielen Veränderungen und Aufteilungen der Zeit anzufreunden und die für den Anwendungsfall sinnvollste Zeitart zu wählen. Eine einfache Daumenregel: Log-Daten und absolute Intervalle in UTC, alle benutzerbezogen Zeiten in deren Ortszeit speichern. Damit kommt man im Regelfall durch. Das frei verfügbare PDF-Beispielkapitel „Zeit und Zeitzonen“ meines neuen Buches Von Geodaten bis NoSQL: Leistungsstarke PHP-Anwendungen handelt von Zeitzonen. Da könnt ihr noch ein wenig mehr nachlesen.

Written by Arno Hollosi

September 14th, 2012 at 10:40 am

12 Responses to 'Die lieben Zeitzonen'

Subscribe to comments with RSS or TrackBack to 'Die lieben Zeitzonen'.

  1. Sehr interessant, vielen Dank für den Artikel :)

    Sven

    14 Sep 12 at 13:01

  2. Die Erde als Scheibe würde diese Probleme sicherlich nicht lösen 😉

    In der Tat ist Zeitrechnung eine schwierige Sache, vor allem wenn man mit Massendaten & z.B. Charts arbeitet und die Herkunft der Daten die dortige Zeitzone schon beinhaltet. In dem Bereich habe ich bereits leidvolle Erfahrung sammeln müssen :)
    Wenn dazu noch verschiedene Kalender ins Spiel kommen geht die Party erst richtig los

    Chris

    14 Sep 12 at 17:24

  3. Auch sehr interessant in diesem Zusammenhang sind die Schaltsekunden.

    David

    15 Sep 12 at 11:48

  4. „Eine einfache Daumenregel: Log-Daten und absolute Intervalle in UTC, alle benutzerbezogen Zeiten in deren Ortszeit speichern.“

    Die Ortszeit eines Benutzers kann sich aber auch ändern. Es gibt ja einige Zeitgenossen, die heute in Sprockhövel, morgen in Kiew und ein Paar Tage später in Sydney im Einsatz sind. Wäre es nicht sinnvoll, die benutzerbezogenen Zeiten ebenfalls in UTC zu speichern und in die jeweils aktuelle Ortszeit umzurechnen?

    Nikolaj

    17 Sep 12 at 08:54

  5. @Nikolaj: Guter Einwand, ich würde dennoch die Ortszeit speichern. Z.B. ist ein wiederkehrender Termin in UTC zwar immer um 10:00 vormittags, dank Sommerzeit aber dann einmal um 9:00 Ortszeit, dann wieder um 10:00 Ortszeit. Das verstehen Benutzer viel weniger, als wenn sie in eine neue Zeitzone umziehen und dann manche Termine anzupassen sind.

    Auch im Fall der Globetrotter: die haben ihre Termine meist auch um 10:00 in Sydney-Ortszeit und tags darauf um 15:00 in Sprockhövel-Ortszeit, da sie ja die Termine mit Leuten vor Ort haben – und die denken nicht in UTC.

    Zudem biete ich den Benutzern in den Einstellungen an, welche Zeitzone sie verwenden wollen – da kann man ja für die paar Leute, die das betrifft, auch UTC als Option dazupacken. Oder, wenn dein Benutzerkreis nur aus solchen Leuten besteht, kannst du ihnen auch bei jeder Termineingabe anbieten, die Zeitzone zu wählen.

    Arno Hollosi

    17 Sep 12 at 09:27

  6. Wie wäre es, wenn wir den Vorschlag von Nikolaj noch etwas ausbauen:

    Ein Datum welches gespeichert wird, wird immer als UTC gespeichert. Zur Anzeige muss man dann vom Benutzer die Zeitzone bekommen, oder die Zeitzone erraten (Anhand vom Land – ip2country).

    Der Vorteil: Es können wieder die MySQL Befehle genutzt werden (nachdem MySQL so eingestellt ist, dass es nur mit UTC arbeitet – http://dev.mysql.com/doc/refman/5.5/en/server-options.html#option_mysqld_default-time-zone)

    Die Speicherung der Daten ist somit immer in UTC und die Ausgabe in der vom Benutzer angegebenen Zeitzone.

    Wäre für mich theoretisch auch schöner – MVC 😉 Nur die View kümmert sich um die Anzeige (incl. Zeitzone) vom Datum.

    Simon Schick

    17 Sep 12 at 17:53

  7. @Simon

    Ahh eine theoretische Diskussion :)

    In neueren Anwendungen läuft häufig viel nebenläufig (klassisch Cron-Job, neu Message-Queue). Auch in diesem Umfeld will man meistens die Aktionen nicht nach Server-Zeit (UTC) sondern nach Anwenderzeit anstoßen. Daher lege ich die Berechnung des lokalisierten Datums auch in das Domänenmodell.

    IMHO ist der View auch nur für das Formatieren und nicht für das berechnen von Daten zuständig.

    Jan Pietrzyk

    23 Sep 12 at 17:01

  8. Also ganz ehrlich der einzige sinnig Ansatz ist eh die Zeit in UTC/GMT zu speichern. Alles andere taugt nichts. Die Ortszeit kann danach leicht mittels der Datenbank über die Zeitzone des Benutzers bestimmt werden, und auch ob er nun Sommer- oder Winterzeit hat. Also intern immer mit UTC arbeiten, da ist der MySQL-Ansatz auch richtig und nur für die Benutzerausgabe die Zeitzone einbeziehen.

    Die Zeit doppelt zu speichern, also als UTC und Ortzeit macht relativ wenig Sinn, es sei denn man möchte den Ort speichern, wo diese Zeit eingegeben wurde.

    Zum Thema Benutzersicht, dem ist es scheißegal wie intern das System mit den Zeiten umgeht, so lange die Anzeige für ihn angepasst wird im System.

    Falls nun der Einwand vom Kalender kommt, würde ich wiegesagt die Zeitzone zusätzlich speichern, nicht nochmal eine Zeit. Damit kann dann auch über eine Option die Ortzeit angezeigt werden, bzw. sogar jede beliege weitere hinzugefügt werden.

    Sascha Ahlers

    11 Okt 12 at 08:54

  9. @Sascha: nochmal die Frage, die ich bereits weiter oben gestellt habe: Wie machst du einen Serientermin für eine lokale Zeitzone in UTC? Ich denke, da kommst du um die lokale Zeit nicht herum.

    Parallel dazu hast du bei Änderungen der Sommerzeit (z.B. Beginn-/Enddatum) das Problem, dass du dann die Datenbank händisch nachziehen musst oder – falls du das Update verpasst – Benutzer um +/-1h den falschen Termin haben. Zeitzonen und Sommerzeiten ändern sich – und das nicht selten. Deshalb ist aus meiner Sicht das einzig sinnvolle, die Zeit in der Lokalzeit des Benutzers zu speichern.

    Arno Hollosi

    22 Okt 12 at 13:21

  10. @Arno: Nehmen wir an du hast einen Blog .. würdest du dann bei jedem Kommentar immer die Lokalzeit des Benutzers anzeigen oder die Zeit, die es in deiner Zeitzone ist? – und wie würdest du das regeln?

    Die 2. Frage für mich ist – wie bekommst du hier die lokale Zeit des Benutzers raus? Btw: Ich bevorzuge Programmierung ohne Javascript – sonst kann ich mir die Zeitzone auch holen und als UTC speichern 😉

    Oder mal ne ganz andere Frage: Gibt es eine Datenbank (vllt. auch eine NoSQL Variante), die die Möglichkeit bietet Zeiten mit lokaler Zeitzone zu speichern, wodurch ich dann alle Zeiten entweder in einer gewünschten Zeitzone oder als Lokalzeit bekommen kann?
    Dann wäre ich auch überzeugt von dem Argument – Zeiten als Lokalzeit zu speichern.

    Simon Schick

    22 Okt 12 at 13:47

  11. @Simon: bei einem Blog würde ich intern nur die UTC nehmen, da muss ich keine Termine planen oder zeitkritische Erinnerungen einrichten :o) Kommentare stellen absolute Zeitpunkte dar, weshalb die UTC dafür passend ist. Anzeige je nachdem:

    * entweder in Lokalzeit des Servers, wenn z.B. die meisten Benutzer aus der gleichen Zeitzone stammen (wie auf diesem Blog),

    * als UTC, wenn ich internationales Publikum habe und nicht lange nachdenken will,

    * oder in der DB als UTC gespeichert und angezeigt in Lokalzeit des Benutzers (basierend entweder auf IP-Adresse oder über Profildaten bei angemeldeten Benutzern), wenn es für die Zielgruppe Sinn macht/wichtig ist

    Bei relativen Zeitangaben „in 14 Tagen“, „geööfnet von 9:00-18:00“ ist der Bezugsrahmen die Zeitzone, weshalb ich da dann auch in der Datenbank die lokale Zeit speichern würde.

    Zeitumrechenfunktionen bieten viele Datenbanken an, auch MySQL. Damit kannst du die Umrechnung direkt in der Datenbank durchführen. Ein DateTime-Datentyp mit Zeitzone ist auch im SQL-Standard definiert, PostgreSQL bietet ihn auch an. Allerdings macht der nichts anderes, als die angegebene Zeit automatisch in UTC umzurechnen und zu speichern. Für absolute Zeitpunkte fein, für relative Zeitpunkte eine mögliche Fehlerquelle.

    Arno Hollosi

    22 Okt 12 at 15:41

  12. Yo yo, fetten Prospect fur coole Name von Seite. Ich vergewaltige zwar kein PHP aber ich hab fetten Prospect vor Ganstas

    MrProspect

    18 Nov 14 at 09:06

Leave a Reply

You can add images to your comment by clicking here.