Aus Linux-Magazin 12/2007

Perl-Skript prüft Rechentrick

Gehirnakrobaten rechnen gerne vor Publikum, auf welchen Wochentag ein zugerufenes Datum fällt. Den Trick kann auch Otto Normalrechner mit etwas Übung lernen. Ob er zuverlässig funktioniert, prüft das im Snapshot vorgestellte Skript mit aktueller Perl-Technologie.

Neulich las ich auf dem Weg zur Arbeit im Zug das Buch “Mind Performance Hacks” [2] und stieß auf den Hack Nummer 43, der zeigt, wie jedermann mit etwas Übung den Wochentag zu jedem beliebigen Datum im Kopf ausrechnen kann. Das Verfahren geht zurück auf Lewis Carroll, den Autor des Romans “Alice im Wunderland”.

Vier Werte sind der Reihe nach zu berechnen: der Jahreswert, der Monatswert, der Tageswert und ein vierter Wert zur Anpassung. Man addiert diese Werte anschließend, bestimmt den Restwert nach einer Division durch 7 und erhält verblüffenderweise den gewünschten Wochentag als Zahl zwischen 0 (Sonntag) und 6 (Samstag).

Als Beispiel dient der Entstehungstag dieses Textes (4.10.2007). Der Jahreswert bestimmt sich aus der folgenden Formel:

(YY + (YY div 4)) mod 7

Dabei ist »YY« die zweistellige Jahreszahl, also 07 für 2007. Der »div«-Operator führt eine Division ohne Restwert aus, 7 div 4 gleich 1, da 7 geteilt durch 4 den Wert 1 ergibt und der Rest 3 wegfällt. Das Ergebnis wird jetzt noch modulo 7 genommen: 1 modulo 7 ergibt 1. Der Jahreswert ist also 1.

Durch die Jahrhunderte

Zum Monatswert: Der ermittelt sich aus Tabelle 1, die auch Nicht-Gehirnakrobaten mit Hilfe einiger später zu erläuternder mnemotechnischer Tricks im Kopf behalten können. Der Oktober hat laut Tabelle den Monatswert 0. Der Tageswert ist einfach der fortlaufend nummerierte Tag des Monats, für den 4. Oktober also der Wert 4.

Tabelle 1:
Monatswerte

Wert

Monat

Januar

3

Februar

3

März

6

April

1

Mai

4

Juni

6

Juli

2

August

5

September

Oktober

3

November

5

Dezember

Der vierte Wert für die Kopfrechnung ergibt sich aus Tabelle 2, die für Jahreszahlen im 21. Jahrhundert (2000 bis 2099) den Wert 6 angibt. Diese Tabelle braucht man sich nicht ganz zu merken, es genügt, die Werte für 2000 (also 6) und 1900 (0) im Kopf zu behalten. In Schaltjahren wäre vom gefundenen Wert noch 1 abzuziehen, wenn das gesuchte Datum im Januar oder Februar liegt. Da aber 2007 kein Schaltjahr ist, entfällt das hier glücklicherweise.

Tabelle 2:
Jahreswerte

Wert

Jahr

1700

4

1800

2

1900

2000

6

2100

4

2200

2

2300

Die vier gefundenen Werte sind also 1 (Jahreswert), 0 (Monatswert), 4 (Tageswert) und 6 (Anpassung). Die Summe ist 11, und 11 modulo 7 ergibt den Wert 4. Ein Blick in Tabelle 3 mit den Wochentagen offenbart, dass – Trommelwirbel – der 4.10.2007 auf einen Donnerstag fällt, und das ist tatsächlich richtig!

Tabelle 3:
Wochentagswerte

Wert

Wochentag

Sonntag

1

Montag

2

Dienstag

3

Mittwoch

4

Donnerstag

5

Freitag

6

Samstag

Probe aufs Exempel

Jetzt stellt sich natürlich die Frage, ob diese Berechnung auch tatsächlich für jedes beliebige Datum zum richtigen Wochentag führt. Das Skript in Listing 1 schraubt sich daher zur Probe vom 1.1.1700 an durch sämtliche Tage der Neuzeit, über die Entdeckung Amerikas und den Goldrausch, durch den ersten und den zweiten Weltkrieg bis in die heutige Zeit und auch noch weiter in die Zukunft, bis zum Raumschiff Enterprise in der nächsten Generation mit Jean-Luc Picard auf dem Kommandosessel im 24. Jahrhundert.

Listing 1:
»mindcal«

01 #!/usr/bin/perl
02 use strict;
03 use warnings;
04 use Test::More qw(no_plan);
05 use DateTime;
06
07 my @MONTH = qw(0 3 3 6 1 4 6 2 5 0 3 5);
08 my %ADJ   = qw(1700 4 1800 2 1900 0 2000 6
09                2100 4 2200 2 2300 0);
10
11 my $dt = DateTime->new(
12     year  => 1700,
13     month => 1,
14     day   => 1,
15 );
16
17 while(1) {
18   my $calc = wday_mindcal(
19          $dt->year, $dt->month, $dt->day);
20
21   is($calc, $dt->wday() % 7, "$dt");
22
23   $dt->add(days => 1);
24
25   last if $dt->year() > 2399;
26 }
27
28 ###########################################
29 sub wday_mindcal {
30 ###########################################
31     my($year, $month, $day) = @_;
32
33     use integer;
34
35     my $year2 = $year % 100;
36     my $cent  = $year / 100;
37     my $y     = ($year2 + ($year2 / 4)) % 7;
38
39     my $m     = $MONTH[$month-1];
40     my $d     = $day;
41
42     my $adj   = $ADJ{$cent * 100};
43
44     $adj-- if leap_year($year) and
45               $month <= 2;
46
47     return( ($y+$m+$d+$adj) % 7 );
48 }
49
50 ###########################################
51 sub leap_year {
52 ###########################################
53     my($year) = @_;
54
55     return 0 if $year % 4;
56     return 1 if $year % 100;
57     return 0 if $year % 400;
58     return 1;
59 }

Das Skript definiert in Zeile 29 die Funktion »wday_mindcal()«, die das vierstellige Jahr, den Monat und den Tag eines Datums entgegennimmt, nach den oben erläuterten Regeln die Jahres- beziehungsweise Monats- und Tages-Anpassungszahlen errechnet und die notwendigen Operationen ausführt, um daraus die Wochentagsnummer zu ermitteln.

Die Monatszahlen stehen im Array »@MONTH«, von Januar bis Dezember. Da Arrays nicht von 1, sondern von 0 an durchnummeriert sind, zieht das Skript vom gesuchten Monat erst 1 ab, bevor es unter dem so erhaltenen Index in den Array hineingreift. Die Anpassungszahlen für die verschiedenen Jahrhunderte stehen im Hash »%ADJ«. Die Zeilen 8 und 9 füllen den Hash, in dem sie ihm eine Liste zuweisen, die abwechselnd Jahrhundertzahlen und die jeweils zugehörigen Anpassungswerte enthält.

Die Vergleichswerte errechnet das CPAN-Modul »DateTime«, das tageweise vom 1.1.1700 bis zum 31.12.2399 hochzählt und bei jedem Schleifendurchgang Tag, Monat und Jahr liefert. Die Endlosschleife ab Zeile 17 ist nicht wirklich endlos, denn Zeile 25 bricht den Reigen ab, falls das Jahr des aktuellen Datums 2399 überschreitet. Ist die Schleife noch nicht am Ende, addiert die Methode »add()« des »DateTime«-Objekts unten einen Tag zum aktuellen Datum – und schon nimmt eine weitere Schleifenrunde ihren Anfang.

CPAN zum Vergleich

»DateTime« verfügt natürlich ebenfalls über eine Wochentagsfunktion »wday()«, die allerdings die Tage von 1 (Montag) bis 7 (Sonntag) durchnummeriert. »mindcal« hängt also noch schnell eine Modulo-7-Operation an, damit sich die Sonntag-7 in eine 0 verwandelt und man die Ergebnisse einfach mit den von »wday_mindcal()« gelieferten Werten von 0 (Sonntag) bis 6 (Samstag) vergleichen kann.

Die Rest-lose Division »div« implementiert das Skript durch das Pragma »use integer« (Zeile 33). Ist dieser Modus gesetzt, rechnet Perl nicht mehr mit Fließkommazahlen, sondern runden Integern, sodass 17:4 zum Beispiel glatt 4 ergibt.

Ob ein Jahr ein Schaltjahr ist, ermittelt die Funktion »leap_year()« ab Zeile 51. Ist es durch 4 teilbar, ist es ein Schaltjahr, es sei denn, es ist durch 100 teilbar. Ist es durch 400 teilbar, ist es wiederum ein Schaltjahr. Dieselbe Logik ist natürlich auch in »DateTime« enthalten, aber schließlich soll ja das Skript den Kopfrechner simulieren.

Die Vergleiche führt die vom CPAN-Modul Test::More exportierte Funktion »is()« aus. Sie nimmt drei Parameter entgegen, den Ist-Wert des simulierten Kopfrechners in »mindcal«, den Sollwert von »DateTime« sowie einen Kommentar, der aus einem stringifizierten und damit lesbaren »DateTime«-Objekt besteht. Mit »no_plan« hinter der »use«-Anweisung von Test::More stellt das Skript klar, dass die Anzahl der auszuführenden Tests unbekannt ist.

Abbildung 1 zeigt die Ausgabe von »mindcal«. Da niemand Lust hat, sich 250000 vorbeirasende Ausgaben anzusehen, fasst das Modul Test::Harness die Ergebnisse einer solchen Testsuite übersichtlich zusammen. Der Aufruf

perl -MTest::Harness -e'runtests("mindcal")'

lässt das Skript »mindcal« ablaufen und gibt genau acht, welche Tests »ok« liefern und welche »not ok«. Am Ende erscheint eine schöne Zusammenfassung, wie in Abbildung 2 gezeigt.

Abbildung 1: Test::More druckt die Ergebnisse der Einzeltests übersichtlich aus.

Abbildung 1: Test::More druckt die Ergebnisse der Einzeltests übersichtlich aus.

Abbildung 2: Da niemand in der Lage wäre, Tausende von Tests live mitzuverfolgen und dabei auch noch einzelne Ausreißer zu erkennen, fasst das Perl-Modul Test::Harness alle Ergebnisse zusammen.

Abbildung 2: Da niemand in der Lage wäre, Tausende von Tests live mitzuverfolgen und dabei auch noch einzelne Ausreißer zu erkennen, fasst das Perl-Modul Test::Harness alle Ergebnisse zusammen.

Aufmerksame Leser bemerken sicher, dass der Kommandozeilenaufruf in der Abbildung etwas anders aussieht als oben gezeigt. Test::Harness hat die unangenehme Marotte, bei mehr als 100000 Tests auf STDERR herumzunörgeln. Der in Abbildung 2 dargesellte Aufruf schickt diese Meldungen über den »__WARN__« -Signalhandler in ein schwarzes Loch. Ergebnis: Der alte Fuchs Lewis Carroll hatte tatsächlich recht, alle 255669 Tests laufen fehlerlos durch.

Mnemonische Tricks

Hier noch eine Gedächtnishilfe zum leichteren Einprägen der Monatszahlen. Von Januar bis Dezember ergeben sie aneinandergereiht 033614625035. Diese Monsterzahl merke ich mir in Dreiergruppen: 033-614-625-035 und folgendem Spruch: “Nach der Einleitung mit 033 kommt 614, ähnlich wie die Kreiszahl 3,14, nur eben mit 6 am Anfang, weil vorher schon zweimal 3 dran war. Die folgende 625 fängt ebenfalls mit 6 an und ist 252, also die Fläche eines Quadrats! Das ist doch eine schöne Abwechslung zum vorhergehenden Kreis. Und am Ende steht 035, 2 mehr als die Einleitung mit 033.”

Die Dreiergruppen stelle ich mir optisch vor und kann deswegen für Juli zum Beispiel schnell an den Anfang der zweiten Jahreshälfte springen, wo die dem Juli zugeordnete Monatszahl 6 steht.

Üben, üben, üben

Die Ermittlung der Jahreszahl kann sich besonders bei den letzten Jahren des vorigen Jahrhunderts etwas in die Länge ziehen. Zum Beispiel 1995: 95 geteilt durch 4 ist 23 plus ein paar Zerquetschte, die unter den Tisch fallen. 95 plus 23 ist dann 118, 118 modulo 7 ist 6. Klappt das mit dem Kopfrechnen noch nicht so schnell, kann sich der Rechenakrobat zum Üben darauf beschränken, vom Publikum nur Fragen nach Datumsangaben in einem bestimmten (beispielsweise dem aktuellen) Jahr zuzulassen. Dann rechnet er die Jahreszahl im Voraus aus und merkt sie sich. Für 2007 ist sie beispielsweise 1.

Tabelle 4 zeigt noch einige Rechenbeispiele zum Üben. Zu beachten ist, dass 2004 ein Schaltjahr ist, was bei einem Datum im Januar oderFebruar zu einem Punktabzug bei der Anpassung führt, bei anderen Monaten jedoch nicht. Wer den Bogen raus hat, kann seine Rechenkünste einem erstaunten Publikum vorführen. Zunächst im kleinen Kreis bei Freunden, dann auf Partys und endlich in ausverkauften Sporthallen! (jcb)

Tabelle 4:
Beispiele

 

Datum

Jahreszahl

Monatszahl

Tageszahl

Anpassung

Wochentag

01.01.1970

3

1

4 | Donnerstag

14.07.1995

6

6

14

5 | Freitag

11.09.2001

1

5

11

6

2 | Dienstag

01.02.2004

5

3

1

5

0 | Sonntag

01.03.2004

5

3

1

6

1 | Montag

04.10.2007

1

4

6

4 | Donnerstag

Infos

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

[2] Ron Hale-Evans, “Mind Performance Hacks”: O\’Reilly, 2007

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.

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