namespace cpp {}

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


lernen:datum

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

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
                                      =====

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)         = Ergebnis 

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 ;-)

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??
}

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:

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
}

oder unter Ausnutzung logischer Verknüpfungen UND (&&) und ODER (||):

//: 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

    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.

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; 
}

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:

  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;
  };

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!)

  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, 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):

int anzahltage(int startjahr, int startmonat, int starttag, 
               int endjahr,   int endmonat,   int endtag)
{
  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;    // 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; 
}

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.

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            // 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); 
}

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:

int tagesNr(int jahr, int monat, int tag)
{
  if (monat > 2) monat -= 3;  // Maerz:  monat==0
  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   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;
}

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.

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;
}

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.

//> 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); 
}
//~

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:

//> tageseit.cpp
 
bool istgueltig(Datum d)
{
  if(d.tag<1 || d.monat<1 || d.monat>12 || d.jahr<0) return 0;
 
  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 <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.

//> 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;
} 
//~

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

#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;
}

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.

//> 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;
}
//~

Abschluss und Ausblick

Damit erfüllt das 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 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:

//# 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.

  1. Im ersten Versuch begegneten uns die C-Sprachkonzepte Funktion, Zählschleife und bedingte Anweisung.
  2. Logische Verknüpfungen und die Nutzung von Funktionen als wiederverwendbare Programmbausteine kamen beim zweiten Versuch hinzu.
  3. Mehrfachverzweigungen, Entscheidungstabellen und indizierte Variablen (Felder) sind im dritten Versuch eng miteinander verknüpft und ineinander überführbar.
  4. 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.
  5. Bei Version 5 brachte der Wechsel des Bezugspunktes erst eine kleine Verbesserung,
  6. in Teil 6 mit mehr geistiger Anstrengung sogar Einsicht in einen unbekannten funktionalen Zusammenhang.
  7. Die Bündelung inhaltlich zusammengehöriger Daten im Teil 7 bildet die Grundlage der Datenabstraktion in objektbasierten Programmen.
  8. 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.
  9. Im Teil 9 wurde ein kleiner Ausflug in die Programmierschnittstelle des Systems unternommen.
  10. 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: 2020-07-27 09:21 von 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki