Ging es um Datumsberechnungen, war ich bislang ein unbekehrbarer
Date::Manip
-Jünger, so schön einfach zu bedienen schien mir
Sullivan Becks Modul. Doch als
ich neulich Steffen Beyers Erzeugnis Date::Calc
in die Hände bekam,
und feststellte, daß es ähnliche Funktionalität bietet und dabei
viel, viel schneller läuft, ließ ich ab vom Gewohnten und gab mich voll
diesem neuen Teufelszeug hin!
Date::Calc
wird
nach dem üblichen Verfahren installiert. Die aktuelle Version liegt
auf dem CPAN unter CPAN/modules/by-module/Date/Date-Calc-4.1.tar.gz
kostenlos zur Abholung bereit.
Date::Calc
-TourDatumsangaben liegen in Date::Calc
üblicherweise als Dreier-Array
($year, $month, $day)
vor. Das heutige Datum liefert die
Funktion Today()
folgendermaßen:
use Date::Calc qw(Today); ($year, $month, $day) = Today();
Hier zeigt sich, daß Date::Calc
von sich aus noch keine Funktionen
exportiert. So steht Today()
erst zur Verfügung, nachdem es der
Export-Liste, die der use
-Anweisung anhängt, beiliegt. Das Jahr
liegt heuer als ``1998'' vor, Monat und Tag entsprechen dem gesundem
Menschenverstand, starten also beide bei 1
.
Um nun zum Beispiel die Tage von heute bis Weihnachten zu
zählen, muß man keine kalendertechnischen Klimmzüge absolvieren und
mit Schaltjahren und ähnlichem Unbill
herumjonglieren, sondern bemüht einfach Delta_Days()
, wie in
Listing xmas.pl
vorgestellt: Delta_Days()
nimmt zwei
Datumsangaben entgegen, also insgesamt sechs Skalare. Nachdem das
heutige Datum mit Today()
ermittelt ist, steckt xmas.pl
den
24.12. des gleichen Jahres in den Array @xmas
, übergibt ihn
Delta_Days()
und -- schwupps! -- schon liefert es die Anzahl der
Tage zwischen den zwei Datumsangaben zurück.
01 #!/usr/bin/perl -w 02 ################################################## 03 # Michael Schilli, 1998 (mschilli@perlmeister.com) 04 ################################################## 05 06 use Date::Calc qw(Delta_Days Today); 07 08 ($year, $month, $day) = Today(); 09 @xmas = ($year, 12, 24); 10 11 $days = Delta_Days($year, $month, $day, @xmas); 12 print "Noch ", $days, " Tage bis Weihnachten.\n";
Zeigt man (zum Beispiel mit dem in dieser Reihe vorgestellten Paket
Chart
) aktuelle Graphiken an, stellt sich zuweilen das Problem, daß
man die Wochentage der hinter einem liegenden Woche auf die X-Achse zaubern
muß -- doch wo beginnen? Listing week.pl
löst das Problem, indem
es einen Array @days aufbaut, in dem es die Wochentags-Kürzel
hintereinanderreiht. Da Date::Calc
aus deutschen Landen stammt,
bietet es eine vorbildliche Vielsprachen-Steuerung an, so daß week.pl
,
falls heute Donnerstag wäre, folgende Reihe ausgäbe:
Fr Sa So Mo Di Mi Do
Zunächst setzt die Language()
-Funktion die verwendete Sprache auf
"Deutsch"
. Language()
selbst verarbeitet aber keinen String als
Parameter, sondern einen kleinen Integer, den kein Mensch kennt, außer
der Funktion Decode_Language()
, die den National-String in das
interne Format umwandelt. Dann ermittelt week.pl
das aktuelle
Datum als Startwert und springt dann
in eine Schleife über sieben Tage, in der es pro Durchgang um einen
Tag zurückspringt.
Mit Day_of_Week()
ermittelt es jeweils die Nummer
des aktuellen Wochentags, verwandelt diese mit Day_of_Week_to_Text()
in einen Wochentags-String voller Länge (z.B. "Montag"
), kürzt diesen
auf zwei Buchstaben und fügt ihn schließlich am Anfang (!) des Arrays
@days
ein. Um pro Schleifendurchgang einen Tag zurückzusetzen,
kommt die Funktion Add_Delta_YMD()
zum Zug, die ein dreiteiliges
Startdatum und ein ebenfalls dreiteiliges Delta aus Jahren, Monaten und
Tagen erwartet. Im
vorliegenden Fall führt der letzte Parameter den Wert -1
, was
einem Zeitsprung von einem Tag in die Vergangenheit entspricht.
01 #!/usr/bin/perl -w 02 ################################################## 03 # Michael Schilli, 1998 (mschilli@perlmeister.com) 04 ################################################## 05 06 use Date::Calc qw(Today Decode_Language Language 07 Day_of_Week Day_of_Week_to_Text 08 Add_Delta_YMD); 09 10 Language(Decode_Language("Deutsch")); 11 12 @date = Today(); 13 14 foreach (1..7) { 15 $day = Day_of_Week_to_Text(Day_of_Week(@date)); 16 unshift(@days, substr($day, 0, 2)); 17 @date = Add_Delta_YMD(@date, 0, 0, -1); 18 } 19 20 print "@days\n";
Die Funktion Day_of_Week()
kalkuliert aus Jahr, Monat und Tag
den Wochentag. Listing first.pl
zeigt, wie sich die Monatsersten
des Jahres 1998 berechnen lassen.
Ohne explizit ausgewähltes Deutsch-Modul liefern Funktionen wie
Month_to_Text()
, die den Namen eines Monats, der als Zahl vorliegt,
liefert, englische Wörter:
Thursday, January 1st, 1998 Sunday, February 1st, 1998 ... Sunday, November 1st, 1998 Tuesday, December 1st, 1998
01 #!/usr/bin/perl -w 02 ################################################## 03 # Michael Schilli, 1998 (mschilli@perlmeister.com) 04 ################################################## 05 06 use Date::Calc qw(Today Day_of_Week 07 Day_of_Week_to_Text Month_to_Text); 08 $year = 1998; 09 10 foreach $month (1..12) { 11 $dow = Day_of_Week($year, $month, 1); 12 13 printf "%s, %s 1st, %d\n", 14 Day_of_Week_to_Text($dow), 15 Month_to_Text($month), $year; 16 }
``Laß uns regelmäßig jeden zweiten Freitag im Atzinger zusammensitzen
und ein, zwei Augustiner trinken!'' -- wer hat diesen Satz nicht so oder
so ähnlich schon ausgesprochen. Listing secfri.pl
zeigt, wie sich das
in die Tat umsetzen läßt: Ausgehend vom aktuellen Datum, spult das
Skript zwölfmal jeweils einen Monat vor, um für den aktuellen Monat mittels der
Funktion Nth_Weekday_of_Month_Year()
das Datum des zweiten Freitags
zu bestimmen. Den Freitag nimmt die Funktion dabei über den dritten
Parameter als "5"
entgegen, dies enspricht der internen
Numerierung von Date::Calc
die am Montag mit "1"
startet.
Der vierte Parameter gibt an der wievielte Freitag im Monat
gemeint ist. Die Ausgabe ist verblüffend richtig:
11.09.1998 09.10.1998 ... 09.07.1999 13.08.1999
01 #!/usr/bin/perl -w 02 ################################################## 03 # Michael Schilli, 1998 (mschilli@perlmeister.com) 04 ################################################## 05 06 use Date::Calc qw(Today Nth_Weekday_of_Month_Year 07 Add_Delta_YMD); 08 09 my ($year, $month, $day) = Today(); 10 11 foreach (1..12) { # Zwölf Monate lang 12 13 # Zum nächsten Monat vorspulen 14 ($year, $month, $day) = 15 Add_Delta_YMD($year, $month, $day, 0, 1, 0); 16 17 # 5: Freitag 2: 2. Freitag 18 ($y, $m, $d) = Nth_Weekday_of_Month_Year($year, 19 $month, 5, 2); 20 printf "%02d.%02d.%d\n", $d, $m, $y; 21 }
Zum Abschluß noch ein Beispiel, das aufzeigt, wie einfach sich mit dem Kalendermodul die Log-Dateien von Webservern analysieren lassen: Eine Zeile des Access-Logs sieht üblicherweise in etwa so aus:
kunde.com - - [30/Jul/1998:02:31:06 -0700] "GET /index.html HTTP/1.0" 200 7304
Um festzustellen, wieviele Hits letzte Woche auf die einzelnen Wochentage verteilt eingegangen sind, iteriert man über die Zeilen der Datei, stellt fest, ob das angegebene Datum (in eckigen Klammern) in die letzte Woche fiel, und wenn, erhöht man einen Zähler für den entsprechenden Wochentag.
Ob ein angegebes Datum in einem festgelegten Zeitrahmen liegt, läßt sich
dadurch bestimmen, daß man sämtliche Angaben in Tage umrechnet, die
seit einem Zeitpunkt weit in der Vergangenheit vergangen sind. Die
Funktion Date_to_Days()
macht Nägel mit Köpfen und liefert zu
einem vorgegeben Datum (Tag-Monat-Jahr) die Anzahl der Tage, die seit
dem Urknall des Universums, nein, Spaß beiseite, seit dem 1.1. des
Jahres 1 anno Domini vergangen sind. So ergibt sich für den
01.08.1998 etwa ein Wert von 729602 Tagen.
Liegen Datumsangaben in solchen Zahlen vor, kann man Vergleiche einfach
numerisch mit <
und >
durchführen.
In Listing log.pl
drehen wir ein bißchen an der Schwierigkeitsschraube,
bitte anschnallen und die Arme im Fahrzeug lassen!
Die zentrale Datenstruktur dort ist der Array @result
, der als Elemente
Referenzen auf kleine 2er-Listen enthält, die als erstes
Element die Abkürzung des Wochentags und als zweites einen Zähler führen,
der anzeigt, wieviele Hits am entsprechenden Wochentag schon eingegangen
sind. @result
führt genau 7
Einträge. Falls heute Donnerstag ist,
steht dort für jeden Tag von Freitag letzter Woche bis heute jeweils
ein Element, das den Wochentag mitsamt dem zugehörigen Zähler enthält.
Um diese Struktur anfangs aufzubauen, könnten wir wieder auf die Logik
nach Listing week.pl
zurückgreifen, aber nach dem Motto
Öfter mal was Neues! legen wir diesmal in @days
eine Liste
mit Mo
bis So
ab, bestimmen das heutige Datum (@date
) mitsamt
der zugehörigen Wochentagszahl ($dow
)
und springen (Zeile 11) in eine Schleife mit 7
Durchgängen, die jeweils den richtigen Wochentag aus @days
holt
und mitsamt eines auf 0
vorbesetzten Zählers in @result
einfügt. Die
Modulo-Logik $_ % 7
setzt dabei den Index des aktuell in @days
gelesenen Elements wieder an den Anfang zurück, falls der Zähler über das Ende
des Arrays hinausschießt.
Die Zeilen 15-17 legen ein Zeitfenster für die vergangene Woche fest,
$window_from
und $window_to
sind jeweils die zwischen Anno Tobak
und dem Anfang bzw. Ende des Zeitfensters vergangenen Tage. Kommt also
ein ebenfalls in diesem Format dargestelltes Datum daher, läßt sich
leicht feststellen, ob es innerhalb oder außerhalb des Fensters liegt.
Zeile 20 öffnet die Log-Datei zum Lesen, der reguläre Ausdruck in Zeile 25 fördert
für jede durchlaufene Zeile das Zugriffsdatum zutage.
Nun hat Date::Calc
zwar die Funktion Parse_Date()
, die Datumsstrings
im Format
Thu Jul 9 19:18:28 PDT 1998
importiert, doch für eine Funktion, die das in der Log-Datei
verwendete Format 18/Aug/1997:20:58:42 -0700
beherrscht, suchte
ich vergebens -- egal, log.pl
macht das zu Fuß.
Da der Monat in dem
oben einsehbaren dreibuchstabigen Kürzel vorliegt, muß er
mit der Funktion Decode_Month()
aus Date::Calc
in eine Zahl von 1-12
umgewandelt
werden, bevor Date_to_Days()
zum Einsatz kommt und den Tageswert
berechnet. Die Zeilen 33 und 34 stellen dann fest, ob das gefundene Datum
im eingestellten Bereich liegt. Ist dies der Fall,
greift Zeile 36 auf das @result
-Element am Index $days - $windows_from
zu (entspricht dem richtigen Wochentag in @result
) und erhöht den
Zähler (zweites Element der Unter-Liste) um Eins.
Bleibt nur noch, in den Zeilen 43-46 den Array @result
zu durchlaufen
und das Ergebnis auszugeben -- man stelle sich vor, was man daraus mit
David Bonners Chart
-Paket zaubern könnte!
01 #!/usr/bin/perl 02 ################################################## 03 # Michael Schilli, 1998 (mschilli@perlmeister.com) 04 ################################################## 05 06 use Date::Calc qw(Today Day_of_Week Decode_Month 07 Add_Delta_YMD Date_to_Days); 08 09 @days = qw/Mo Di Mi Do Fr Sa So/; 10 11 @today = Today(); # Heutiges Datum 12 $dow = Day_of_Week(@today); # Wochentag (Nummer) 13 14 foreach ($dow..$dow+6) { 15 push(@result, [$days[$_ % 7], 0]); 16 } 17 # 'Fenster' für Woche 18 @six_days_back = Add_Delta_YMD(@today, 0, 0, -6); 19 $window_from = Date_to_Days(@six_days_back); 20 $window_to = Date_to_Days(@today); 21 22 # Access-Log-Datei bearbeiten 23 open(LOGFILE, "<access.log") || 24 die "Cannot open file access.log"; 25 26 while(<LOGFILE>) { # Datum steht in [...] im 27 # Format dd/Mon/yyyy:hh:mm:ss 28 if(m#\[(\d+)/(\w+)/(\d+)#) { 29 ($day, $monthname, $year) = ($1, $2, $3); 30 $month = Decode_Month($monthname); 31 @date = ($year, $month, $day); 32 33 # Prüfen, ob Datum in 34 # letzter Woche liegt 35 $days = Date_to_Days(@date); 36 if($days >= $window_from && 37 $days <= $window_to) { 38 # Wochentag-Zähler erhöhen 39 $result[$days-$window_from]->[1]++; 40 } 41 } 42 } 43 close(LOGFILE); 44 45 # Wochen-Report ausgeben 46 foreach $result (@result) { 47 my ($weekday, $count) = @$result; 48 printf "$weekday: %d\n", $count; 49 }
Wie jedem guten Modul liegt auch Date::Calc
ausführliche Dokumentation
bei, die nach der Installation mit perldoc Date::Calc
zum Vorschein
kommt. Viel Spaß damit!
Ein ganzes Jahr Perl-Snapshot ist um, meine lieben Leser: Zeit, ein bißchen in sich zu gehen und über die Zukunft nachzudenken. Hat's Euch gefallen? Was gibt's zu verbessern? Was interessiert Euch besonders? Rafft's Euch auf, schreibt's, und sagt's mir, ob Ihr noch weitere G'schichterln aus Perl-Land lesen wollt. Wenn ich genug Fanpost und Themenvorschläge kriege, häng' ich glatt noch ein Jahr dran, soviel Spaß hat's gemacht! Laßt was hören!
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. |