namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


lernen:datum

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

lernen:datum [2016-12-28 12:46] (aktuell)
Zeile 1: Zeile 1:
 +====== Wie alt bist du (in Tagen) ? ======
 +> Selbst das längste Leben lässt sich in Sekunden ausdrücken.
 +>>​--- ​ Robert Merle: Madrapur
 +
 +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 ​    (Jahr Monat Tag): 2002 12 24
 +  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 ====
 +<​code>​
 +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,​
 +   ​zusammengefasst:​
 +    33 Jahre je 365 Tage ...  +10945= 11229
 +    bei Schaltjahr je ein Tag +9    = 11238 (1968,​72,​76,​80,​84,​88,​92,​96,​2000)
 +
 +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
 +                                      =====
 +</​code>​
 +Die ganze Rechnung klappt nicht, wenn Anfangs- und Endjahr das gleiche sind. 
 +Dann wäre genau die Länge des Jahres abzuziehen:
 +
 +<​code>​
 +D Falls Anfangs- gleich Endjahr:
 +    - (365+evtl. Schalttag) ​        = Ergebnis ​
 +</​code>​
 +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 endmonat, ​  int endtag)
 +{
 +  int tage = -starttag; ​         // Teil A: Rest des Startjahres
 +  if (startmonat==1) ​ tage += 31;
 +  if (startmonat<​=2)
 +  {
 +    tage += 28; 
 +    if (startjahr%4==0) ​  ​tage++;​ // Schaltjahr: Julianische Regel
 +    if (startjahr%100==0) tage--; // doch keins: Gregorianische Regel
 +    if (startjahr%400==0) tage++; // trotzdem eins: Gregorianische Ausnahme
 +  }
 +  if (startmonat<​=3) ​ tage += 31;
 +  if (startmonat<​=4) ​ tage += 30;
 +  if (startmonat<​=5) ​ tage += 31;
 +  if (startmonat<​=6) ​ tage += 30;
 +  if (startmonat<​=7) ​ tage += 31;
 +  if (startmonat<​=8) ​ tage += 31;
 +  if (startmonat<​=9) ​ tage += 30;
 +  if (startmonat<​=10) tage += 31;
 +  if (startmonat<​=11) tage += 30;
 +  if (startmonat<​=12) tage += 31;
 +
 +  for (int jahr=startjahr+1;​ jahr<​endjahr;​ jahr++) // Teil B: Zwischenjahre
 +  {
 +    tage += 365;
 +    if (jahr%4==0) ​  ​tage++;​
 +    if (jahr%100==0) tage--;
 +    if (jahr%400==0) tage++;
 +  }
 +
 +  if (endmonat>​1) ​ tage += 31;   // Teil C: Anfang des Endjahres
 +  if (endmonat>​2)
 +  {
 +    tage += 28);
 +    if (endjahr%4==0) ​  ​tage++;​
 +    if (endjahr%100==0) tage--;
 +    if (endjahr%400==0) tage++;
 +  }
 +  if (endmonat>​3) ​ tage += 31;
 +  if (endmonat>​4) ​ tage += 30;
 +  if (endmonat>​5) ​ tage += 31;
 +  if (endmonat>​6) ​ tage += 30;
 +  if (endmonat>​7) ​ tage += 31;
 +  if (endmonat>​8) ​ tage += 31;
 +  if (endmonat>​9) ​ tage += 30;
 +  if (endmonat>​10) tage += 31;
 +  if (endmonat>​11) tage += 30;
 +
 +  tage += endtag;
 + 
 +  // Teil D: Korrektur ​
 +
 +  if (startjahr == endjahr)
 +  {
 +    tage -= 365;
 +    if (startjahr%4==0) ​  ​tage++;​
 +    if (startjahr%100==0) tage--;
 +    if (startjahr%400==0) tage++;
 +  }
 +  return tage; // endlich! richtig??
 +}
 +</​code>​
 +Für C-Fremdlinge seien einige komische Zeichen erklärt: ​
 +''​+=''​ erhöht die links stehende Variable um den rechten Wert, 
 +''​-=''​ zieht ab, 
 +''​++''​ und ''​--''​ zählen die Variable eins rauf bzw. runter, ​
 +''​a%b''​ wird a modulo b gelesen und berechnet den Rest der Division von a durch b.
 +
 +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-"​Eiertanz"​ lässt sich herausziehen:​
 +
 +<code cpp>
 +bool schaltjahr(int jahr)
 +{
 +  if (jahr%400 == 0) return 1; // besondere Schaltjahre:​ 1600, 2000, 2400
 +  if (jahr%100 == 0) return 0; // andere Jahrhunderte nicht
 +  if (jahr%4 ​  == 0) return 1; // gewoehnliche Schaltjahre
 +  /* else */         ​return 0; // gemeine Jahre
 +}
 +</​code>​
 +oder unter Ausnutzung logischer Verknüpfungen UND (&&​) und ODER (||):
 +
 +<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;​
 +}
 +//~
 +</​code>​
 +Ganze 20 Zeilen lassen sich einsparen, z.B. im Teil B stünde dann nur
 +<code cpp>
 +    tage += 365 + schaltjahr(jahr);​
 +</​code>​
 +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 endmonat, ​  int endtag)
 +{
 +  int tage = 0;     // Teil A': zurueck zum Anfang des Startjahres
 +  if (startmonat>​1) ​ tage -= 31;
 +  if (startmonat>​2) ​ tage -= 28 + schaltjahr(startjahr);​
 +  if (startmonat>​3) ​ tage -= 31;
 +  if (startmonat>​4) ​ tage -= 30;
 +  if (startmonat>​5) ​ tage -= 31;
 +  if (startmonat>​6) ​ tage -= 30;
 +  if (startmonat>​7) ​ tage -= 31;
 +  if (startmonat>​8) ​ tage -= 31;
 +  if (startmonat>​9) ​ tage -= 30;
 +  if (startmonat>​10) tage -= 31;
 +  if (startmonat>​11) tage -= 30;
 +  tage -= starttag;
 + 
 +  for (int jahr=startjahr;​ jahr<​endjahr;​ jahr++) // Teil B': ganze Jahre
 +  {
 +    tage += 365 + schaltjahr(jahr);​
 +  }
 +
 +  if (endmonat>​1) ​ tage += 31;  // Teil C: Anfang des Endjahres
 +  if (endmonat>​2) ​ tage += 28 + schaltjahr(endjahr);​
 +  if (endmonat>​3) ​ tage += 31;
 +  if (endmonat>​4) ​ tage += 30;
 +  if (endmonat>​5) ​ tage += 31;
 +  if (endmonat>​6) ​ tage += 30;
 +  if (endmonat>​7) ​ tage += 31;
 +  if (endmonat>​8) ​ tage += 31;
 +  if (endmonat>​9) ​ tage += 30;
 +  if (endmonat>​10) tage += 31;
 +  if (endmonat>​11) tage += 30;
 +  tage += endtag;
 +
 +  return tage; 
 +}
 +</​code>​
 +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 "​schlimme"​ Konstruktionen in C. 
 +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; 
 +             ​break;​
 +    case 1:  break;
 +  };
 +</​code>​
 +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);​
 +
 +</​code>​
 +Jetzt wird nur noch eine Addition ausgeführt. Gut, nicht? Es geht noch besser.
 +
 +Solche Mehrfachverzweigungen,​ die nur einzelne Werte nachschlagen,​
 +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 endmonat, ​  int endtag)
 +{
 +  const int 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; ​   // Teil A'
 +  if (startmonat > 2) tage += schaltjahr(startjahr);​
 +
 +  tage = -tage; // Tage bis einschliesslich Starttag werden abgezogen
 +
 +  for (int jahr=startjahr;​ jahr<​endjahr;​ jahr++) // Teil B'
 +  {
 +    tage += 365 + schaltjahr(jahr);​
 +  }
 +
 +  tage += monatstage[endmonat] + endtag; ​       // Teil C'
 +  if (endmonat > 2) tage+= schaltjahr(endjahr);​
 +
 +  return tage; 
 +}
 +</​code>​
 +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:​ A picture counts more than ... Kwords.)
 +Machen wir uns ein Bild von dem, was geschieht:
 +
 +  Anfang
 +  Startjahr ​  ​A'​
 +  |-------------------Start ​            ​C' ​
 +  |------------------------------|------------Ende
 +              B' ​                ​Anfang
 +                                 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"​-Stimmung.
 +Vorher rechnete man "ad urbe condita",​
 +ab dem sagenhaften Gründungsdatum der "​ewigen"​ Stadt (Rom) 753 v.u.Z.
 +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,​ die nach Tieren benannt sind: 
 +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)
 +{
 +  const int 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 ​           // Normaljahre
 +             + jahr/​4 ​           // Anzahl gewoehnliche Schaltjahre
 +             - jahr/​100 ​         // ohne Jahrhunderte
 +             + jahr/​400 ​         // aber mit 400er Schaltjahren
 +             + monatstage[monat] // Vormonate
 +             + tag;
 +
 +  if (monat<​=2 && schaltjahr(jahr)) tage--; // vor dem Schalttag
 +
 +  return tage;
 +}
 +
 +int anzahltage(int startjahr, int startmonat, int starttag, ​
 +               int endjahr, ​  int endmonat, ​  int endtag)
 +{
 +  return tagesNr(endjahr, ​  ​endmonat, ​  ​endtag)
 +        -tagesNr(startjahr,​ startmonat, starttag); ​
 +}
 +</​code>​
 +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: ​ monat==0
 +  else       
 +  {
 +    jahr--; monat += 9; 
 +  }
 +
 +  const int 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   ​1 ​  ​2 ​  ​3 ​  ​4 ​   5    6    7    8    9    10   11
 +  };
 +
 +  return 365*jahr ​                       // Normaljahre
 +         + jahr/4 - jahr/100 + jahr/​400 ​ // Schalttage
 +         + monatstage[monat] ​            // Vormonate
 +         + tag;
 +}
 +</​code>​
 +==== 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)/​10
 +
 +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>​2) monat -= 3; // Maerz: monat==0
 +  else       
 +  {
 +    jahr--; monat += 9; 
 +  }
 +  return 1461*jahr/4 - jahr/100 + jahr/400 // ganze Jahre
 +         + (306*monat+5)/​10 ​               // Vormonate
 +         + tag;
 +}
 +</​code>​
 +Jetzt ist die Tagesnummerberechnung kompakt. ​
 +Die Ideen, die zu dieser Formel geführt haben, ​
 +sind am Quelltext nicht mehr nachvollziehbar. ​
 +Die "​magischen"​ Zahlen 1461 und 306 scheinen "vom Himmel gefallen"​ zu sein. 
 +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/​4 - d.jahr/100 + d.jahr/400 // ganze Jahre
 +         + (306 * d.monat + 5)/10                // Vormonate
 +         + d.tag;
 +}
 +
 +int anzahltage(Datum start, Datum ende)
 +{
 +  return tagesNr(ende) - tagesNr(start); ​
 +}
 +//~
 +</​code>​
 +==== 8. Fassung ====
 +Computerprogramme,​ die Nutzereingaben abfragen, ​
 +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<​1 || d.monat<​1 || d.monat>​12 || d.jahr<​0) return 0;
 +
 +  const int 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));​
 +}
 +//~
 +</​code>​
 +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 ''<​ctime>''​-Bibliothek nachsehen. ​
 +Dort gibt es eine Funktion ''​time()'',​ die die aktuelle Zeit liefert, ​
 +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 ''​localtime()''​
 +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 <​ctime>​
 +
 +Datum heute()
 +{
 +  using namespace std;
 +
 +  time_t jetzt = time(nullptr);​
 +  struct tm* t = localtime(&​jetzt);​
 +
 +  Datum d;
 +  d.jahr ​ = t->​tm_year+1900;​
 +  d.monat = t->​tm_mon +1;
 +  d.tag   = t->​tm_mday;​
 +  return d;
 +
 +//~
 +</​code>​
 +Die Funktion ''​localtime()''​ übernimmt die Adresse (''&''​) ​
 +einer ''​time_t''​-Typ-Variable und liefert einen Zeiger (''​*''​), ​
 +über den mit ''​->''​ auf die Komponenten einer 
 +systeminternen Variable vom Typ ''​tm''​ zugegriffen werden kann.
 +
 +==== 10. Fassung ====
 +Nun fehlt noch das Hauptprogramm. Das Einfachste wäre
 +
 +<code cpp>
 +#include <​iostream>​
 +
 +int main()
 +{
 +  Datum start, ende;
 +
 +  std::cout << "​Anfangsdatum (Jahr Monat Tag): ";
 +  std::cin >> start.jahr >> start.monat >> start.tag;
 +
 +  std::cout << "​Enddatum ​    (Jahr Monat Tag): ";
 +  std::cin >> ende.jahr >> ende.monat >> ende.tag;
 + 
 +  std::cout << "Tage = " << anzahltage(start,​ ende) << '​\n';​
 +  return 0;
 +}
 +</​code>​
 +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 <​iostream>​
 +#include <​cstdlib>​
 +
 +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::​cin.width(20);​
 +    std::cin >> puffer;
 +    std::​cin.ignore(200,'​\n'​);​
 +    geburt.jahr ​ = atoi(puffer);​
 +    geburt.monat = atoi(puffer+5);​
 +    geburt.tag ​  = atoi(puffer+8);​
 +  }
 +  else if (argc == 2) // Datum als Argument
 +  {
 +    geburt.jahr ​ = atoi(argv[1]);​
 +    geburt.monat = atoi(argv[1]+5);​
 +    geburt.tag ​  = atoi(argv[1]+8);​
 +  }
 +
 +  if (!istgueltig(geburt))
 +  {
 +    std::cerr << "Kein gueltiges Datum.\n";​
 +    return 1;
 +  }
 +
 +  std::cout << "Tage = " << anzahltage(geburt,​ heute()) << '​\n';​
 +  return 0;
 +}
 +//~
 +</​code>​
 +===== Abschluss und Ausblick =====
 +Damit erfüllt das {{tageseit.cpp|Programm}} die ihm gestellte Aufgabe. ​
 +
 +In diesem Text ist vorgesehen, ​
 +dass ab den mit ''​%%//:​%%''​ beginnenden Zeilen ​
 +bzw. mit ''​%%//>​%%''​ fortsetzend ​
 +bis zur mit ''​%%//​~%%''​ beginnenden Zeile stehender Quelltext ​
 +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:​onesource:​extract|Hilfsprogramm]] erledigen, ​
 +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:
 +
 +<​code>​
 +//# make.bat
 +call cc tageseit.cpp
 +tageseit
 +//~
 +</​code>​
 +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,​ Entscheidungstabellen und indizierte Variablen (Felder) sind im dritten Versuch eng miteinander verknüpft und ineinander überführbar.
 +  -  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,​ ein schwieriges,​ die Umrechnungsfunktion,​ und ein triviales, die Differenzbildung. Insgesamt es ist jedoch einfacher als alle vorherigen Versuche.
 +  - 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 ''​Datum''​ auch in objektorientierter Weise weiterentwickeln, ​
 +aber dieses Programmierbeispiel ist schon lang genug. Schluss jetzt!
 +
  
lernen/datum.txt · Zuletzt geändert: 2016-12-28 12:46 (Externe Bearbeitung)