Python und Datumsberechnungen

von Hubert Schmid vom 2013-05-26

Viele Anwendungen müssen mit Kalenderdaten umgehen können. Auf den ersten Blick sieht das vergleichsweise einfach aus. Im Detail gibt es jedoch viele Dinge zu beachten. Im folgenden Listing ist dazu eines meiner Lieblingsbeispiele zu sehen:

$ TZ=Asia/Kathmandu $ date -d '1986-01-01 00:00:00' date: invalid date ‘1986-01-01 00:00:00’

Das Programm hat recht: Dieses Datum existiert in Nepal nicht, und die Fehlermeldung verdeutlicht die Lücke zwischen Intuition und Realität. Denn auch abgesehen vom Wechsel zwischen Sommer- und Winterzeit ist es keineswegs so, dass jede Zeit oder jeder Tag an jedem Ort existiert.

Für fast alle Programmiersprachen gibt es mehr und weniger umfangreiche Bibliotheken für Datumsberechnungen. Aufgrund der inhärenten Kalenderkomplexität können sie jedoch unweigerlich nur einen Teil der Use-Cases gut unterstützen. Entweder sie fokussieren sich auf eine einfache Verwendung und haben Schwierigkeiten mit Ausnahmefällen, oder sie bilden alle Einzelheiten zu Lasten der Lesbarkeit ab.

Meine Erfahrung ist: Entscheidend für Effektivität und Effizienz ist nicht die Wahl der richtigen Bibliothek, sondern die Wahl eines geeigneten Lösungswegs. Das gilt aus meiner Sicht in der Softwareentwicklung für fast alle Technologie-Entscheidungen, und ich verwende das Beispiel Datumsberechnungen nur, weil es daran besonders auffällig ist, und weil ich kürzlich darüber gestolpert bin.

Meine Anforderung war, aus einer Reihe von täglich erzeugten Backups entsprechend der Retention-Policy tägliche, wöchentliche, monatliche und jährliche Sicherungen beizubehalten – beziehungsweise die nicht mehr benötigten zu löschen. Dabei muss berücksichtigt werden, dass nicht für jeden Tag ein Backup existiert. Erst im zweiten Anlauf bin ich auf folgende Implementierung gekommen:

def filter_dates(dates, **policy): formats = { 'year': '%Y', 'month': '%Y%m', 'week': '%Y%W', 'day': '%Y%j', } dates = sorted(dates, reverse=True) result = set() for interval, retention in policy.items(): format = formats[interval] candidates = { date.strftime(format): date for date in dates } result.update(sorted(candidates.values(), reverse=True)[:retention]) return result

Der Funktion wird die Liste der Kalendertage übergeben, für die ein Backup vorhanden ist. Für jedes Intervall wird aus dieser Liste das früheste Datum bestimmt, das in das entsprechende Intervall fällt. Schließlich werden entsprechend der Retention-Policy jeweils die gerade ausgewählten Daten aus den neusten Intervallen übernommen und als Menge zurückgegeben.

Im Nachhinein sieht das nach einer offensichtlichen Lösung aus. Dennoch habe ich dafür einen zweiten Anlauf benötigt, nachdem ich erkannt hatte, dass mein erster Ansatz zu umständlich gewesen ist. Ich kann mir gut vorstellen, diese Aufgabe als Kata für andere Programmiersprachen zu verwenden. An Ergebnissen dazu wäre ich sehr interessiert.

Demonstrieren lässt sich die Funktionalität mit folgenden Code. Dabei werden zunächst 15 zufällige Kalendertage der letzten 50 Tage ausgewählt. Anschließend werden mit einer einfachen Retention-Policy die beizubehaltenen Daten bestimmt und schließlich ausgegeben.

initial = datetime.date(2013, 5, 26).toordinal() dates = { datetime.date.fromordinal(initial - i) for i in random.sample(range(50), 15) } filtered = filter_dates(dates, week=9, day=5) print(' '.join(date.strftime('%m/%d') for date in sorted(dates))) print(' '.join('^^^^^' if date in filtered else ' ' for date in sorted(dates)))

Die Ausgabe ist im folgenden Listing zu sehen. Die erste Zeile enthält die zufällig ausgewählten Kalendertage, und in der zweiten Zeile sind die Gefilterten markiert.

04/12 04/13 04/14 04/17 04/21 04/26 04/28 04/30 05/03 05/04 05/12 05/13 05/14 05/18 05/26 ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^