Aus Linux-Magazin 02/2006

Datumsberechnungen

Datumsberechnungen haben ihre Tücken, denn Kalenderregeln sind historisch und sogar von politischen Entscheidungen beeinflusst. Perls »DateTime«-Modul kennt alle Tricks.

Wenn ein Backup-Skript um zehn Uhr abends startet und um vier Uhr nachts erfolgreich abschließt, wie lange läuft es dann? Sechs Stunden? Kommt drauf an. Denn was ist, wenn der Prozess in der Nacht vom 25. auf den 26. März 2006 irgendwo in Deutschland läuft? Da wird um zwei Uhr nachts die Uhr um eine Stunde vorgestellt, die korrekte Antwort ist dann: fünf Stunden.

Liefe der gleiche Prozess zur selben Zeit in den USA, hieße die Antwort sechs Stunden, da dort die Sommerzeit-Umstellung eine Woche später erfolgt. Allerdings nicht im Bundesstaat Indiana, der bis dato keine Sommerzeit kennt. Dieses Jahr soll aber erstmals umgestellt werden [2]. Selbst solcher Wirrwarr bringt das vom CPAN erhältliche Modul »DateTime« [5] aber glücklicherweise nicht aus dem Tritt.

Seit wann gibt es in Deutschland die Sommerzeit? Das Skript »dsthist« (Listing 1) 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 addiert. Findet es keinen Schalttag, stoppt es:

...
1984: DST
1983: DST
1982: DST
1981: DST
1980: No DST

Die Ausgabe zeigt die Antwort: 1981 war das erste Jahr.

Summer in the City

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 die Einwohner durch sie effizienter mit dem Tageslicht umgehen sollen.

Aber nicht nur die vielen Länder des großen Kontinents haben ihre individuelle Vorstellung von Sommerzeit. Selbst einzelne Bundesstaaten der USA kochen ihr eigenes Süppchen, manche Landbezirke weichen sogar wieder von den Regelungen ihres Bundesstaats ab. Zudem haben sich diese Regeln natürlich im Laufe der Geschichte gewandelt.

Listing »dstchk« (Listing 2) 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 dies darauf hin, dass in der ersten Jahreshälfte eine Zeitverschiebung eingetreten und damit die entsprechende Zeitzone irgendwann während dieser Zeitspanne in die Sommerzeit übergegangen ist.

Ortszeiten

Die Zonen liegen normalerweise im Format Kontinent/Stadt vor, so 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 Bundesstaats 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 – 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.

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 Insulaner die Uhr nur eine halbe Stunde verstellen. Das Listing »lord_howe« (Listing 3) zeigt, dass sich dort eine lokale Ortszeit von 02:30:00 ergibt, falls man zu 2005-10-30 01:59:59 nur eine Sekunde addiert.

Alles fließt

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 (reale) Zone »UTC« (Universal Time Coordinated). Soll ein »DateTime«-Objekt hingegen in derselben Zeitzone liegen 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();

Das ergab auf dem in San Francisco stationierten Rechner des Perlmeister-Testlabors doch glatt »America/Los_Angeles«. Nicht schlecht.

Wie viele 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 bekam die Greenwich-Zeitzone UTC (eine Stunde vor der deutschen Zeitzone) eine Schaltsekunde verordnet. Diese Minute war tatsächlich 61 Sekunden lang!

Listing 1:
»dsthist«

02 use strict;
03 use DateTime;
04
05 YEAR:
06 for my $year (reverse 1964..2006) {
07
08     for my $day (1..31) {
09
10         my $dt = DateTime->new(
11           year   => $year,
12           month  => 3,
13           day    => $day,
14           hour   => 1,
15           minute => 59,
16           second => 59,
17           time_zone => "Europe/Berlin",
18         );
19
20         $dt->add(seconds => 1);
21
22         if($dt->hour() == 3) {
23             print "$year: DSTn";
24             next YEAR;
25         }
26     }
27     print "$year: No DSTn";
28     last;
29 }

Listing 2:
»dstchk«

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 }

Listing 3:
»lord_howe«

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";

Zeitkonten für Schaltsekunden

Wie auf [3] nachzulesen, ist eine Sekunde seit 1967 nicht mehr als Bruchteil eines Erdentages definiert, sondern über die viel konstantere Resonanz des Cäsiumatoms. Die Rotationsgeschwindigkeit unseres Planeten hat aber in den letzten 40 Jahren langsam abgenommen. Da die Erde gegenwärtig geringfügig länger als 24 mal 3600 atomgenaue Sekunden für eine Rotation benötigt, wurde seit 1972 etwa alle 18 Monate eine Schaltsekunde in die offizielle Zeit eingefügt. Maßgebend sind der 30. Juni und der 31. Dezember. Sobald 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 die Erde auf einmal schneller, würden die gestrengen Zeitwächter einfach eine Schaltsekunde aus der offiziellen Zeit entfernen. Allerdings ist das bisher noch nicht vorgekommen.

Listing »leapsec« (Listing 4) arbeitet sich von 1960 bis 2005 vor und untersucht, ob jeweils der 30.6. oder der 31.12. eine Schaltsekunde eingelegt hat. 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 – eine Schaltsekunde wurde entlarvt.

Dann 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 und die Anzahl der bisher gefundenen Schaltsekunden aus, siehe Abbildung 2. Die unterschiedlichen lokalen Schalt-Uhrzeiten am 1.7. erklären sich aus der Sommerzeitumstellung Anfang der 80er.

Listing 4:
»leapsec«

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 }

Löchrige Abstraktionen

Allerdings kümmert sich die auf Unix-Systemen übliche Zählung der Sekunden seit 1970 nicht um Schaltsekunden. Während also in Deutschland am 1. Januar 1999 der Sekundenzeiger von 00:59:59 Uhr auf 00:59:60 schnellte, erhöhte sich der Zählerwert auf den Unix-Maschinen nach dem Posix-Standard von 915148799 auf 915148800. 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 repräsentiert korrekt die Unix-Zeit 915148800.

Wer also zwei Unix-Zeiten voneinander subtrahiert und daraus die inzwischen verstrichene UTC-Zeit berechnet, muss korrigierend eingreifen, falls zwischenzeitlich eine Schaltsekunde stattfand. Details zu diesem verwirrenden Verfahren stehen unter [4] und [5].

Abbildung 2: An welchen Tagen von 1960 bis heute wurden Schaltsekunden eingelegt?

Abbildung 2: An welchen Tagen von 1960 bis heute wurden Schaltsekunden eingelegt?

»DateTime« bietet die Klassenmethode »from_epoch(epoch => )« 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« (Listing 5) zeigt, was passiert, wenn man zum 1.1.1990 einfach die aufmultiplizierten Sekunden für 5000 Tage hinzuzählt: Das Ergebnis ist »2003-09-09T23:59:53«, also fehlen sieben Sekunden bis zum vollen Tag. Addiert man hingegen mit »add(days => 5000)« einfach 5000 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 »->subtract_datetime_absolute()« das »DateTime«-Objekt » von einem »DateTime«-Objekt » abziehen und erhält daraufhin ein »DateTime::Duration«-Objekt, dessen »seconds()«-Methode ihm die tatsächlich vergangene Anzahl von Sekunden liefert.

Superman

Eine weitere Kuriosität ist die Datumsgrenze. Beim Fliegen 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 vorangegangenen 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üdostasien vorgelagerten Inselwelt.

Abbildung 3: Unterschiedliche Sprachen und Landesbräuche bei der Übersetzung und Formatierung eines Datumsstrings.

Abbildung 3: Unterschiedliche Sprachen und Landesbräuche bei der Übersetzung und Formatierung eines Datumsstrings.

Das Programm »daytrip« (Listing 6) zeigt, was passiert, wenn ein Reisender 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

Er fliegt also laut Plan am Sonntagmorgen los, kommt aber, obwohl der Flug sechseinhalb Stunden dauert, einen Tag früher an, nämlich am Samstagabend, noch vor Bekanntgabe der Lottozahlen. Schade, dass dies lediglich für die jeweilige Ortszeit gilt.

Viele Sprachen sprechen

Das Programm »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. Der erste Teil dieses Locale-Ausdrucks bestimmt Deutsch als Sprache, der zweite Teil bestimmt das Land und dessen spezielle Regeln.

Listing 7 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, »es_ES« und »es_MX« formatieren das Datum in Spanisch für Spanien und Mexiko. Ist der Formatierer einmal mit dem richtigen Locale initialisiert, bekommt er das »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 die unterschiedlichen Formatierungen ein und desselben »DateTime«-Objekts.

Listing 5:
»leapreveal«

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 "$dtn";

Listing 6:
»daytrip«

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:  $dtn";
19
20 $dt->add( DateTime::Duration->new(
21     hours   => 6,
22     minutes => 30) );
23
24 $dt->set_time_zone( 'Pacific/Honolulu' );
25 print "Ankunft: $dtn";

Listing 7:
»locales«

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: $dtn";
21 }

Schaltjahre

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, daher eine etwas kompliziertere Aufgabe: Wie lang ist die Liste aller Freitage, die zwischen 1980 und 2020 auf einen 29. Februar fielen?

Elegant ist diese Rechnung mit zwei Sets von »DateTime«-Objekten zu lösen: 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 »frifeb29« (Listing 8) mit »weekly(days => 5)« ein weiteres Set, das alle Freitage (den fünften 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, spezifiziert der Parameter »start« sowohl ein Startdatum als auch das Ende des Suchzeitraums in einem »DateTime«-Objekt. Die While-Schleife ab Zeile 18 stößt den Iterator mit »next()« an und treibt ihn weiter. Das Ergebnis: Im Jahr 1980 fiel der 29. Februar auf einen Freitag und 2008 wird es wieder so weit sein. (jcb)

Listing 8:
»frifeb29«

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 }

Infos

[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2006/02/Perl]

[2] “What time is it in Indiana?”:[http://www.mccsc.edu/time.html]

[3] Homepage des von Dave Rolsky geleiteten Datetime-Projekts: [http://datetime.perl.org]

[4] Unix-Time: [http://en.wikipedia.org/wiki/Unix_time]

[5] UTC, TAI and Unix-Time: [http://cr.yp.to/proto/utctai.html]

Der Autor


Michael Schilli arbeitet 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 ist: [http://perlmeister.com]

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben