lernen:datum
Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
— | lernen:datum [2020-07-27 09:21] (aktuell) – angelegt - Externe Bearbeitung 127.0.0.1 | ||
---|---|---|---|
Zeile 1: | Zeile 1: | ||
+ | ====== Wie alt bist du (in Tagen) ? ====== | ||
+ | > Selbst das längste Leben lässt sich in Sekunden ausdrücken. | ||
+ | >> | ||
+ | |||
+ | Idee aus: [Hartmut Ring: Texteditoren in C. McGraw-Hill (1989), S. 10-17.] | ||
+ | |||
+ | ===== Aufgabe ===== | ||
+ | Wie alt bist du? | ||
+ | In Jahren weiß das jeder sofort. Aber in Tagen gemessen? | ||
+ | Es sind die einfachen Fragen, die schwierig zu beantworten sind. | ||
+ | |||
+ | Schreibe ein Programm in C++, welches mit dem Aufruf | ||
+ | |||
+ | tageseit 1967-03-22 | ||
+ | |||
+ | (Datum gemäß EN) die entsprechende Zahl auf den Bildschirm schreibt. | ||
+ | Es spricht nichts dagegen, | ||
+ | den gefundenen Algorithmus für ein abgewandeltes Programm einzusetzen: | ||
+ | |||
+ | tagebis 2002-12-24 | ||
+ | |||
+ | Für den Anfang genügt auch ein Programm, | ||
+ | bei dem beide Daten einzugeben sind: | ||
+ | |||
+ | tageseit | ||
+ | Anfangsdatum (Jahr Monat Tag): 1967 3 22 | ||
+ | Enddatum | ||
+ | Tage = ???? | ||
+ | |||
+ | Anstelle der Fragezeichen sollte die Anzahl der Tage | ||
+ | vom Anfangs- bis zum Enddatum stehen. | ||
+ | Zur Vereinfachung kann vorerst angenommen werden, | ||
+ | dass der Nutzer sich nicht vertippt | ||
+ | und die Daten in der richtigen Folge eingegeben werden. | ||
+ | |||
+ | ===== Lösungsansatz ===== | ||
+ | ==== 1. Fassung ==== | ||
+ | < | ||
+ | A Lebenstage im Geburtsjahr bestimmen | ||
+ | B Tage der Zwischenjahre hinzurechnen | ||
+ | C Tage im aktuellen Jahr (bis heute) hinzuzählen | ||
+ | |||
+ | Beispiel (22.03.1967 bis 24.12.2002): | ||
+ | |||
+ | A) verbleibende Tage im März 31-22 = 9 | ||
+ | Tage im April ........... +30 = 39 | ||
+ | Tage im Mai ............. +31 = 70 | ||
+ | Tage im Juni ............ +30 = 100 | ||
+ | Tage im Juli ............ +31 = 131 | ||
+ | Tage im August .......... +31 = 162 | ||
+ | Tage im September ....... +30 = 192 | ||
+ | Tage im Oktober ......... +31 = 223 | ||
+ | Tage im November ........ +30 = 253 | ||
+ | Tage im Dezember ........ +31 = 284 | ||
+ | |||
+ | Wäre das Startjahr Schaltjahr und der Startmonat Januar oder Februar, | ||
+ | wäre noch der Schalttag zu addieren. | ||
+ | |||
+ | B) für alle Zwischenjahre 1968 bis 2001 die Jahreslängen hinzuzählen, | ||
+ | | ||
+ | 33 Jahre je 365 Tage ... +10945= 11229 | ||
+ | bei Schaltjahr je ein Tag +9 = 11238 (1968, | ||
+ | |||
+ | C) für alle vollen Monate im aktuellen Jahr | ||
+ | Tage im Januar .......... +31 = 11308 | ||
+ | Tage im Februar ......... +30 = 11338 | ||
+ | Tage im März ............ +31 = 11369 | ||
+ | Tage im April ........... +30 = 11399 | ||
+ | Tage im Mai ............. +31 = 11430 | ||
+ | Tage im Juni ............ +30 = 11460 | ||
+ | Tage im Juli ............ +31 = 11491 | ||
+ | Tage im August .......... +31 = 11522 | ||
+ | Tage im September ....... +30 = 11552 | ||
+ | Tage im Oktober ......... +31 = 11583 | ||
+ | Tage im November ........ +30 = 11613 | ||
+ | |||
+ | Wäre das aktuelle Jahr Schaltjahr und aktueller Monat mindestens März, | ||
+ | wäre noch der Schalttag zu addieren. | ||
+ | |||
+ | Tage im aktuellen Monat.. +24 = 11637 | ||
+ | ===== | ||
+ | </ | ||
+ | Die ganze Rechnung klappt nicht, wenn Anfangs- und Endjahr das gleiche sind. | ||
+ | Dann wäre genau die Länge des Jahres abzuziehen: | ||
+ | |||
+ | < | ||
+ | D Falls Anfangs- gleich Endjahr: | ||
+ | - (365+evtl. Schalttag) | ||
+ | </ | ||
+ | Diese erste Version ließe sich wie folgt implementieren (als Funktion). | ||
+ | Würden Programmierer nach Quelltextzeilen bezahlt, gäbe das ein hübsches Sümmchen. | ||
+ | (Vielleicht würden wir noch ein bisschen mehr hinzuzaubern ;-) | ||
+ | |||
+ | <code cpp> | ||
+ | int anzahltage(int startjahr, int startmonat, int starttag, | ||
+ | int endjahr, | ||
+ | { | ||
+ | int tage = -starttag; | ||
+ | if (startmonat==1) | ||
+ | if (startmonat< | ||
+ | { | ||
+ | tage += 28; | ||
+ | if (startjahr%4==0) | ||
+ | if (startjahr%100==0) tage--; // doch keins: Gregorianische Regel | ||
+ | if (startjahr%400==0) tage++; // trotzdem eins: Gregorianische Ausnahme | ||
+ | } | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | if (startmonat< | ||
+ | |||
+ | for (int jahr=startjahr+1; | ||
+ | { | ||
+ | tage += 365; | ||
+ | if (jahr%4==0) | ||
+ | if (jahr%100==0) tage--; | ||
+ | if (jahr%400==0) tage++; | ||
+ | } | ||
+ | |||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | { | ||
+ | tage += 28); | ||
+ | if (endjahr%4==0) | ||
+ | if (endjahr%100==0) tage--; | ||
+ | if (endjahr%400==0) tage++; | ||
+ | } | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | |||
+ | tage += endtag; | ||
+ | |||
+ | // Teil D: Korrektur | ||
+ | |||
+ | if (startjahr == endjahr) | ||
+ | { | ||
+ | tage -= 365; | ||
+ | if (startjahr%4==0) | ||
+ | if (startjahr%100==0) tage--; | ||
+ | if (startjahr%400==0) tage++; | ||
+ | } | ||
+ | return tage; // endlich! richtig?? | ||
+ | } | ||
+ | </ | ||
+ | Für C-Fremdlinge seien einige komische Zeichen erklärt: | ||
+ | '' | ||
+ | '' | ||
+ | '' | ||
+ | '' | ||
+ | |||
+ | Die Berechnung mag zwar richtig sein, aber beim Lesen befallen einen doch Zweifel. | ||
+ | Aufwändig (frühere Schreibung: aufwendig) ist die Lösung. | ||
+ | (Man könnte die Wand hoch gehen, wie langsam die Kiste ist.) | ||
+ | Und besonders elegant ist sie auch nicht, höchstens als Kunstschmiedearbeit. | ||
+ | Programmieren ist schließlich (auch) eine Kunst. | ||
+ | Die Kunst beim Programmieren besteht jedoch im Streben nach Einfachheit und Wiederverwendbarkeit. | ||
+ | |||
+ | ==== 2. Fassung ==== | ||
+ | Als erstes sticht die viermalige Verwendung der Schaltjahr-Regel hervor. | ||
+ | Wiederverwendung wurde zwar auch betrieben, | ||
+ | aber nur durch Kopieren, Einfügen und Ändern von Quelltext. | ||
+ | Kein Grund, stolz zu sein. Dieser Schaltjahr-" | ||
+ | |||
+ | <code cpp> | ||
+ | bool schaltjahr(int jahr) | ||
+ | { | ||
+ | if (jahr%400 == 0) return 1; // besondere Schaltjahre: | ||
+ | if (jahr%100 == 0) return 0; // andere Jahrhunderte nicht | ||
+ | if (jahr%4 | ||
+ | /* else */ | ||
+ | } | ||
+ | </ | ||
+ | oder unter Ausnutzung logischer Verknüpfungen UND (&& | ||
+ | |||
+ | <code cpp> | ||
+ | //: tageseit.cpp : Berechnung der Tagesdifferenz - R.Richter 2002-10-06 | ||
+ | /////////////////////////////////////////////////////////////////////// | ||
+ | |||
+ | bool schaltjahr(int jahr) | ||
+ | { | ||
+ | return (jahr%4==0 && jahr%100!=0) || jahr%400==0; | ||
+ | } | ||
+ | //~ | ||
+ | </ | ||
+ | Ganze 20 Zeilen lassen sich einsparen, z.B. im Teil B stünde dann nur | ||
+ | <code cpp> | ||
+ | tage += 365 + schaltjahr(jahr); | ||
+ | </ | ||
+ | Schaut man etwas genauer hin, | ||
+ | lassen sich der Teil A und der (unschöne) Teil D zusammenfassen. | ||
+ | Das Vertauschen der Reihenfolge ist möglich, da nur Additionen der Tage stattfinden. | ||
+ | Eigentlich wird da weiter nichts betrieben, | ||
+ | als auf den Anfang des Geburtsjahres zurückzurechnen | ||
+ | oder vom Anfang des Geburtsjahres bis zum Geburtstag zu zählen. | ||
+ | Dies geschieht im wesentlichen mit dem gleichen Quelltext wie im Teil C. | ||
+ | Diese Zahl ist als negativer Anfangswert zu nehmen (Teil A' | ||
+ | Danach kann von Beginn des Geburtsjahres in Jahresschritten vorwärtsgegangen werden (Teil B'). | ||
+ | Teil C bleibt, wie er war. | ||
+ | |||
+ | <code cpp> | ||
+ | int anzahltage(int startjahr, int startmonat, int starttag, | ||
+ | int endjahr, | ||
+ | { | ||
+ | int tage = 0; // Teil A': zurueck zum Anfang des Startjahres | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | if (startmonat> | ||
+ | tage -= starttag; | ||
+ | |||
+ | for (int jahr=startjahr; | ||
+ | { | ||
+ | tage += 365 + schaltjahr(jahr); | ||
+ | } | ||
+ | |||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | if (endmonat> | ||
+ | tage += endtag; | ||
+ | |||
+ | return tage; | ||
+ | } | ||
+ | </ | ||
+ | Wir sind zwar noch nicht schneller, aber der Formulierung ist übersichtlicher geworden. | ||
+ | |||
+ | > Wenn zwei Wege zum Ergebnis führen, ist wahrscheinlich der einfachere auch der richtige. | ||
+ | |||
+ | sagt Roger Penrose. Vermutlich geht es noch einfacher. | ||
+ | |||
+ | ==== 3. Fassung ==== | ||
+ | Die fortlaufenden Tests kosten einen Haufen Rechenzeit. | ||
+ | Hier wäre ein direkter Sprungbefehl wünschenswert. | ||
+ | Aber wir sollen goto ja nicht verwenden. | ||
+ | Der am 07.08.2002 verstorbene Edsger Wybe Dijkstra hätte sonst keine gesegnete Ruhe: | ||
+ | |||
+ | > Wenn Sie unsauber programmieren und dabei denken, Dijkstra hätte das nicht gemocht, | ||
+ | > so ist dies genug Unsterblichkeit für mich. | ||
+ | |||
+ | Nun, es geht ohne goto. Es gibt noch genügend andere " | ||
+ | Lernen wir etwas C-Syntax hinzu: | ||
+ | |||
+ | <code cpp> | ||
+ | switch(endmonat) | ||
+ | { | ||
+ | case 12: tage += 30; // immer die Vormonate drauf rechnen | ||
+ | case 11: tage += 31; | ||
+ | case 10: tage += 30; | ||
+ | case 9: tage += 31; | ||
+ | case 8: tage += 31; | ||
+ | case 7: tage += 30; | ||
+ | case 6: tage += 31; | ||
+ | case 5: tage += 30; | ||
+ | case 4: tage += 31; | ||
+ | case 3: tage += 28 + schaltjahr(endjahr); | ||
+ | case 2: tage += 31; | ||
+ | | ||
+ | case 1: break; | ||
+ | }; | ||
+ | </ | ||
+ | Die Mehrfachverzweigung springt sofort an die richtige Stelle | ||
+ | und wurstelt sich bis zum nächsten Abbruch (break) durch. | ||
+ | Auch eine Art goto, ohne es so zu nennen. | ||
+ | Java-Fans brüsten sich damit, dass in Java goto nicht erlaubt ist, | ||
+ | dafür kann break ein Sprungziel angeben. Wie originell: goto ohne goto! | ||
+ | |||
+ | Die fortlaufende Addition macht auch noch zu viel (unnütze) Arbeit. | ||
+ | Wir können das ändern, in dem wir dem Compiler diese Arbeit abnehmen. | ||
+ | (Natürlich erfordert das geistige Anstrengung. Nichts ist kostenlos!) | ||
+ | |||
+ | <code cpp> | ||
+ | switch(endmonat) | ||
+ | { case 1: break; | ||
+ | case 2: tage += 31; break; | ||
+ | case 3: tage += 59; break; | ||
+ | case 4: tage += 90; break; | ||
+ | case 5: tage += 120; break; | ||
+ | case 6: tage += 151; break; | ||
+ | case 7: tage += 181; break; | ||
+ | case 8: tage += 212; break; | ||
+ | case 9: tage += 243; break; | ||
+ | case 10: tage += 273; break; | ||
+ | case 11: tage += 304; break; | ||
+ | case 12: tage += 334; break; | ||
+ | }; | ||
+ | if (endmonat > 2) tage=+ schaltjahr(endjahr); | ||
+ | |||
+ | </ | ||
+ | Jetzt wird nur noch eine Addition ausgeführt. Gut, nicht? Es geht noch besser. | ||
+ | |||
+ | Solche Mehrfachverzweigungen, | ||
+ | sind wiederum Ungetüme, die durch ein anderes Konzept aus dem Rennen geschlagen werden, | ||
+ | durch eine Tabelle oder Liste (Feld oder indizierte Variable): | ||
+ | |||
+ | <code cpp> | ||
+ | int anzahltage(int startjahr, int startmonat, int starttag, | ||
+ | int endjahr, | ||
+ | { | ||
+ | int const monatstage[] = | ||
+ | { | ||
+ | 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 | ||
+ | //[0] Jan Feb Mar Apr Mai Jun Jul Aug Sep Okt Nov Dez | ||
+ | }; | ||
+ | |||
+ | int tage = 0; | ||
+ | |||
+ | tage += monatstage[startmonat] + starttag; | ||
+ | if (startmonat > 2) tage += schaltjahr(startjahr); | ||
+ | |||
+ | tage = -tage; // Tage bis einschliesslich Starttag werden abgezogen | ||
+ | |||
+ | for (int jahr=startjahr; | ||
+ | { | ||
+ | tage += 365 + schaltjahr(jahr); | ||
+ | } | ||
+ | |||
+ | tage += monatstage[endmonat] + endtag; | ||
+ | if (endmonat > 2) tage+= schaltjahr(endjahr); | ||
+ | |||
+ | return tage; | ||
+ | } | ||
+ | </ | ||
+ | Die Anfangswertbelegung mit 0 und die Umkehr der Tageszahl ist weitschweifig, | ||
+ | verdeutlicht aber die inhaltliche Gleichheit der Teile A' und C' | ||
+ | Ein Bild sagt mehr als Tausend Worte. | ||
+ | (Frei übersetzt ins Computer-Englisch: | ||
+ | Machen wir uns ein Bild von dem, was geschieht: | ||
+ | |||
+ | Anfang | ||
+ | Startjahr | ||
+ | |-------------------Start | ||
+ | |------------------------------|------------Ende | ||
+ | B' | ||
+ | Ende | ||
+ | |||
+ | Das soll uns auf die nächste Idee bringen, | ||
+ | mit der die zeitraubende Schleife (Teil B') aufgelöst wird. | ||
+ | Lässt sich der Abstand von Start bis Ende ohne die Schleife bestimmen? | ||
+ | Natürlich geht das. | ||
+ | Wir müssen nur eine einheitliche Skalenbeschriftung anbringen, | ||
+ | an der wir den Wert ablesen. | ||
+ | Dann können wir einfach die Differenz der Skalenwerte als Abstand nehmen! | ||
+ | |||
+ | ==== 4. Fassung ==== | ||
+ | Die neue Lösung basiert auf dem Zusammenhang | ||
+ | |||
+ | Anzahl der Tage = TagesNummer(Enddatum) - TagesNummer(Startdatum) | ||
+ | |||
+ | Für die TagesNummer-Umrechnung können wir die Teil B' und C' zusammenfassen. | ||
+ | Dabei fällt allerdings der Anfang des Startjahres als Bezugspunkt weg. | ||
+ | Was nehmen wir stattdessen? | ||
+ | |||
+ | Das Nächstliegende ist, | ||
+ | einfach alle Jahre seit Anfang der (unseren) Zeitrechung zu nehmen. | ||
+ | Es ist (für unsere Zwecke) auch gar nicht so wichtig, | ||
+ | ob es ein Jahr Null gab oder nicht. | ||
+ | |||
+ | (Es gab keins. Auch vorher haben schon Menschen gelebt, | ||
+ | vermutlich, ohne das Gefühl gehabt zu haben, vorzeitliche Wesen zu sein. | ||
+ | Man fühlt sich eher am Ende einer Epoche: "fin de siècle" | ||
+ | Vorher rechnete man "ad urbe condita", | ||
+ | ab dem sagenhaften Gründungsdatum der " | ||
+ | durch die Wolfskinder Romulus und Remus. | ||
+ | Tatsächlich ist Rom wohl um 575 v.u.Z. unter etruskischer Herrschaft entstanden. | ||
+ | |||
+ | Noch eher, ab 776 v.u.Z., rechneten die Griechen in Olympiaden. | ||
+ | Israeliten setzten die Erschaffung der Welt als Anfangspunkt fest, | ||
+ | nach ihren Vorstellungen 3761 v.u.Z.; die Ägypter 4713 v.u.Z. | ||
+ | Für Mohammedaner beginnt die Zeitrechnung im Jahr 622 u.Z. mit der Hedschra, | ||
+ | der Flucht Mohammeds. | ||
+ | Buddhisten rechnen immer in Jahreszyklen, | ||
+ | Tiger, Drache usw. | ||
+ | Japaner rechnen in Regierungsjahren ihres jeweiligen Kaisers, | ||
+ | z.Z. Akihito, eine Methode, die lange Regierungszeiten voraussetzt, | ||
+ | um praktisch zu sein. | ||
+ | |||
+ | Unsere Zeitrechnung (nach Christi Geburt) wurde auch nicht im Jahre 1 eingeführt, | ||
+ | sondern erst im Jahre 532 u.Z. durch den Mönch Dionysius Exiguus | ||
+ | [Schlag nach Natur, S.88]. | ||
+ | Christi Geburt zu Weihnachten kurz vor Beginn des Jahres 1 u.Z. | ||
+ | ist so genau nicht überliefert. | ||
+ | Es gibt Theorien, dass dies um mehrere Jahre danebenliegt. | ||
+ | So könnte der Stern von Bethlehem eine Konjunktion, | ||
+ | d.h. ein Vorbeigang, von Jupiter und Saturn im Jahre 7 v.u.Z. gewesen sein, | ||
+ | der Astronomen aus der Fremde, uns als die drei Könige dem Morgenland bekannt, | ||
+ | in Bewegung brachte zum besten Beobachtungsplatz. | ||
+ | Es gibt Menschen, die die Gestalt Jesus für eine Erfindung halten. | ||
+ | Und es gibt Menschen, die ungefähr 3 Jahrhunderte | ||
+ | für eine Erfindung spät-mittelalterlicher Urkundenfälscher halten | ||
+ | [Illig: Das erfundene Mittelalter]. Klammer zu.) | ||
+ | |||
+ | Rechnen wir also so, als ob es ein Jahr Null gegeben hätte, | ||
+ | in dem Maria schwanger war. | ||
+ | |||
+ | <code cpp> | ||
+ | int tagesNr(int jahr, int monat, int tag) | ||
+ | { | ||
+ | int const monatstage[] = | ||
+ | { | ||
+ | 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 | ||
+ | //[0] Jan Feb Mar Apr Mai Jun Jul Aug Sep Okt Nov Dez | ||
+ | }; | ||
+ | |||
+ | int tage = 365*jahr | ||
+ | + jahr/ | ||
+ | - jahr/ | ||
+ | + jahr/ | ||
+ | + monatstage[monat] // Vormonate | ||
+ | + tag; | ||
+ | |||
+ | if (monat< | ||
+ | |||
+ | return tage; | ||
+ | } | ||
+ | |||
+ | int anzahltage(int startjahr, int startmonat, int starttag, | ||
+ | int endjahr, | ||
+ | { | ||
+ | return tagesNr(endjahr, | ||
+ | -tagesNr(startjahr, | ||
+ | } | ||
+ | </ | ||
+ | Für weit zurückliegende Datumsangaben ist die | ||
+ | Anwendung der Differenzberechnung mit Vorsicht zu genießen. | ||
+ | In den katholischen Ländern verordnete Papst Gregor XIII., | ||
+ | dass der 15. Oktober unmittelbar auf den 4. Oktober 1582 folgte, | ||
+ | um die Fehler des Julianischen Kalenders auszugleichen, | ||
+ | und künftig die Hundertjahr- sowie die Vierhundertjahr-Schaltregel zu befolgen. | ||
+ | Das protestantische Deutschland vollzog diesen Schritt am 1. März 1700, | ||
+ | England im September 1752, Schweden 1753, Japan 1873, die Türkei 1916, | ||
+ | Russland 1918, Rumänien 1919, Griechenland erst 1923. | ||
+ | |||
+ | ==== 5. Fassung ==== | ||
+ | Die Festlegung, wann das Jahr beginnt, ist willkürlich. | ||
+ | Erst im Spätmittelalter setzte sich der Januar als Jahresanfang durch. | ||
+ | Die alten Römer begannen ihr Jahr mit dem Monat, | ||
+ | der nach dem Kriegsgott Mars benannt ist (Martius = März). | ||
+ | Mit der Öffnung des Mars-Tempels konnte wieder ein Feldzug beginnen, | ||
+ | um neue Provinzen zu unterwerfen. | ||
+ | Die Monatsnamen September (septem = der siebte) | ||
+ | bis Dezember (decem = der zehnte) künden noch von dieser Zählweise. | ||
+ | Der etwas kürzer geratene Februar beschloss das Jahr. | ||
+ | Der Schalttag befand sich also ursprünglich am Jahresende. | ||
+ | Er wurde auf Julius Caesars Vorschlag eingeführt | ||
+ | (Julianischer Kalender 46 v.u.Z.), | ||
+ | um den Fehler zwischen Kalenderjahr (365 Tage) | ||
+ | und Sonnenjahr (365,2422 Tage) zu mindern. | ||
+ | |||
+ | Lassen wir das Jahr ab März beginnen | ||
+ | und schlagen Januar und Februar dem Vorjahr zu, | ||
+ | können wir die Schaltjahrprozedur einsparen. | ||
+ | Allerdings müssen wir die Monatstabelle neu berechnen: | ||
+ | |||
+ | <code cpp> | ||
+ | int tagesNr(int jahr, int monat, int tag) | ||
+ | { | ||
+ | if (monat > 2) monat -= 3; // Maerz: | ||
+ | else | ||
+ | { | ||
+ | jahr--; monat += 9; | ||
+ | } | ||
+ | |||
+ | int const monatstage[] = | ||
+ | { | ||
+ | 0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337 | ||
+ | // Mar Apr Mai Jun Jul Aug Sep Okt Nov Dez Jan Feb | ||
+ | // 0 | ||
+ | }; | ||
+ | |||
+ | return 365*jahr | ||
+ | + jahr/4 - jahr/100 + jahr/ | ||
+ | + monatstage[monat] | ||
+ | + tag; | ||
+ | } | ||
+ | </ | ||
+ | ==== 6. Fassung ==== | ||
+ | Jetzt erst zahlt sich auch die Umstellung aus! | ||
+ | Die mit März beginnende Monatstabelle enthält eine Regelmäßigkeit. | ||
+ | Sie ist nicht ganz einfach zu entdecken, unter Zeitdruck schon gleich gar nicht. | ||
+ | Hier brauchen wir eine Erleuchtung. | ||
+ | Eine genaue (!) Zeichnung auf Millimeterpapier kann auf die Sprünge helfen: | ||
+ | Es ist (fast) eine Gerade. | ||
+ | Durch Experimentieren (Tabellenkalkulation!) findet man den linearen Ausdruck, | ||
+ | der die Tabelle überflüssig macht: | ||
+ | |||
+ | monatstage[monat] = (306*monat+5)/ | ||
+ | |||
+ | Die Fünf in der Klammer erzwingt die korrekte Rundung zur nächstliegenden Ganzzahl. | ||
+ | Und noch eine letzte Überlegung: | ||
+ | Vier Jahre haben 4*365+1 = 1461 Tage, von Jahrhunderten abgesehen. | ||
+ | |||
+ | <code cpp> | ||
+ | int tagesNr(int jahr, int monat, int tag) | ||
+ | { | ||
+ | if(monat> | ||
+ | else | ||
+ | { | ||
+ | jahr--; monat += 9; | ||
+ | } | ||
+ | return 1461*jahr/4 - jahr/100 + jahr/400 // ganze Jahre | ||
+ | + (306*monat+5)/ | ||
+ | + tag; | ||
+ | } | ||
+ | </ | ||
+ | Jetzt ist die Tagesnummerberechnung kompakt. | ||
+ | Die Ideen, die zu dieser Formel geführt haben, | ||
+ | sind am Quelltext nicht mehr nachvollziehbar. | ||
+ | Die " | ||
+ | Unter diesem Blickwinkel gesehen, wurde hier evtl. schon zu viel optimiert, | ||
+ | auf Kosten der Verständlichkeit. | ||
+ | |||
+ | ==== 7. Fassung ==== | ||
+ | Werden Datum-Angaben häufig benutzt, kann es lästig werden, | ||
+ | jeweils drei Parameter an die Funktionen übergeben zu müssen. | ||
+ | Aus diesem Grund lassen sich solche Dinge zu einem zusammengesetzten Typ, | ||
+ | Datensatz, Verbund oder Struktur genannt, bündeln. | ||
+ | |||
+ | <code cpp> | ||
+ | //> tageseit.cpp | ||
+ | |||
+ | struct Datum | ||
+ | { | ||
+ | int jahr, monat, tag; | ||
+ | }; | ||
+ | |||
+ | int tagesNr(Datum d) | ||
+ | { | ||
+ | if (d.monat > 2) d.monat -= 3; // Maerz: d.monat==0 | ||
+ | else | ||
+ | { | ||
+ | d.jahr--; d.monat += 9; | ||
+ | } | ||
+ | return 1461*d.jahr/ | ||
+ | + (306 * d.monat + 5)/10 // Vormonate | ||
+ | + d.tag; | ||
+ | } | ||
+ | |||
+ | int anzahltage(Datum start, Datum ende) | ||
+ | { | ||
+ | return tagesNr(ende) - tagesNr(start); | ||
+ | } | ||
+ | //~ | ||
+ | </ | ||
+ | ==== 8. Fassung ==== | ||
+ | Computerprogramme, | ||
+ | müssen damit rechnen, dass falsche, | ||
+ | sogar offensichtlich unsinnige Eingaben getätigt werden. | ||
+ | Einige davon lassen sich durch eine Gültigkeitsprüfung erkennen: | ||
+ | |||
+ | <code cpp> | ||
+ | //> tageseit.cpp | ||
+ | |||
+ | bool istgueltig(Datum d) | ||
+ | { | ||
+ | if(d.tag< | ||
+ | |||
+ | int const tage[] = | ||
+ | { | ||
+ | 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 | ||
+ | // Jan Feb Mar Apr Mai Jun Jul Aug Sep Okt Nov Dez | ||
+ | }; | ||
+ | return d.tag <= tage[d.monat] + (d.monat==2 && schaltjahr(d.jahr)); | ||
+ | } | ||
+ | //~ | ||
+ | </ | ||
+ | Da brauchen wir auch die Schaltjahrbestimmung wieder. Nichts war umsonst! | ||
+ | |||
+ | ==== 9. Fassung ==== | ||
+ | In Programmen gibt es viele seichte, triviale Stellen. | ||
+ | Nicht zu einfache Programme sind jedoch an einigen Stellen auch tief oder knifflig. | ||
+ | Eine solche Frage ist die nach dem aktuellen Datum. | ||
+ | Der Nutzer soll dieses nicht eingeben müssen. | ||
+ | |||
+ | Hier werden Kenntnisse der Systemschnittstelle benötigt. | ||
+ | Da es um Zeitangaben geht, | ||
+ | sollten wir zuerst in der ''< | ||
+ | Dort gibt es eine Funktion '' | ||
+ | allerdings in einer sehr unhandlichen Form: | ||
+ | die Anzahl der Sekunden seit dem 1. Januar 1970, 0.00 Uhr Weltzeit (GMT). | ||
+ | Diese Zeitangabe lässt sich mit einer Funktion '' | ||
+ | in Ortszeit umrechnen und aufbrechen in Jahr, Monat, Tag, Stunde usw. | ||
+ | Zu beachten ist, | ||
+ | dass Monate ab 0 (Januar) beginnend zählen und 1900 als Jahr Null angegeben wird. | ||
+ | |||
+ | <code cpp> | ||
+ | //> tageseit.cpp | ||
+ | |||
+ | #include < | ||
+ | |||
+ | Datum heute() | ||
+ | { | ||
+ | using namespace std; | ||
+ | |||
+ | time_t jetzt = time(nullptr); | ||
+ | struct tm* t = localtime(& | ||
+ | |||
+ | Datum d; | ||
+ | d.jahr | ||
+ | d.monat = t-> | ||
+ | d.tag = t-> | ||
+ | return d; | ||
+ | } | ||
+ | //~ | ||
+ | </ | ||
+ | Die Funktion '' | ||
+ | einer '' | ||
+ | über den mit '' | ||
+ | systeminternen Variable vom Typ '' | ||
+ | |||
+ | ==== 10. Fassung ==== | ||
+ | Nun fehlt noch das Hauptprogramm. Das Einfachste wäre | ||
+ | |||
+ | <code cpp> | ||
+ | #include < | ||
+ | |||
+ | int main() | ||
+ | { | ||
+ | Datum start, ende; | ||
+ | |||
+ | std::cout << " | ||
+ | std::cin >> start.jahr >> start.monat >> start.tag; | ||
+ | |||
+ | std::cout << " | ||
+ | std::cin >> ende.jahr >> ende.monat >> ende.tag; | ||
+ | |||
+ | std::cout << "Tage = " << anzahltage(start, | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | Es soll aber mit einem Datum als Parameter aufrufbar sein: | ||
+ | |||
+ | tageseit 1967-03-22 | ||
+ | |||
+ | Das Datum ist nach EN anzugeben. | ||
+ | Wird kein Datum angegeben, wird das Geburtsdatum im Nutzerdialog erfragt. | ||
+ | Das Programm berechnet die Anzahl der Tage bis zum aktuellen Datum, | ||
+ | das nicht eingegeben wird. Ungültige Eingaben sind zurückzuweisen. | ||
+ | |||
+ | <code cpp> | ||
+ | //> tageseit.cpp | ||
+ | |||
+ | #include < | ||
+ | #include < | ||
+ | |||
+ | int main(int argc, char* argv[]) | ||
+ | { | ||
+ | Datum geburt; | ||
+ | |||
+ | if (argc == 1) // Aufruf ohne Parameter | ||
+ | { | ||
+ | std::cout << "seit Datum (Jahr-Monat-Tag): | ||
+ | char puffer[20]; | ||
+ | std:: | ||
+ | std::cin >> puffer; | ||
+ | std:: | ||
+ | geburt.jahr | ||
+ | geburt.monat = atoi(puffer+5); | ||
+ | geburt.tag | ||
+ | } | ||
+ | else if (argc == 2) // Datum als Argument | ||
+ | { | ||
+ | geburt.jahr | ||
+ | geburt.monat = atoi(argv[1]+5); | ||
+ | geburt.tag | ||
+ | } | ||
+ | |||
+ | if (!istgueltig(geburt)) | ||
+ | { | ||
+ | std::cerr << "Kein gueltiges Datum.\n"; | ||
+ | return 1; | ||
+ | } | ||
+ | |||
+ | std::cout << "Tage = " << anzahltage(geburt, | ||
+ | return 0; | ||
+ | } | ||
+ | //~ | ||
+ | </ | ||
+ | ===== Abschluss und Ausblick ===== | ||
+ | Damit erfüllt das {{tageseit.cpp|Programm}} die ihm gestellte Aufgabe. | ||
+ | |||
+ | In diesem Text ist vorgesehen, | ||
+ | dass ab den mit '' | ||
+ | bzw. mit '' | ||
+ | bis zur mit '' | ||
+ | in Dateien mit dem dahinterstehenden Namen zu schreiben ist. | ||
+ | Auf diese Weise werden Quelltext und Dokumentation | ||
+ | nach der Idee des Literate Programming von Donald E. Knuth zusammengefasst. | ||
+ | Prinzip one source: | ||
+ | Halte Quelle und Dokumentation stets konsistent, am besten an einem Ort. | ||
+ | |||
+ | Das Herausziehen der Quellen (aus einer ASCII-Klartextdatei) | ||
+ | kann ein [[anwenden: | ||
+ | eine schöne Übungsaufgabe in Sachen Dateiarbeit. | ||
+ | Die Idee dafür habe ich aus [Bruce Eckel: Thinking in C++]. | ||
+ | |||
+ | Das endgültige Programm umfasst nur 95 Zeilen! | ||
+ | Hier ist noch ein Testrahmen: | ||
+ | |||
+ | < | ||
+ | //# make.bat | ||
+ | call cc tageseit.cpp | ||
+ | tageseit | ||
+ | //~ | ||
+ | </ | ||
+ | Auf dem Weg zur Lösung haben wir einen weiten Bogen geschlagen. | ||
+ | Angefangen haben wir bei den Problemen der Algorithmierung, | ||
+ | mit denen Programmierer unabhängig von der Sprache zu tun haben, | ||
+ | in der sie Algorithmen niederschreiben. | ||
+ | - Im ersten Versuch begegneten uns die C-Sprachkonzepte Funktion, Zählschleife und bedingte Anweisung. | ||
+ | - Logische Verknüpfungen und die Nutzung von Funktionen als wiederverwendbare Programmbausteine kamen beim zweiten Versuch hinzu. | ||
+ | - Mehrfachverzweigungen, | ||
+ | - Im vierten Anlauf kam die Idee der Umrechnung in eine geeignetere Darstellung auf; ein neues Bezugssystem kann umwälzende Auswirkungen auf das zu bewältigende Problem haben. Das Problem zerfällt in zwei Teilprobleme, | ||
+ | - Bei Version 5 brachte der Wechsel des Bezugspunktes erst eine kleine Verbesserung, | ||
+ | - in Teil 6 mit mehr geistiger Anstrengung sogar Einsicht in einen unbekannten funktionalen Zusammenhang. | ||
+ | - Die Bündelung inhaltlich zusammengehöriger Daten im Teil 7 bildet die Grundlage der Datenabstraktion in objektbasierten Programmen. | ||
+ | - Die Teilaufgabe 8 bekümmerte sich um die Plausibilität von Eingabedaten. Das GIGO-Prinzip der Datenverarbeitung lautet: garbage in, garbage out. Schickt man Datenmüll in eine Berechnung, so wird auch wieder Datenmüll herauskommen. | ||
+ | - Im Teil 9 wurde ein kleiner Ausflug in die Programmierschnittstelle des Systems unternommen. | ||
+ | - Die Kommandozeilenparameter im abschliessenden Teil 10 zeigen, wie die Oberfläche von Systemdienstprogrammen mit den Nutzereingaben verbunden wird. | ||
+ | |||
+ | Mit der einfachen Frage am Anfang wurde ein großer Bereich | ||
+ | der Programmiergrundlagen berührt. | ||
+ | Dabei haben wir aber keine Dateien benutzt, | ||
+ | schließlich muss das Programm keine Daten speichern. | ||
+ | |||
+ | Man könnte den Typ '' | ||
+ | aber dieses Programmierbeispiel ist schon lang genug. Schluss jetzt! | ||
+ | |||