Datumsberechnungen haben ihre Tücken, da Kalenderregeln historisch wachsen
und politische Entscheidungen integrieren. Perls DateTime
-Modul
kennt sie alle.
Wenn ein Backup-Skript um zehn Uhr abends startet und um vier Uhr nachts erfolgreich abschließt, wie lange ist der Prozess dann gelaufen? Sechs Stunden? Kommt darauf an.
Was ist, wenn der Prozess in der Nacht vom 25. auf den 26. März 2006 irgendwo in Deutschland läuft? Dann wird um zwei Uhr nachts die Uhr um eine Stunde vorgestellt und die korrekte Antwort lautet daher: Fünf Stunden. Liefe derselbe Prozess zur gleichen Zeit in den USA, hieße die Antwort sechs Stunden, da dort die Sommerzeitumstellung eine Woche später erfolgt. Allerdings nicht im Bundesstaat Indiana, der bis dato keine Sommerzeit kennt. Dieses Jahr (2006) soll aber erstmals umgestellt werden ([2]). Derlei Wirrwarr kann das vom CPAN erhältliche Modul DateTime ([5]) glücklicherweise elegant verarbeiten.
Seit wann gibt es in Deutschland die Sommerzeit? Das Skript dsthist
findet es heraus, indem es vom Jahr 2000 an rückwärts schreitet
und jeweils durch alle Märztage rattert, um herauszufinden,
ob irgendwann 3 Uhr herauskommt, wenn man eine Sekunde zur Uhrzeit
01:59:59 hinzuaddiert. Findet es keinen Schalttag, stoppt es, nachdem
das Jahr ausgegeben wurde. Die Ausgabe zeigt die Antwort, 1981 war
das erste Jahr:
... 1984: DST 1983: DST 1982: DST 1981: DST 1980: No DST
01 #!/usr/bin/perl -w 02 use strict; 03 use DateTime; 04 05 YEAR: 06 for my $year (reverse 1964..2006) { 07 08 for my $month (3..4) { 09 for my $day (1..31) { 10 11 next if $day > DateTime->last_day_of_month( 12 year => $year, month => $month)->day(); 13 14 my $dt = DateTime->new( 15 year => $year, 16 month => $month, 17 day => $day, 18 hour => 1, 19 minute => 59, 20 second => 59, 21 time_zone => "Europe/Berlin", 22 ); 23 24 $dt->add(seconds => 1); 25 26 if($dt->hour() == 3) { 27 print "$year: DST\n"; 28 next YEAR; 29 } 30 } 31 } 32 print "$year: No DST\n"; 33 last; 34 }
In Europa ist die Sommerzeit ja relativ einheitlich geregelt. Nicht so auf dem amerikanischen Kontinent, wo sie in den englischsprachigen Ländern ``Daylight Savings Time'' genannt wird, da man durch sie effizienter mit dem Tageslicht umgeht.
Aber nicht nur die vielen Länder des amerikanischen Kontinents haben ihre eigene Vorstellungen von Sommerzeit. Selbst einzelne Bundesstaaten der USA kochen ihr eigenes Süppchen und manche Landbezirke weichen sogar wieder von den Regelungen eines Bundesstaates ab. Und natürlich haben sich die Regelungen auch im Laufe der Geschichte verändert.
Listing dstchk
holt alle Zeitzonen, die dem Modul
DateTime::TimeZone
(ebenfalls auf dem CPAN erhältlich)
bekannt sind, mit all_names()
hervor, fährt
den ersten Januar des gegenwärtigen Jahres in der gerade untersuchten
Zeitzone an und zählt sechs Monate hinzu. Kommt ein Datum heraus,
dessen Stundenwert ungleich Null ist, deutet das darauf hin, dass in
der ersten Jahreshälfte eine Zeitverschiebung eingetreten ist und
damit die entsprechende Zeitzone irgendwann während dieser Zeitspanne
in die Sommerzeit eingetreten ist.
Die Zonen liegen normalerweise im Format
"Kontinent/Stadt"
vor, das gilt zumindest für Europe/Berlin
(ganz Deutschland), America/New_York
(der Bundesstaat New York in
den USA), America/Vancouver
(der kanadische Bundesstaat British
Columbia) und Pacific/Honolulu
(das zur USA gehörende Hawaii). Ist
aber zum Beispiel ein Landkreis in seiner gewählten Zeitzone oder
Sommerzeitregelung irgendwann einmal von der des Bundesstaates abgewichen,
wird weiter untergliedert. So bezeichnet America/Kentucky/Louisville
den US-
Bundesstaat Kentucky, dessen größte Stadt Louisville ist. (Die
Hauptstadt von Kentucky ist, wie in den USA nicht ungewöhnlich, ein
völlig unbekanntes Nest namens Frankfort.) Da allerdings ein Landkreis
in Kentucky namens Monticello bis zum Jahr 2000 einer anderen Zeitzone
angehörte, führt DateTime::TimeZone
auch einen Eintrag für America/Kentucky/Monticello
.
Abbildung 1 zeigt die Ausgabe von dstchk
, das Sommerzeitbezirke
mit Term::ANSIColor
grün anzeigt.
Abbildung 1: Welche Bezirke auf dem amerikanischen Kontinent verwenden Sommerzeit? |
01 #!/usr/bin/perl -w 02 use strict; 03 use DateTime; 04 use Term::ANSIColor qw(:constants);; 05 06 for my $zone ( 07 DateTime::TimeZone::all_names()) { 08 09 my $from = DateTime->now( 10 time_zone => $zone); 11 12 $from->truncate(to => "year"); 13 my $to = $from->clone()->add( 14 months => 6); 15 16 print "$zone: "; 17 18 if( $to->hour() == 0 ) { 19 print RED, "no", RESET, "\n"; 20 } else { 21 print GREEN, "yes", RESET, "\n"; 22 } 23 }
So kann DateTime
in allen denkbaren Zeitzonen rechnen, sogar mit
Daten der Vergangenheit, in Zeitzonen, die sich historisch entwickelt haben.
Die Australien vorgelagerte Insel Lord Howe Island kapriziert sich
sogar mit einer Sommerzeit, bei der die Uhr nur eine halbe Stunde verstellt
wird. Listing lord_howe
zeigt, dass sich eine lokale Ortszeit von
02:30:00 ergibt, falls man zu 2005-10-30 01:59:59 nur eine Sekunde
addiert.
01 #!/usr/bin/perl -w 02 use strict; 03 use DateTime; 04 05 my $dt = DateTime->new( 06 year => 2005, 07 month => 10, 08 day => 30, 09 hour => 1, 10 minute => 59, 11 second => 59, 12 time_zone => 'Australia/Lord_Howe', 13 ); 14 15 $dt->add( DateTime::Duration->new( 16 seconds => 1) ); 17 18 # 2005-10-30 02:30:00 19 print $dt->date(), " ", 20 $dt->hms(), "\n";
Ein mit dem Konstruktor new
erzeugtes DateTime
-Objekt existiert
zunächst in einer speziellen Zeitzone namens floating
, falls
der Parameter time_zone
nicht explizit eine Zeitzone angibt. In diesem
Zustand passt es sich temporär der Zeitzone anderer DateTime
-
Objekte an, wenn es mit ihnen verglichen wird. Wer bei der Zeitrechnung
Sommerzeitspielchen ausschalten will, wählt entweder floating
oder
die ebenfalls sommerzeitfreie (wirkliche) Zone UTC
(Universal Time Coordinated). Soll ein
DateTime-Objekt hingegen in derselben Zeitzone beheimatet sein
wie der Rechner, auf dem es läuft, sorgt time_zone => "local"
dafür, dass DateTime
mit allerlei ausgefuchsten Tricks die
dort eingestellte Zeitzone errät:
my $dt = DateTime->now( time_zone => "local"); print $dt->time_zone()->name();
ergab auf dem in San Francisco stationierten Rechner des Perlmeister-Testlabors doch glatt ``America/Los_Angeles''. Nicht schlecht.
Wieviele Sekunden sind in Deutschland am 1. Januar 1999 zwischen 00:59:00 und 01:00:00 Uhr vergangen? Eine Minute, also 60 Sekunden? Falsch! Tipp: Damals wurde in der Greenwich-Zeitzone UTC (eine Stunde vor der deutschen Zeitzone) eine Schaltsekunde eingelegt. Diese Minute war tatsächlich 61 Sekunden lang!
Wie man auf [3] nachlesen kann, ist eine Sekunde seit 1967 nicht mehr als Bruchteil eines Erdentages definiert, sondern über die viel konstantere Resonanz des Cäsiumatoms. Die Rotationsgeschwindigkeit der Erde hat aber während der letzten 40 Jahre langsam abgenommen. Da die Erde gegenwärtig geringfügig länger als 24 mal 3600 atomgenaue Sekunden für eine Rotation benötigt, wurden seit 1972 etwa alle 18 Monate eine Schaltsekunde in die offizielle Zeit eingefügt. Maßgebend sind der 30. Juni und der 31. Dezember. Falls sich etwa eine Sekunde angestaut hat, wird sie am Ende dieser Stichtage eingefügt. Allerdings scheint sich der Erdball neuerdings wieder konstanter zu drehen, nach 1998 war nur Ende 2005 ein Sekundenspritzerl notwendig. Drehte sich der Erdball auf einmal schneller, würden die gestrengen Zeitwächter einfach eine Schaltsekunde aus der offiziellen Zeit entfernen. Allerdings ist das in der kurzen Geschichte der Schaltsekunden noch nicht vorgekommen.
Listing leapsec
arbeitet sich von 1960 bis 2005 vor und untersucht,
ob jeweils am 30.6. oder am 31.12. eine Schaltsekunde eingelegt wurde.
In der UTC-Zeitzone stellt es die Uhrzeit 23:59:00 ein, addiert 60 Sekunden
und prüft, ob der Sekundenanteil den ungewöhnlichen Wert von 60
anzeigt. Ist dies der Fall, war die untersuchte Minute tatsächlich 61
Sekunden lang und eine Schaltsekunde wurde entlarvt.
Anschließend setzt set_time_zone()
die Zeitzone wieder auf die in
Deutschland übliche mitteleuropäische Zeit plus -- falls gegeben --
Sommerzeit. Eine
print
-Anweisung gibt die lokale Uhrzeit aus und die Anzahl der bisher
gefundenen Schaltsekunden aus.
Abbildung 2 zeigt die Ausgabe. Die unterschiedlichen lokalen Schalt-Uhrzeiten am 1.7. erklären sich aus der Sommerzeitumstellung anfang der 80er.
Abbildung 2: An welchen Tagen von 1960 bis heute wurden Schaltsekunden eingelegt? |
01 #!/usr/bin/perl -w 02 use strict; 03 use DateTime; 04 05 my $secs; 06 07 for my $year (1960..2005) { 08 for my $date ([30,6], [31,12]) { 09 my $now = DateTime->new( 10 year => $year, 11 month => $date->[1], 12 day => $date->[0], 13 hour => 23, 14 minute => 59, 15 second => 0, 16 time_zone => "UTC"); 17 18 my $later = $now->clone()->add( 19 seconds => 60); 20 21 $later->set_time_zone("Europe/Berlin"); 22 23 if($later->second() == 60) { 24 print $later->dmy(), " ", 25 $later->hms(), ": ", 26 ++$secs, "\n"; 27 } 28 } 29 }
Allerdings kümmert sich die auf Unix-Systemen übliche Zählung der Sekunden seit 1970 nicht um Schaltsekunden. Während in Deutschland am 1. Januar 1999 der Sekundenzeiger von 00:59:59 Uhr auf 00:59:60 schnellte, wurde der Zählerwert auf den Unix-Maschinen nach dem POSIX-Standard von 915148799 auf 915148800 erhöht. Während des nächsten (virtuellen) Hüpfers von 00:59:60 auf 01:00:00 änderte sich jedoch die Unix-Zeit nicht, beide Zeitpunkte werden korrekt von der Unix-Zeit 915148800 repräsentiert. Natürlich hat dafür kein Unix-Rechner die Zeit angehalten, die Differenz ergibt sich lediglich aus der Definition der Unix-Zeit im POSIX-Standard.
Wer also zwei Unix-Zeiten voneinander subtrahiert und daraus die dazwischen verstrichene UTC-Zeit berechnet, muss korrigierend eingreifen, falls dazwischen eine Schaltsekunde stattfand. Details zu diesem verwirrenden Verfahren stehen unter [6] und [7].
DateTime
bietet die
Klassenmethode from_epoch(epoch => $time)
an, um aus einem Zähler
ein DateTime
-Objekt zu konstruieren. Umgekehrt exportiert die
epoch()
-Methode eines DateTime
-Objekts wieder einen Zählerwert.
Listing leapreveal
zeigt, was passiert, wenn man zum 1.1.1990
einfach die aufmultiplizierten Sekunden für 5.000 Tage hinzuzählt:
Das Ergebnis ist 2003-09-09T23:59:53
, also fehlen 7 Sekunden bis zum
vollen Tag. Addiert man hingegen mit add(days => 5000)
einfach 5.000
Tage, ist das Ergebnis 2003-09-10T00:00:00
. DateTime
verarbeitet
Zeiteinheiten wie Tage und Sekunden strikt getrennt
und rechnet normalerweise eine Zeitdauer wie ``5000 Tage'' nicht in
Sekunden um. Wer sich dennoch dafür interessiert und mit wohligem
Schauer die dann durchsiebte Abstraktion ansehen will, kann mit der
Methode $to->subtract_datetime_absolute($from)
das DateTime-
Objekt $from
von einem DateTime
-Objekt $to
abziehen und
erhält ein DateTime::Duration
-Objekt, dessen seconds()
-Methode
die tatsächlich vergangene Anzahl von Sekunden liefert.
01 #!/usr/bin/perl -w 02 use strict; 03 use Sysadm::Install qw(:all); 04 05 use DateTime; 06 07 my $dt = DateTime->new( 08 year => 1990, 09 time_zone => 'UTC' 10 ); 11 12 $dt->add(seconds => 3600*24*5000); 13 print "$dt\n";
Eine weitere Kuriosität ist die Datumsgrenze. Fliegt man gen Osten, wird es -- nach der Ortszeit in den durchquerten Zeitzonen -- immer später am Tag. Irgendwo muss es also einen Punkt geben, an dem das Datum auf den vorhergehenden Tag umspringt, sonst wäre es leicht möglich, mit einem schnellen Flugzeug in die Zukunft zu reisen. Diese Datumsgrenze ([4]) zieht sich von Nord nach Süd durch den pazifischen Ozean, leicht östlich der Süd-Ost-Asien vorgelagerten Inselwelt.
Listing daytrip
zeigt, was passiert, wenn man mit einem schnellen Flugzeug
von Japan (westlich der Datumsgrenze) nach Hawaii (östlich davon) fliegt:
Abflug: Sonntag, den 29.01.2006 um 07:30 Ankunft: Samstag, den 28.01.2006 um 19:00
Man fliegt man am Sonntag morgen los, kommt aber, obwohl der Flug sechseinhalb Stunden dauert, einen Tag früher an. Am Samstag abend, noch vor Bekanntgabe der Lottozahlen. Schade nur, dass dies nur für die jeweilige Ortszeit gilt.
01 #!/usr/bin/perl -w 02 use strict; 03 use DateTime; 04 use DateTime::Format::Strptime; 05 06 my $format = 07 DateTime::Format::Strptime->new( 08 pattern => "%A, den %d.%m.%Y um %H:%M", 09 locale => "de_DE", 10 time_zone => 'Asia/Tokyo', 11 ); 12 13 my $dt = $format->parse_datetime(" 14 Sonntag, den 29.01.2006 um 07:30"); 15 16 $dt->set_formatter($format); 17 18 print "Abflug: $dt\n"; 19 20 $dt->add( DateTime::Duration->new( 21 hours => 6, 22 minutes => 30) ); 23 24 $dt->set_time_zone( 'Pacific/Honolulu' ); 25 print "Ankunft: $dt\n";
Listing daytrip
zeigt auch, wie DateTime
mit unterschiedlichen
Datumsformaten umgeht. Sowohl beim Parsen eines Datumsstrings mit
parse_datetime()
als
auch bei der Ausgabe nutzt es Formatierer aus der Klassenhierarchie
DateTime::Format::*
. Ein besonders flexibler Formatierer ist
DateTime::Format::Strptime
der, ähnlich wie die C-Funktion
strptime()
, einen Formatstring mit arrangierten Platzhaltern
entgegennimmt. %A
steht dabei für den ausgeschriebenen Monatsnamen,
%d
für das Datum, %H
für die Stunde, und so weiter.
Der Parameter locale
ist auf den Wert "de_DE"
gesetzt. de
wählt
Deutsch als Sprache. Der zweite Teil des Locales bestimmt das Land und
dessen spezielle Regeln.
Listing locales
zeigt einige weitere Beispiele: en_GB
und
en_US
sind die Locales für die englische Sprache in
Großbritannien und den USA. fr_FR
wählt Französisch, und es_ES
und es_MX
formatiert das Datum in Spanisch für Spanien und Mexiko.
Ist der Formatierer einmal mit dem richtigen Locale initialisiert, wird
er dem DateTime
-Objekt mit set_formatter
untergejubelt. Ab dann
werden ``stringifizierte'' DateTime
-Objekte entsprechend den im
Formatierer festgelegten Regeln in Strings umgewandelt. Abbildung 3
zeigt einige Beispiele für unterschiedliche Formatierungen ein und
desselben DateTime-Objektes.
01 #!/usr/bin/perl -w 02 use strict; 03 use DateTime; 04 use DateTime::Format::Strptime; 05 06 my $dt = DateTime->now(); 07 08 for my $locale (qw(en_GB en_US de_DE fr_FR 09 es_ES es_MX)) { 10 11 $dt->set_locale($locale); 12 13 my $format = 14 DateTime::Format::Strptime->new( 15 pattern => $dt->locale()-> 16 long_datetime_format() 17 ); 18 19 $dt->set_formatter($format); 20 print "$locale: $dt\n"; 21 }
Abbildung 3: Unterschiedliche Sprachen und Landesbräuche bei der Übersetzung und Formatierung eines Datumsstrings. |
Dass alle vier Jahre ein Schaltjahr ist, aber nicht, wenn es durch 100 teilbar ist, aber dann doch wieder, falls die Division durch 400 aufgeht, ist jedem Programmierer spätestens seit dem Jahr 2000 bekannt.
DateTime
beherrscht die Regeln natürlich, deswegen eine etwas
kompliziertere Aufgabe: Wie lang ist die Liste aller Freitage, die zwischen
1980 und 2020 auf einen 29. Februar fielen?
Elegant löst man diese Rechnung mit zwei Sets von DateTime
-Objekten: eines,
das alle Freitage enthält und ein anderes, das alle Tage enthält, die
auf einen 29. Februar fallen. Ein Objekt der Klasse DateTime::Set
hält
eine möglicherweise unendliche Menge von DateTime
-Objekten. Erzeugt
wird ein Set am einfachsten mit Hilfe des Moduls DateTime::Event::Recurrence
vom CPAN. Der Konstruktor
DateTime::Event::Recurrence-> yearly(days => 29, months => 2);
liefert ein Objekt vom Typ DateTime::Set
zurück, das alle 29.
Februare als abstrakte Beschreibung enthält. Gleichfalls definiert
Listing frifeb29
mit weekly(days => 5)
ein weiteres Set, das
alle Freitage (der 5. Wochentag) führt. Die Schnittmenge wird durch
die Methode intersection()
bestimmt und liefert ein Set mit
allen Tagen, an denen der 29. Februar auf einen Freitag fällt.
Damit der in Zeile 13 definierte Iterator des Ergebnis-Sets
weiß, wo er anfangen soll zu laufen, legt der Parameter start
ein Startdatum fest und auch das Ende wird mit einem DateTime
-Objekt
spezifiziert. Die while
-Schleife ab Zeile 18 stößt den Iterator mit
next()
an und treibt ihn weiter, bis das Ende des untersuchten Zeitraums
erreicht ist. Ergebnis: Im Jahr 1980 fiel der 29. Februar auf einen Freitag
und auch 2008 wird es wieder soweit sein.
01 #!/usr/bin/perl -w 02 use strict; 03 use DateTime; 04 use DateTime::Event::Recurrence; 05 06 my $feb29 = DateTime::Event::Recurrence-> 07 yearly(days => 29, months => 2); 08 my $fri = DateTime::Event::Recurrence-> 09 weekly(days => 5); 10 11 my $set = $fri->intersection($feb29); 12 13 my $it = $set->iterator( 14 start => DateTime->new(year => 1980), 15 end => DateTime->new(year => 2020), 16 ); 17 18 while(my $dt = $it->next()) { 19 $dt->set_locale("de_DE"); 20 print $dt->day_name(), ", den ", 21 $dt->day(), ".", 22 $dt->month(), ".", 23 $dt->year(), "\n"; 24 }
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |