Erschienen in 64'er Magazin, Ausgabe 03/1994 · Originaldatei: KALENDER.TXT
Hinweis: Dies ist das an die Redaktion eingereichte Manuskript, nicht der gedruckte Endtext. Layout, Bildunterschriften, Korrekturen und Kürzungen der Redaktion können in der veröffentlichten Fassung abweichen.
Kalender unter Kontrolle
Mit einem kleinen Trick bekommen Sie als Programmierer die Jahrhunderte in den Griff. Wir zeigen Ihnen, mit welchen Verfahren Routinen geschrieben werden, die mit Tagesdaten umgehen.
Nikolaus M. Heusler
In vielen Computerprogrammen kommen als Daten auch Tage vor. Beispielsweise wird ein Datum eingegeben, das dann im Folgenden weiterverwertet wird. Nun könnte das Programm natürlich beispielsweise den Wochentag dieses Datums ausrechnen. Dennoch werden Sie eine solche Funktion nur bei sehr wenigen Programmen finden. So kompliziert kann die Berechnung doch nicht sein. Doch wie sagen wir dem Rechner etwa, daß der Dezember 31 Tage und der Februar manchmal 28, manchmal aber auch 29 hat? Am besten gar nicht.
Manchmal muß auch die Differenz zweier Daten in Tagen berechnet werden. Wozu man das braucht? Denken Sie nur an Biorhythmus-Programme (Duden!). Aber auch für viele andere ernsthafte Bereiche, etwa das weite Feld der »Bürokratie« werden solche Algorithmen benötigt. Wie gesagt, nur wenige Lösungen sind bekannt. Ein Beispiel dafür, daß das Problem zu schwer ist? Mag sein. Aber es gibt einige Tricks, wie man sich das Leben hier bedeutend einfacher machen kann.
Stellen wir uns eine Aufgabe. Ein Programm soll die Nummer eines Tages im Kalenderjahr ausgeben. Wir sollten jetzt nicht einfach wild drauflos programmieren, sondern das Problem systematisch angehen. Zunächst einmal ist das gesuchte Datum einzugeben.
10 INPUT"DATUM FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5))
Wir geben den String im Format »TTMMJJJJ« ein, also ohne Punkte oder sonstige Trennzeichen. Die Eingabe »18021991« RETURN bedeutet mithin 18. Februar 1991. Das Jahrhundert soll also eingegeben werden. In einem anwenderfreundlichen Programm erscheinen diese Hinweise übrigens auch am Bildschirm, das aber nur nebenbei. Danach zerlegen wir die Eingabe in drei Teile und ermitteln die Wertigkeit von Tag, Monat und Jahr. In unserem Beispiel wird T = 18, M = 2 und J = 1991. Zur Sicherheit kann eine Prüfung der Eingabe auf Plausibilität nicht schaden, das erledigen die folgenden Zeilen:
50 IF M<1 OR M>12 THEN RUN
60 IF T<1 OR T>31 THEN RUN
Wie berechnet man jetzt die Tagesnummer? Im Prinzip könnte man einfach durchzählen. Aber es geht einfacher. In der folgenden Tabelle sehen Sie zusammengefaßt die Anzahl Tage der Monate März bis Dezember:
Monat Tage Monat Tage
3 31 8 31
4 30 9 30
5 31 10 31
6 30 11 30
7 31 12 31
Bis auf den Sprung von 7 nach 8 wechseln sich also immer die Werte 30 und 31. Und nun probieren Sie im Direktmodus folgende Schleife aus:
FOR M=3 TO 12 : PRINT M, INT (30.6 * (M+1) ) : NEXT
Dieser Algorithmus gibt also genau die obige Tabelle aus. Das ist einfacher, als die Monatslängen in einem Array zu Programmbeginn zu definieren und kostet weniger wertvollen Speicherplatz. Aber wie steht es mit Januar und Februar? Das ist weiter nicht schlimmt, wir beginnen die Zählung des Jahres einfach mit dem ersten März und hängen die Monate Januar und Februar hinten nach dem Dezember an. Der Februar ist somit der letzte Monat unseres »EDV-Jahres«, es ist egal, ob er 28 oder 29 Tage hat, die Zählung arbeitet immer richtig.
Vervollständigen wir unser Programm nach dieser Methode:
70 REM WELCHE TAGESNUMMER ?
80 IF M>2 THEN N=INT(30.6*(M+1))-63+T:GOTO 100
90 N=INT(30.6*(M+13))-428+T
100 PRINT"NUMMER"N
110 END
In Zeile 80 erfolgt eine Fallunterscheidung. Ist der eingegebene Monat März oder höher, kann die angegebene Formel verwendet werden. Wir ziehen vom Ergebnis noch 63, die Nummer des ersten März ab. Ebenso muß in Zeile 90 bei der Bearbeitung der Monate Januar und Februar das Ergebnis kosmetisch nachbehandelt werden.
Diese Routine funktioniert jetzt aber nur für ein Jahr (und produziert unter Umständen bei Schaltjahren falsche Werte). In der Praxis wird man diese Routine wohl kaum benötigen. Das folgende Problem tritt dagegen weitaus häufiger auf. Beispielsweise wird die Frage gestellt, wie viele Tage zwischen zwei bestimmten Daten liegen. Kennen Sie Ihr genaues Alter in Tagen? Man geht hierbei so vor, daß jedes Datum in eine absolute Zahl umgewandelt wird. Dazu nimmt man die Anzahl der Tage, die seit einem bestimmten fest definierten Ereignis vergangen sind, etwa seit Christi Geburt, seit dem Geburtstag der Freundin, seit der Vereinigung Deutschlands oder was auch immer. Aus programmtechnischen und astronomischen Gründen wählt man in der Praxis dabei das Datum 1.01.4713 vor unserer Zeitrechnung, ein Datum, das weit vor allen geschichtlich überlieferten Ereignissen liefert. Man nennt dieses Datum auch »julianisches Datum«. Wir müssen jetzt also das in den Variablen T, M und J übergebene Datum in das julianische Datum, eine einfache Zahl, umwandeln. Folgende Unterroutine übernimmt das freundlicherweise für uns:
1000 REM KALENDER -> JULIANISCH
1010 IF M>2 THEN M=M-3:GOTO 1030
1020 M=M+9:J=J-1
1030 JH=INT(J/100):JA=J-100*JH
1040 JD=INT(146097*JH/4)+INT(1461*JA/4)
1050 JD=JD+INT((153*M+2)/5)+T
1060 RETURN
Die genaue Funktionsweise, insbesondere die Bedeutung der Konstanten 146097, 1461 und 153 kann hier aus Platzgründen leider nicht beschrieben werden, da umfangreiche astronomische Überlegungen dahinterstecken. Kluge Menschen haben sich schon den Kopf darüber zerbrochen, uns soll daher die fertige Lösung genügen. Diese berücksichtigt natürlich auch Schaltjahre und Jahreszahlen. In Zeile 1010 prüfen wir, ob der Monat März oder höher war. Wenn ja, rechnen wir ihn in unseren »EDV-Jahr-Monat« um. In Zeile 1020 erfolgt die Rechnung für die Monate Januar oder Februar. Wir addieren 9 zum Monat und erniedrigen dafür das Jahr um eins. In Zeile 1030 berechnen wir das Jahrhundert (JH) und Kalenderjahr ohne Jahrhundert (JA), dies läßt sich mit der Zerlegung in High- und Lowbyte bei der Binärrechnung vergleichen und wird im folgenden zur Berücksichtigung der Schaltjahr (Zeile 1040) benötigt. In 1050 addieren wir dann noch die nach bekannter Methode ermittelte Tagesnummer im Jahr zu JD. JD enthält dann das julianische Datum zu T, M und J.
Um die Funktionsweise zu testen, löschen Sie bitte die Zeilen 70 bis 110 des vorherigen Musterprogramms und geben folgendes ein:
70 GOSUB 1000
80 PRINT "JULIANISCH: "JD
90 END
Auch der umgekehrte Weg läßt sich gehen. Wir wandeln das in JD übergebene julianische Datum in das Kalenderdatum um und übergeben dieses in den Basic-Variablen T, M und J. Die Routine dazu soll ab Zeile 2000 beginnen und sieht so aus:
2000 REM JULIANISCH -> KALENDER
2005 J=INT((4*JD-1)/146097)
2010 JD=4*JD-1-146097*J
2020 T=INT(JD/4)
2030 JD=INT((4*T+3)/1461)
2040 T=4*T+3-1461*JD
2050 T=INT((T+4)/4)
2060 M=INT((5*T-3)/153)
2070 T=5*T-3-153*M
2080 T=INT((T+5)/5)
2090 J=100*J+JD
2100 IFM<10THENM=INT(M+3):GOTO2120
2110 M=INT(M-9):J=J+1
2120 RETURN
Man sieht also, daß der umgekehrte Weg viel umständlicher ist komplizierter ist. Wir werden aber später sehen, daß er durchaus nützlich sein kann, etwa wenn es um Fristen geht. Mit folgendem Programm probieren wir die Funktionsweise der Routine aus:
10 INPUT "JULIANISCHES DATUM ";JD
20 GOSUB 2000
30 PRINT "KALENDERDATUM:";T;".";M;".";J
40 END
Unsere Aufgabe war, die Differenz zwischen zwei Daten in Tagen zu errechnen. Dazu dient folgendes Programm, das auf die Routine ab 1000 zurückgreift:
10 INPUT"DATUM 1 FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5))
50 GOSUB 1000
60 J1=JD
70 INPUT"DATUM 2 FORMAT TTMMJJJJ ";D$
80 T=VAL(MID$(D$,1,2))
90 M=VAL(MID$(D$,3,2))
100 J=VAL(MID$(D$,5))
110 GOSUB 1000
120 R=JD-J1
130 PRINT "DAZWISCHEN LIEGEN"R"TAGE."
140 END
In Zeilen 10 bis 40 lassen wir das erste Datum eingeben und werten es aus. In Zeile 50 wird die Berechnung des julianischen Datums aufgerufen. Das Unterprogramm gibt den Wert in JD zurück, diese Variable müssen wir zur weiteren Berechnung in J1 zwischenspeichern (Zeile 60). Danach werten wir auf gleiche Weise in 70 bis 110 das zweite Datum aus und berechnen in Zeile 120 die Differenz. Geben Sie in das Programm zum Spaß einmal als erstes Ihr Geburtsdatum ein, und als zweites das heutige Datum. Sie wissen dann Ihr Lebensalter - in Tagen! Diese Zahl könnte nun beispielsweise mit SIN-Funktionen für einen Biorhythmus aufbereitet werden, doch das soll nicht Inhalt dieses Aufsatzes sein.
Wir können nun die Differenz zwischen zwei Daten (Mehrzahl von Datum) berechnen (lassen). Ein anderes Programm muß mit Tagesfristen operieren können. Wann muß der Antrag, der Einspruch, für den man bekanntlich 30 Tage nach Zugang Zeit hat, beim Finanzamt eingereicht sein? An welchem Tag sollte eine Rechnung mit Zahlungsziel »20 Tage nach Rechnungsdatum« spätestens bezahlt werden? Das folgende Programm rechnet es für Sie aus.
10 INPUT"DATUM FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5))
50 GOSUB 1000
60 INPUT"FRIST IN TAGEN ";R
70 JD=JD+R
80 GOSUB 2000
90 PRINT "LAEUFT AM";T;".";M;".";J;". AB!"
100 END
Es findet wieder die bekannte Routine zur Eingabe und Zerlegung des Tagesdatums Verwendung. In Zeile 50 wird das julianische Datum dazu berechnet. Wir geben daraufhin die Frist in Tagen ein. Hier sind auch negative Werte und die Null zugelassen! Die Eingabe addieren wir zum julianischen Datum, das ergibt ein neues julianisches Datum. Zeile 80 prüft nun, welches Kalenderdatum diesem entspricht und gibt es in Zeile 90 aus. Probieren Sie einmal aus, ob alles funktioniert.
Jetzt fehlt nur noch ein wichtiger Punkt. Können Sie herausfinden, aus welchen Wochentag ein bestimmtes Datum fällt? Wissen Sie beispielsweise, an welchem Tag Sie geboren wurden? (Sie meinen, das wäre egal? Haben Sie schon einmal den Ausdruck »Sonntagskind« gehört? Fragen Sie einmal einen Astrologen!) Aber ernsthaft: Viele Planungs- und Verwaltungsprogramme könnten wirksam Schaden vermeiden, wenn sie selbständig feststellen, ob Tage auf Samstag, Sonntag oder Feiertag fallen (natürlich nur, wenn ein Verzeichnis aller Feiertage eingebaut ist). Ein Beispiel wäre eine Urlaubsverwaltung.
Die Folge der sieben Tage wiederholt sich ja regelmäßig. Sie brauchen also nur die Anzahl der Tage zu bestimmen, die seit einem bestimmten Datum, etwa dem 1.01.1980 vergangen sind. Wie das geht, haben wir ja schon gelernt. Das Ergebnis wird durch sieben geteilt. Der Rest ist die Nummer des Wochentages. Wir müssen noch wissen, daß der 1.1.80 ein Dienstag war, zu der Formel sollte also noch zwei addiert werden, sonst kommt das Programm auf Dienstag.
10 INPUT"DATUM FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5))
50 GOSUB 1000
60 UNT=JD-723121+2
70 TAG=UNT-INT(UNT/7)*7
80 TAG$(0)="SONNTAG"
90 TAG$(1)="MONTAG"
100 TAG$(2)="DIENSTAG"
110 TAG$(3)="MITTWOCH"
120 TAG$(4)="DONNERSTAG"
130 TAG$(5)="FREITAG"
140 TAG$(6)="SAMSTAG"
150 PRINT"WOCHENTAG: "TAG$(TAG)
160 END
Bis zu Zeile 60 enthält das Programm nichts neues. In Zeile 60 berechnen wir, wie viele Tage seit dem 1.1.80 (dessen julianisches Datum 723121 ist) vergangen sind. Dazu wird 2 addiert, um die Skala zu normieren. In Zeile 70 findet sich ein kleiner »Kniff«. Da es im Commodore-Basic keine Funktion zur Berechnung des Restes einer Division gibt, behelfen wir uns so:
A MOD B = A - INT (A/B) * B
A soll durch B geteilt und der Rest ermittelt werden. B ist hier konstant 7 (Anzahl Wochentage), A ist UNT, die Differenz der Tage. Mit Hilfe des in Zeilen 80 bis 140 definierten String-Arrays geben wir in Zeile 150 dann den Wochentag aus. Man könnte das Ganze noch etwas eleganter mit einer FOR..NEXT und READ..DATA-Struktur gestalten, die die Wochentagsnamen einliest, aber hier geht es nur ums Prinzip. Und nun sollten Sie unbedingt einmal Ihr Geburtsdatum eingeben und den Wochentag feststellen lassen.
Die Formeln, die Ihnen hier vorgestellt wurden, sind auf den ersten Blick sicher sehr schwer verständlich. Es ist aber gar nicht notwendig, daß Sie wissen, wie sie funktionieren. Bauen Sie einfach je nach Bedarf die Unterprogramme ab 1000 und/oder 2000 in Ihr Programm ein, und arbeiten Sie damit.
(Nikolaus M. Heusler)
Listing 1. Primitive Berechnung der Tagesnummer (von 1 bis 365)
5 REM UNGEEIGNET FUER SCHALTJAHRE
10 INPUT"DATUM FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5)):REM OHNE BEDEUTUNG
50 IF M<1 OR M>12 THEN RUN
60 IF T<1 OR T>31 THEN RUN
70 REM WELCHE TAGESNUMMER ?
80 IF M>2 THEN N=INT(30.6*(M+1))-63+T:GOTO 100
90 N=INT(30.6*(M+13))-428+T
100 PRINT"NUMMER"N
110 END
Listing 2. Bestimmt die julianische Tagesnummer
10 INPUT"DATUM FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5))
50 IF M<1 OR M>12 THEN RUN
60 IF T<1 OR T>31 THEN RUN
70 GOSUB1000
80 PRINT "JULIANISCH: "JD
90 END
1000 REM KALENDER -> JULIANISCH
1010 IF M>2 THEN M=M-3:GOTO 1030
1020 M=M+9:J=J-1
1030 JH=INT(J/100):JA=J-100*JH
1040 JD=INT(146097*JH/4)+INT(1461*JA/4)
1050 JD=JD+INT((153*M+2)/5)+T
1060 RETURN
Listing 3. Umrechnung julianisch -> Datum
10 INPUT "JULIANISCHES DATUM ";JD
20 GOSUB 2000
30 PRINT "KALENDERDATUM:";T;".";M;".";J
40 END
2000 REM JULIANISCH -> KALENDER
2005 J=INT((4*JD-1)/146097)
2010 JD=4*JD-1-146097*J
2020 T=INT(JD/4)
2030 JD=INT((4*T+3)/1461)
2040 T=4*T+3-1461*JD
2050 T=INT((T+4)/4)
2060 M=INT((5*T-3)/153)
2070 T=5*T-3-153*M
2080 T=INT((T+5)/5)
2090 J=100*J+JD
2100 IFM<10THENM=INT(M+3):GOTO2120
2110 M=INT(M-9):J=J+1
2120 RETURN
Listing 4. Wieviele Tage liegen zwischen zwei Daten?
10 INPUT"DATUM 1 FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5))
50 GOSUB 1000
60 J1=JD
70 INPUT"DATUM 2 FORMAT TTMMJJJJ ";D$
80 T=VAL(MID$(D$,1,2))
90 M=VAL(MID$(D$,3,2))
100 J=VAL(MID$(D$,5))
110 GOSUB 1000
120 R=JD-J1
130 PRINT "DAZWISCHEN LIEGEN"R"TAGE."
140 END
1000 REM KALENDER -> JULIANISCH
1010 IF M>2 THEN M=M-3:GOTO 1030
1020 M=M+9:J=J-1
1030 JH=INT(J/100):JA=J-100*JH
1040 JD=INT(146097*JH/4)+INT(1461*JA/4)
1050 JD=JD+INT((153*M+2)/5)+T
1060 RETURN
Listing 5. Ermittelt das Datum, das von einem gegebenen Datum eine bestimmte Anzahl an Tagen entfernt liegt
10 INPUT"DATUM FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5))
50 GOSUB 1000
60 INPUT"FRIST IN TAGEN ";R
70 JD=JD+R
80 GOSUB 2000
90 PRINT "LAEUFT AM";T;".";M;".";J;". AB!"
100 END
1000 REM KALENDER -> JULIANISCH
1010 IF M>2 THEN M=M-3:GOTO 1030
1020 M=M+9:J=J-1
1030 JH=INT(J/100):JA=J-100*JH
1040 JD=INT(146097*JH/4)+INT(1461*JA/4)
1050 JD=JD+INT((153*M+2)/5)+T
1060 RETURN
2000 REM JULIANISCH -> KALENDER
2005 J=INT((4*JD-1)/146097)
2010 JD=4*JD-1-146097*J
2020 T=INT(JD/4)
2030 JD=INT((4*T+3)/1461)
2040 T=4*T+3-1461*JD
2050 T=INT((T+4)/4)
2060 M=INT((5*T-3)/153)
2070 T=5*T-3-153*M
2080 T=INT((T+5)/5)
2090 J=100*J+JD
2100 IFM<10THENM=INT(M+3):GOTO2120
2110 M=INT(M-9):J=J+1
2120 RETURN
Listing 6. Errechnet den Wochentag zu einem Datum
10 INPUT"DATUM FORMAT TTMMJJJJ ";D$
20 T=VAL(MID$(D$,1,2))
30 M=VAL(MID$(D$,3,2))
40 J=VAL(MID$(D$,5))
50 GOSUB 1000
60 UNT=JD-723121+2
70 TAG=UNT-INT(UNT/7)*7
80 TAG$(0)="SONNTAG"
90 TAG$(1)="MONTAG"
100 TAG$(2)="DIENSTAG"
110 TAG$(3)="MITTWOCH"
120 TAG$(4)="DONNERSTAG"
130 TAG$(5)="FREITAG"
140 TAG$(6)="SAMSTAG"
150 PRINT"WOCHENTAG: "TAG$(TAG)
160 END
1000 REM KALENDER -> JULIANISCH
1010 IF M>2 THEN M=M-3:GOTO 1030
1020 M=M+9:J=J-1
1030 JH=INT(J/100):JA=J-100*JH
1040 JD=INT(146097*JH/4)+INT(1461*JA/4)
1050 JD=JD+INT((153*M+2)/5)+T
1060 RETURN