C-Funktionen zur Wochentagsermittlung

Ermittlung der Kalenderwoche
Ermittlung des Osterdatums
Tagesdifferenz ermitteln
Wochentagsermittlung für den julianischen Kalender
Verbesserungen an den Funktionen
Ermittlung des Wochentages von historischen Daten

Es fing damit an, dass ich unter Windows in der Eingabeaufforderung ein Utility ähnlich wie unter Linux (Unix) Kommando "cal" vermisste.
Es gibt zwar ein Utility namens "dcal", was genau das gleiche unter Windows kann, aber die Ausgabe lässt zu wünschen übrig:

C:\> dcal 3 2008

           M∑rz 2008

 So Mo Di Mi Do Fr Sa
                                       1
   2    3    4    5    6    7    8
   9  10  11  12  13  14  15
 16  17  18  19  20  21  22
 23  24  25  26  27  28  29
 30  31  

Der Umlaut 'ä' im Monat März (und Jänner) wird als Summenzeichen dargestellt und die Spalten sind nicht optimal formatiert. Außerdem wäre es nicht schlecht, den Kalender auf Wunsch auch mit Montag beginnen zu lassen. Da hilft nur eines: selbst programmieren! Dazu benötigt man aber eine Funktion, die den Wochentag ermitteln kann.

Anfang der 80er Jahre gab es im ORF eine Sendung namens "Computerbox" und es wurde so ein Programm in Basic für den Commodore C64 vorgestellt.
Dieses habe ich später dann in Pascal konvertiert und jetzt in C:

// 1  = Sonntag, 2 = Montag, ... , 7 = Samstag

int dayofweek(int d,int m,int y)
{
  int s=0,k,l,o,t,z1,z2,z3,z4,z5,z6;
  double p,z;
  z=(0.6+(1.0/m));
  k=(int) z;
  l=y-k;
  o=m+12*k;
  p=l/100.0;
  z=p/4.0;
  z1=(int) z;
  z2=(int) p;
  z=(5.0*l)/4.0;
  z3=(int) z;
  z=(13.0*(o+1)/5.0);
  z4=(int) z;
  z5=z4+z3-z2+z1+d-1;
  z=z5/7.0;
  z6=(int) z;
  t=z5-(7*z6)+1;
  return(t);
}

Die Funktion erfüllt zwar ihren Zweck, aber sie ist ziemlich kompliziert und undurchsichtig. Es muss also noch einfacher gehen und eine reine Integer-Arithmetik ausreichen. In der Newsgroup "de.comp.lang.c" habe ich meinen Wunsch geäußert und tatsächlich eine Antwort erhalten. Eine Datumsplausibilitätsprüfung wurde noch ergänzt und das ganze sieht dann so aus:

// gibt Wochentag zurück
// 1  = Sonntag, 2 = Montag, ... , 7 = Samstag
// 0  = Datum ungültig z.B. 29.2.2007, 32.12.2007
// -1 = Jahr kleiner als 1583 oder größer als 9999

int dayofweek(int d, int m, int y)                  // Tag, Monat, Jahr; Jahr unbedingt vierstellig
{
  int s=0, mtag[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};    // Anzahl der Tage pro Monat
  if ((y % 4==0 && y % 100!=0) || y % 400==0) s=1;              // Schaltjahrescheck
  if (y<1583 || y>9999) return(-1);                             // Jahr auf sinnvollen Bereich einschränken
  if (m<1 || m>12) return(0);                                   // Monat auf Gültigkeit prüfen
  if (d<1 || d>mtag[m]+s*(m==2)) return(0);                     // Tag auf Gültigkeit prüfen
  if (m < 3) { m += 13; y--; } else  m++;                       // Jahresanfang auf März verschieben
  s = d + 26*m/10 + y + y/4 - y/100 + y/400 + 6;                // eigentliche Berechnung
  return(s % 7 + 1);                                            // Ergebnis anpassen
}

Zur Info: Nicht alle Länder haben 1582 den Gregoranischen Kalender eingeführt.
England und seine Kolonien, somit auch Nordamerika haben erst 1752 umgestellt.

( Details siehe Wikipedia: http://de.wikipedia.org/wiki/Gregorianischer_Kalender )

Noch kurz zur Schaltjahresregel: Ein Jahr ist ein Schaltjahr, wenn es ohne Rest durch 4 teilbar ist. Somit sind z. B. die Jahre 1992, 1996, 2004 und 2008 Schaltjahre. Aber es gibt eine Ausnahme: Ist ein Jahr durch 100 ohne Rest teilbar, dann ist es kein Schaltjahr, es sei denn es ist durch 400 ohne Rest teilbar. Somit waren 1700, 1800 und 1900 keine Schaltjahre und 2100 wird ebenfalls keines sein; wohl aber 1600 und 2000.

Dadurch gibt es im Gregorianischem Kalender gibt es einen 400 Jahres-Rhythmus:

400 Jahre * 365 Tage sind 146000 Tage. Dazu noch 97 Schalttage (100 weniger 3) ergibt 146097 Tage. Das entspricht genau 20871 Wochen. Somit fallen sowohl der 1.1.1601 als auch der 1.1.2001 auf einen Montag! Und der 1.1.2401 wird ebenfalls wieder ein Montag  sein.

Mit dieser Info und ein bisschen Denksport sollte die Funktionsweise der Berechnung durchschaubar werden. Nach der Plausibilitätsprüfung des Datums wird der "Jahresanfang" offensichtlich auf 1. März verschoben (exakt "0. März", weil ja der Tag noch addiert wird). Dadurch braucht man nur den Wochentag des "0. März" ermitteln und kann davon den Rest des Jahres berechnen ohne darauf Rücksicht nehmen zu müssen, ob es ein Schaltjahr ist oder nicht. Somit ergibt sich:

 if (m < 3) { m += 13; y--; } else  m++;

  1          2008 = 14       2007
  2          2008 = 15       2007
  3          2008 =   4       2008
  4          2008 =   5       2008
 …
 11         2008 = 12       2008
 12         2008 = 13       2008


Jetzt wird es interessant: 26 * m / 10

(Man kann diese Berechnung auch auf 13 * m / 5 kürzen – funktioniert genauso.)

Tja, was macht diese Berechnung? Man beachte, dass es reine Integer-Arithmetik ist.
Beginnen wir mit März, weil der Jahresanfang auf den "0. März" verschoben wurde:

Monat
m
*26
/10
Differenz zum nächsten Monat
März
4
104
10
3
April
5
130
13
2
Mai
6
156
15
3
Juni
7
182
18
2
Juli
8
208
20
3
August 9
234
23
3
September
10
260
26
2
Oktober
11
286
28
3
November
12
312
31
2
Dezember
13
338
33
3
Jänner (Januar)
14
364
36
3
Februar
15
390
39
-


Interessant ist hierbei die Differenz zwischen zwei benachbarten Monaten: Hat ein Monat 30 Tage ergibt sich eine 2, hat es dagegen 31 Tage dann eine 3. Hier ist wieder gut, dass der Februar das letzte Monat ist. 28 Tage sind exakt 4 Wochen; die 2 und die 3 sind die Differenz zu 30 und 31 Tagen. Somit wird geschickt der Bezug des Wochentages zum imaginären Jahresbeginn hergestellt. Somit ist die Formel klar:

 s = d + 26* m/10 + y + y/4 - y/100 + y/400 + 6;

Addiere also den Tag (d) plus den Versatz zum Jahresanfang (26*m/10) plus das Jahr (y) plus die Schaltjahreskorrektur  (y/4 – y/100 + y/400) plus einen Korrekturwert (6 wenn Sonntag der erste Tag der Woche sein soll).

 return(s % 7 + 1);

Das ganze wird noch durch 7 geteilt und hier ist der Divisionsrest ausschlaggebend, weil sich der Wochentagsrhythmus ja in 7 Tagen wiederholt. Das heißt mathematisch "modulo". In C steht dafür das % - Zeichen; in anderen Programmiersprachen z.B. die Funktion MOD. Somit ergibt sich eine 0 für Sonntag, eine 1 für Montag usw.

Wenn man noch eine 1 dazuzählt, dann erleichtert man sich den Umgang mit dem Ergebnis dieser Funktion und man kann die 0 dazu verwenden, um ein ungültiges Datum anzuzeigen. Die Plausibilitätsprüfung des Datums muss vor der Berechnung erfolgen, weil die eigentliche Berechnungsroutine alle Werte "schluckt" und die Modulo-Funktion  mit 7 immer nur Zahlen von 0 bis 6 ausgeben kann.

Die neue unkomplizierte Funktion ist ziemlich genau 3x so schnell die alte komplizierte. In der Praxis fällt der Unterschied kaum auf; zum Test habe ich den 13. eines jeden Monats von 1601 bis 2000 berechnet und das ganze 32000 (!) mal wiederholt. Dennoch dauerten diese fast 154 Millionen Berechnungen auf einem aktuellen PC erwartungsgemäß deutlich weniger als eine Minute.

Wenn man genau hinsieht, dann macht die alte Version im Prinzip auch dasselbe; es ist alles nur komplizierter formuliert: (auszugsweise)

 z=(0.6+(1.0/m));  k=(int) z;

 k wird bei Jänner und Februar 1; ab März 0. -> verschiebe also den Jahresanfang

 z=(13.0*(o+1)/5.0);  z4=(int) z;

 ist im Prinzip 26 * m / 10 gekürzt auf  13  * m  / 5

 z=z5/7.0;  z6=(int) z;  t=z5-(7*z6)+1;

Das entspricht der Modulo-Funktion. Zum Schluß wird wieder 1 dazugezählt, um den Bereich von 0 ... 6 auf 1 ... 7 zu verschieben.

Nachdem wir jetzt eine Funktion haben, welche den Wochentag ermittelt kann das Kalenderprogramm entworfen werden. Eine Möglichkeit ist: Frage die Wochentage aller Tage von 1 bis 31 ab und warte bis ein ungültiges Datum zurückgemeldet wird. Somit ist auch klar erkennbar ob der Februar 28 oder 29 Tage hat.

Hier die Kernfunktion des Programmes: 

#include <stdio.h>

main()
{
  int i,j,s,t=0,monat,jahr;
  monat=2;                             // gewünschtes Monat
  jahr=2008;                           // gewünsches Jahr
  printf("\n\n  So  Mo  Di  Mi  Do  Fr  Sa\n");
  for (i=1; i<32; i++)
  {
    s=dayofweek(i,monat,jahr);
    if (s)                             // wenn Datum gültig ist
    {
        if (t==0)                      // ersten Tag des Monats der richtigen Spalte zuordnen
      
{
          printf(" ");
          for (j=1; j<s; j++)
            printf("    ");
          t=1;
        }
        printf(" %2d ",i);
        if (s==7) printf("\n ");       // wenn Samstag erreicht dann eine neue Zeile beginnen
      }
   }
  printf("\n");
}

Man kann jetzt auch noch die Frage beantworten, wie viele Freitage tatsächlich auf einen 13. fallen.
Die Berechnung eines 400-Jahres-Rhythmuses von 1601 bis 2000 ergibt folgendes Ergebnis:


       687 Tage fallen auf einen Sonntag - das sind 14.31250 Prozent
       685 Tage fallen auf einen Montag - das sind 14.27083 Prozent
       685 Tage fallen auf einen Dienstag - das sind 14.27083 Prozent
       687 Tage fallen auf einen Mittwoch - das sind 14.31250 Prozent
       684 Tage fallen auf einen Donnerstag - das sind 14.25000 Prozent
       688 Tage fallen auf einen Freitag - das sind 14.33333 Prozent
       684 Tage fallen auf einen Samstag - das sind 14.25000 Prozent

Tatsächlich ist der Freitag der 13. überdurchschnittlich oft vertreten, obwohl es von Jahr zu Jahr starke Schwankungen gibt! Aber ehrlich gesagt, in der Praxis fällt das nicht ins Gewicht.

An und für sich würde sich ja alle 28 Jahre (= das kleinste gemeinsame Vielfache von 7 Wochentagen und 4 Jahren [3 "normale" Jahre und 1 Schaltjahr]) der Kalender wiederholen, wenn nicht durch den Ausfall von 3 Schalttagen in 400 Jahren dieser Zyklus gestört werden würde. Zum Beispiel sind die Jahre 1901, 1929, 1957, 1985 und (2000 ist ein Schaltjahr daher auch) 2013, 2041, 2069 und 2097 (hier Ende weil 2100 kein Schaltjahr mehr ist) bzgl. der Wochentage identisch. Daher ist innerhalb einer solchen "kleinen Periode" wie z.B. von 1901 - 1928 (28 Jahre) und von 1901 - 2096 (7*28 = 196 Jahre) jeder Wochentag exakt gleich - also zu 14,28571 % (exakt 1/7) vertreten!

Ermittlung der Kalenderwoche

Noch ein Anwendungsbeispiel ist die Ermittlung der Kalenderwoche. Zuerst einmal zu der Definition der Kalenderwoche, wie sie im deutschsprachigem Raum ermittelt wird:
(heißt exakt "Kalenderwoche nach DIN 1355 / ISO 8601")

- jeden Montag und nur montags beginnt eine neue Kalenderwoche
- die erste Kalenderwoche ist diejenige, die mind. 4 Tage des neuen Jahres enthält (d.h. muss der erste Donnerstag im Jahr in der ersten Kalenderwoche liegen)
- der 29., 30. und 31. Dezember können schon zur ersten Kalenderwoche des Folgejahres gehören
- der 1., 2. und 3. Jänner können noch in der letzten Kalenderwoche des Vorjahres liegen
- jedes Jahr hat daher entweder 52 oder 53 vollständige Kalenderwochen zu genau 7 Tagen
- ein Jahr hat genau dann 53 Kalenderwochen, wenn es mit einem Donnerstag beginnt oder endet
- ist es ein Schaltjahr und beginnt  mit einem Mittwoch, dann hat es auch 53 Kalenderwochen (war z. B. 1992 der Fall)
(wenn ein Schaltjahr mit Mittwoch beginnt, dann endet es an einem Donnerstag)

So, jetzt erstellen wir einen Algorithmus zur Ermittlung der Kalenderwoche. Wir benötigen neben der Funktion "dayofweek" zur Ermittlung des Wochentages, die wir schon kennen, noch die Funktion "dayofyear" zur Ermittlung der fortlaufenden Nummer des Tages im Jahr. Diese ist relativ einfach zu programmieren:

int dayofyear(int d, int m, int y)                    // Tag, Monat, Jahr; Jahr unbedingt vierstellig
{
  int t=0,s=0, mtag[13]={0,31,28,31,30,31,30,31,31,30,31,30,31}; // Anzahl der Tage pro Monat
  if ((y % 4==0 && y % 100!=0) || y % 400==0) s=1;    // Schaltjahrescheck
  if (y<1583 || y>9999) return(-1);                   // Jahr auf sinnvollen Bereich einschränken
  if (m<1 || m>12) return(0);                         // Monat auf Gültigkeit prüfen
  if (s) mtag[2]=29;                                  // Bei einem Schaltjahr hat der Februar 29 Tage
  if (d<1 || d>mtag[m]) return(0);                    // Tag auf Gültigkeit prüfen
  do     
  {
    m--;
    t+=mtag[m];                                       // Tage der Vormonate in einer Schleife addieren
  } while (m>1);
  return(t+d);                                        // Ergebnis = Tage aller Vormonate + Tag des aktuellen Monats
}

So und hier ist die eigentliche Funktion zur Ermittlung der Kalenderwoche:

int weekofyear(int d, int m, int y)                  // Tag, Monat, Jahr; Jahr unbedingt vierstellig
{
  int r=0,s,t,w,t1,w1, mtag[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};  // Anzahl der Tage pro Monat
  if ((y % 4==0 && y % 100!=0) || y % 400==0) r=1;   // Schaltjahrescheck
  if (y<1583 || y>9999) return(-1);                  // Jahr auf sinnvollen Bereich einschränken
  if (m<1 || m>12) return(0);                        // Monat auf Gültigkeit prüfen
  if (d<1 || d>mtag[m]+r*(m==2)) return(0);          // Tag auf Gültigkeit prüfen
  t=dayofyear(d,m,y);                                // laufenden Tag des Jahres ermitteln
  w=dayofweek(1,1,y)-2;                              // Wochentag des 1. Jänners (1. Januars) ermitteln und anpassen
  if (w>3) w=w-7;                                    // Wochentag anpassen
  s=((t-1+w)/7)+1;                                   // Kalenderwoche ermitteln
  if (s==1)                                          // wenn KW 1 prüfen ob nicht doch letzte KW vom Vorjahr
  {
    w=w*(-1);
    if (w>=d)
    {
      t1=dayofyear(31,12,y-1);                       // KW vom 31.12. des Vorjahres ermitteln
      w1=dayofweek(1,1,y-1)-2;
      if (w1>3) w1=w1-7;
      s=((t1-1+w1)/7)+1;
    }
  }
  else
  {                                                  // wenn KW 53 erreicht prüfen ob nicht doch KW 1 vom nächsten Jahr
    if (s==53 && w==2 && r==1)                       // Jahr ist Schaltjahr und beginnt mit Mittwoch, dann passt KW 53
      s=53;
    else
      if (s==53 && w!=3) s=1;                        // Jahr beginnt nicht mit Donnerstag, dann KW 1 nächstes Jahr
  }
  return(s);
}

Erklärung des Algorithmusses:

 int r=0,s,t,w,t1,w1, mtag[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
 if ((y % 4==0 && y % 100!=0) || y % 400==0) r=1;
 if (y<1583 || y>9999) return(-1);
 if (m<1 || m>12) return(0);
 if (d<1 || d>mtag[m]+r*(m==2)) return(0);

Dieser Bereich ist die bereits bekannte Plausibilitätsprüfung des Tages und des Monats.

  t=dayofyear(d,m,y);
 w=dayofweek(1,1,y)-2;
 if (w>3) w=w-7;

Zuerst wird die laufende Nummer des Tages, dessen Kalenderwoche bestimmt werden soll, ermittelt.
Dann wird der Wochentag des 1. Jänner ermittelt und dieser wird noch angepasst.

Somit ergibt sich zuerst: Montag = 0, Dienstag = 1, Mittwoch = 2, Donnerstag = 3, Freitag = 4, Samstag = 5, Sonntag = -1
und dann:                      Montag = 0, Dienstag = 1, Mittwoch = 2, Donnerstag = 3, Freitag = -3, Samstag = -2, Sonntag = -1

somit beginnt einerseits die Woche mit Montag und andererseits erhält der Donnerstag das meiste "Gewicht".
(Wir erinnern uns: Der erste Donnerstag im Jahr fällt in die KW 1.)

  s=((t-1+w)/7)+1;

Hier wird die Kalenderwoche berechnet. Das Ergebnis ist immer mind. 1 und exakt jeden Montag beginnt dann die neue Kalenderwoche. Man beachte auch die große Differenz zwischen Donnerstag und Freitag (wichtig weil ja der erste Donnerstag das Kriterium dafür ist, ob die Woche die KW 1 ist).

  if (s==1)
  {
    w=w*(-1);
    if (w>=d)
    {
      t1=dayofyear(31,12,y-1);
      w1=dayofweek(1,1,y-1)-2;
      if (w1>3) w1=w1-7;
      s=((t1-1+w1)/7)+1;
    }
  }

Falls es sich um die erste Kalenderwoche handelt, muss noch geprüft werden, ob die ersten Tage im Jänner noch zu der letzten Kalenderwoche des Vorjahres oder schon zur ersten Woche des aktuellen Jahres gehören. Beginnt das Jahr mit einem Freitag, Samstag oder Sonntag, dann ist diese Bedingung erfüllt. Durch Multiplikation mit -1 wird aus Freitag eine 3, aus Samstag eine 2 und aus Sonntag eine 1. Alle anderen Wochentage sind dann 0 oder negativ. In diesem Fall wird erst gar nicht weitergeprüft. Beginnt aber das Jahr mit einem Freitag, dann fallen 3 Tage in die KW des Vorjahres, bei einem Samstag 2 Tage und bei einem Sonntag 1 Tag. Ist diese Bedingung erfüllt, dann wird die Kalenderwoche des 31. Dezember des Vorjahres nach dem bereits bewährten Verfahren ermittelt.

  else
  {
    if (s==53 && w==2 && r==1)
      s=53;
    else
      if (s==53 && w!=3) s=1;
  }

Sollte sich eine 53. Kalenderwoche ergeben, dann wird geprüft, ob die Bedingungen erfüllt sind. Entweder das Jahr beginnt mit einem Mittwoch und ist ein Schaltjahr (wie z.B. 1992) dann gibt es eine 53. KW oder das Jahr beginnt mit einem Donnerstag, ungeachtet dessen ob es ein Schaltjahr ist oder nicht. Ansonsten gibt es keine KW 53 sondern der Tag gehört schon zur KW 1 des folgenden Jahres.

Ermittlung des Osterdatums

Um den Kalender noch vollständig zu machen, müssen auch die beweglichen Feiertage ermittelt werden. Diese basieren auf dem Osterdatum. Dafür gibt es schon einen Algorithmus:

// ermittelt den Ostersonntag
// 0 = Bereich außerhalb von 1583 - 9999
// Zahl bis 31 ist März
// Zahl größer als 31 ist April
// für korrekten Tag im April 31  abziehen

int eastersunday(int y)
{
  int a,b,c,d,e,k,p,q,m,n,s;
  if (y<1583 || y>9999) return(0);
  a=y % 19;
  b=y % 4;
  c=y % 7;
  k=y / 100;
  p=(8*k + 13) / 25;
  q=k / 4;
  m=(15+k-p-q) % 30;
  n=(4+k-q) % 7;
  d=(19*a + m) % 30;
  e=(2*b + 4*c + 6*d + n) % 7;
  s=22+d+e;
  if (s==57) s=50;  // erste Ausnahme weil 26. April nicht zulässig, daher wird der 19. April genommen
  if (d==28 && e==6 && (11*m+11) % 30 <19) s=49; // zweite Ausnahme nötig weil sonst Ergebnis Samstag 24. April sein würde, daher 18. April (z.B. im Jahre 1954)
  return(s);
}

Zur Info: Für das Osterdatum im Julianischen Kalender gilt immer m=15 und n=6.
Man kann aber auch folgende kürzere julianische Osterformel verwenden:

  a=y % 19;
  d=(19*a+15) % 30;
  e=(y + y/4 + d) % 7;
  s=28+d-e;

Für 2009 ergibt sich: a=14, d=11, e=2, s=37 - also der 6. April Julianischer Kalender. Dieser hinkt im 21. Jahrhundert 13 Tage dem Gregorianischem Kalender hinterher (10 entfallene Tage vom Oktober 1582 + 29.2.1700 + 29.2.1800 + 29.2.1900), so dass der 6. April jul. dem 19. April greg. entspricht. Somit ist das Julianische Osterfest eine Woche später als das Gregorianische (12. April). Dann ist aber der Vollmond schon in einen Halbmond (genauer das Letzte Viertel) übergegangen, was zeigt warum die Reform notwendig war.

Für 2010 ergibt sich: a=15, d=0, e=6, s=22 - also der 22. März jul, was dem 4. April greg. entspricht. Zufällig fällt auch im Gregorianischem Kalender dann am gleichen Tag der Ostersonntag.

Erklärung siehe Wikipedia unter Osterdatum und hier.

Von diesem Tag an können alle anderen beweglichen Feiertage ermittelt werden:

Feiertag
Offset zum Ostersonntag
Rosenmontag
-48 Tage
Faschingdienstag
-47 Tage
Aschermittwoch
-46 Tage
Gründonnerstag
-3 Tage
Karfreitag
-2 Tage
Ostermontag
+1 Tag
Christi Himmelfahrt
+39 Tage
Pfingstsonntag
+49 Tage
Pfingstmontag
+50 Tage
Fronleichnam
+60 Tage

Vorsicht ist beim Aschermittwoch (und den Tagen davor) geboten, da er in den Februar fallen kann und man so den ev. vorhandenen Schalttag berücksichtigen muss:

    iDay=eastersunday(iYear); // Ostersonntag ermitteln

    s=0;
    if ((iYear % 4==0 && iYear % 100!=0) || iYear % 400==0) s=1; // Schaltjahr ermitteln

    // Aschermittwoch = 46 Tage vor Ostersonntag

    iMonth=3;
    iDay-=46;  // bzw. 47 Tage für Faschingdienstag, 48 Tage für Rosenmontag
    if (iDay<1) {iMonth--; iDay=iDay+28+s;}

Hier ist auch eine Funktion, welche bis zum Jahr 1582 den Julianischen Ostersonntag ermittelt:

int eastersunday(int y)

{
  int a,b,c,d,e,i,j,k,p,q,m,n,s;
  if (y<1 || y>10000) return(0);
  a=y % 19;
  if (y<1583)    // Julianischer Kalender
  {
    i=(19*a + 15) % 30;
    j=(y + y/4 + i) % 7;
    return(i-j+28);
  }              // Gregorianischer Kalender
  b=y % 4;
  c=y % 7;
  k=y / 100;
  p=(8*k + 13) / 25;
  q=k / 4;
  m=(15+k-p-q) % 30;
  n=(4+k-q) % 7;
  d=(19*a + m) % 30;
  e=(2*b + 4*c + 6*d + n) % 7;
  s=22+d+e;
  if (s==57) s=50;
  if (d==28 && e==6 && (11*m + 11) % 30 < 19) s=49;
  return(s);
}

Jean Meeus hat in seinem Buch folgende Formel veröffentlicht:

int eastersunday(int y)

{
  int a,b,c,d,e,f,g,h,i,k,l,m,q,r,s,x,z;
  z = y % 19;
  if (y<1583)     // Julianischer Kalender
  {
    a = y % 4;
    b = y % 7;
    d = (19*z + 15) % 30;
    e = (2*a + 4*b - d + 34) % 7;
    q = d + e + 114;
  }
  else
  {              // Gregorianischer Kalender
    b = y / 100;
    c = y % 100;
    d = b / 4;
    e = b % 4;
    f = (b + 8) / 25;
    g = (b - f + 1) / 3;
    h = (19*z + b - d - g + 15) % 30;
    i = c / 4;
    k = c % 4;
    l = (32 + 2*e + 2*i - h - k)  % 7;
    m = (z + 11*h + 22*l) / 451;
    q = h + l - 7*m + 114;
  }
  r = q / 31;
  s = q % 31;
  x = (r - 3)*31 + s + 1;
  return(x);
}


Tagesdifferenz ermitteln

Wie ermittelt man am besten die Differenz zwischen zwei Tagen? Man rechnet von einem Referenzjahr, welches unbedingt früher sein muss als das Jahr des ersten Datums, die Tage der Vorjahre zusammen sowie den aktuellen Tag im Jahr dazu. Dabei muss man natürlich die Schalttage berücksichtigen. Das macht man mit den beiden Tagen und ermittelt die Differenz.

Beispiel: (Referenzjahr 1999 der Einfachheit angenommen)

Erstes Datum: 11. September 2001
Tage der Vorjahre (1999, 2000): 365 + 366 = 731 Tage
aktueller Tag im Jahr: 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 11 = 254 Tage
gesamt : 985 Tage

Zweites Datum: 11. März 2004
Tage der Vorjahre (1999 - 2003): 365 + 366 + 365 + 365 + 365 = 1826 Tage
aktueller Tag im Jahr: 31 + 29 + 11 = 71 Tage
gesamt: 1897 Tage

Differenz: 1897 - 985 = 912 Tage

  // Limitierung ist wichtig weil sonst die Funktion versagt

  if (iYear1<1600) iYear1=1600;
  if (iYear1>8200) iYear1=8200;
  if (iMonth1<1)   iMonth1=1;
  if (iMonth1>12)  iMonth1=12;
  if (iDay1<1)     iDay1=1;
  if (iDay1>31)    iDay1=31;

  if (iYear2<1600) iYear2=1600;
  if (iYear2>8200) iYear2=8200;
  if (iMonth2<1)   iMonth2=1;
  if (iMonth2>12)  iMonth2=12;
  if (iDay2<1)     iDay2=1;
  if (iDay2>31)    iDay2=31;

  do
  {
    iWDay1=dayofweek(iDay1, iMonth1, iYear1);
    if (iWDay1==0) iDay1--;
  } while(!iWDay1);      // für gültiges Datum sorgen erstes Datum

  do
  {
    iWDay2=dayofweek(iDay2, iMonth2, iYear2);
    if (iWDay2==0) iDay2--;
  } while(!iWDay2);      // für gültiges Datum sorgen zweites Datum

  lSumme1=0; lSumme2=0;  // Summen auf Null setzen

  for (i=1599; i<iYear1; i++)
  {
    s=0;
    if ((i % 4==0 && i % 100!=0) || i % 400==0) s=1;
    lSumme1=lSumme1+365+s;         // Summe der Tage der Vorjahre addieren
  }
  s=dayofyear(iDay1, iMonth1, iYear1); // plus aktuellen Tag im Jahr dazu
  lSumme1=lSumme1+s;

  for (i=1599; i<iYear2; i++)
  {
    s=0;
    if ((i % 4==0 && i % 100!=0) || i % 400==0) s=1;
    lSumme2=lSumme2+365+s;         // Summe der Tage der Vorjahre addieren
  }
  s=dayofyear(iDay2, iMonth2, iYear2); // plus aktuellen Tag im Jahr dazu
  lSumme2=lSumme2+s;

  lDiff=lSumme2-lSumme1;              // hier wird die Tagesdifferenz ermittelt
  dDiff=(double) lDiff / 7.0;         // Anzahl der Wochen berechnen   
  dDiff=(double) lDiff / 30.436875;   // Anzahl der Monate berechnen (Durchschnittswert)
  dDiff=(double) lDiff / 365.2425;    // Anzahl der Jahre berechnen (Durchschnittswert)

Hinweis: Die Zahl 365,2425 für die Jahre ergibt sich folgendermaßen:

400 Jahre zu 365 Tagen + 100 weniger 3 Schalttage = 400*365 + 100 - 3 = 146097 Tage
macht pro Jahr durchschnittlich 365,2425 Tage
und pro Monat ist gleich 365,2425 / 12 = 30,436875 Tage pro Monat

Während man das Jahr durchaus noch mit zwei Kommastellen ausgeben kann, ist beim Monat wegen der starken Schwankungen nur eine Kommastelle empfehlenswert.

Wochentagsermittlung für den julianischen Kalender


Auch für den Julianischen Kalender existiert ein Algorithmus, mit dem man den Wochentag ermitteln kann:

// gibt Wochentag zurück                            
// 1  = Sonntag, 2 = Montag, ... , 7 = Samstag      
// 0  = Datum ungültig z.B. 29.2.2007, 32.12.2007   
// Julianischer Kalender (auch ab 1583!)

int dayofweekjul(int d, int m, int y)
{
  int s=0, mtag[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
  if (y<100 || y>10000) return(-1);
  if (m<1 || m>12) return(0);
  if (y % 4==0) s=1;
  if (d<1 || d>mtag[m]+s*(m==2)) return(0);
  if (m < 3) { m += 13; y--; } else  m++;
  s = d + 26*m/10 + y + y/4 + 4;
  return(s % 7 + 1);
}

Es ist im Prinzip die gleiche Formel, nur dass aufgrund der einfacheren Schaltjahresregelung diese etwas kürzer ist.

Beides kombiniert sieht dann so aus:
(bis 4. 10. 1582 julianischer Kalender, danach gregorianischer Kalender - Vorsicht, gilt nicht für alle Länder!)

int dayofweekjulandgreg(int d, int m, int y)
{
  int s=0, greg=0, mtag[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
  if (y<100 || y>10000) return(-1);
  if (m<1 || m>12) return(0);
  if (y==1582 && m==10 && d>4 && d<15) return(0); // verbotener (nicht existenter) Bereich 5.10.-14.10.1582
  if (y>1582 || (y==1582 && m>10) || (y==1582 && m==10 && d>14)) greg=1;
  if (greg)  // gregorianisch
  {
    if ((y % 4==0 && y % 100!=0) || y % 400==0) s=1;
    if (d<1 || d>mtag[m]+s*(m==2)) return(0);
    if (m < 3) { m += 13; y--; } else m++;
    s = d + 26*m/10 + y + y/4 - y/100 + y/400 + 6;
  }
  else       // julianisch
  { 
    if (y % 4==0) s=1;
    if (d<1 || d>mtag[m]+s*(m==2)) return(0);
    if (m < 3) { m += 13; y--; } else m++;
    s = d + 26*m/10 + y + y/4 + 4;
  }
  return(s % 7 + 1);
}

Der Oktober 1582 sieht dann so aus:

    Mo  Di   Mi   Do   Fr   Sa   So
      1     2     3     4   15   16   17
    18   19   20   21   22   23   24
    25   26   27   28   29   30   31

Noch ein Hinweis:

Ich wurde auch schon wegen dieser Formulierung gefragt:

    if (d<1 || d>mtag[m]+s*(m==2)) return(0);

Natürlich könnte man diese abändern in (und die Variable s einsparen)

    if ((y % 4==0 && y % 100!=0) || y % 400==0) mtag[2]=29;
    if (d<1 || d>mtag[m]) return(0);

Diese Methode ist für C-Einsteiger wahrscheinlich am leichtesten nachvollziehbar.

Es gibt halt immer mehrere Möglichkeiten ein Ziel zu erreichen.

Der Ausdruck   s*(m==2)   ist immer dann 1 (und symbolisiert den Schalttag im Februar) wenn
ein Schaltjahr vorliegt (s also 1 ist) UND m 2 ist (Februar), dann ergibt m==2 (man beachte das doppelte =) den Wert 1, weil die Bedingung erfüllt, also wahr ist.

Sonst ist immer mindestens einer der beiden Multiplikatoren 0 und damit das Ergebnis ebenso 0 und hat dann keine Auswirkung auf mtag[m].

Und hier die Luxusversion:

int dayofweekjulandgreg(int d, int m, int y, int *sch, int *greg)
{
  int mtag[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};

  /* nur wenn automatisch korrigiert werden soll, falls Tag und Jahr vertauscht sind
  int c;
  if (d>y) {c=d; d=y; y=c;}
  */

  *sch=0; *greg=0;
  if (y<100 || y>10000) return(-1);
  if (y==1582 && m==10 && d>4 && d<15) {*greg=-1; return(0);} // verbotener (nicht existenter) Bereich 5.10.-14.10.1582
  if (y>1582 || (y==1582 && m>10) || (y==1582 && m==10 && d>14)) *greg=1;
  if (*greg)  // gregorianisch
  {
    if ((y % 4==0 && y % 100!=0) || y % 400==0) {*sch=1;  mtag[2]=29;}
    if (m<1 || m>12) return(0);
    if (d<1 || d>mtag[m]) return(0);
    if (m < 3) { m += 13; y--; } else m++;
    s = d + 26*m/10 + y + y/4 - y/100 + y/400 + 6;
  }
  else  // julianisch
  { 
    if (y % 4==0) {*sch=1; mtag[2]=29;}
    if (m<1 || m>12) return(0);
    if (d<1 || d>mtag[m]) return(0);
    if (m < 3) { m += 13; y--; } else m++;
    s = d + 26*m/10 + y + y/4 + 4;
  }
  return(s % 7 + 1);
}

Aufruf mit

wo=dayofweekjulandgreg(tag, monat, jahr, &sch, &greg);  // alle Variablen Integer

Ergebnis:

wo:    1 = Sonntag, 2 = Montag, ... , 7 = Samstag, 0 = ungültiger Tag, -1 = Jahr außerhalb des Bereiches
sch:    1 = Schaltjahr, 0 = kein Schaltjahr
greg:   1 = gregorianisch, 0 = julianisch, -1 = verbotener Bereich

Es wergen Zeiger verwendet, weil eine Funktion in C nur einen Wert zurückgeben kann.
Man beachte auch, dass zuerst auf das Schaltjahr geprüft wird und dann erst das Monat, damit auch bei einem ungültigem Monat wenigstens zurückgegeben wird, ob das Jahr ein Schaltjahr ist.

Folgende Erweiterung kann man noch einbauen wenn man möchte:
(gilt auch für alle vorhergehenden Funktionen)

  int c;
  if (d>y) {c=d; d=y; y=c;}

In diesem Fall ist es egal, ob man dayofweek(tag, monat, jahr); oder dayofweek(jahr, monat, tag); schreibt. Man braucht dann beim Programmieren nicht daran denken ob man den Tag oder das Jahr zuerst aufruft. Das Monat ist sowieso in der Mitte. Der Wert für den Tag ist sowieso kleiner als der Wert für das Jahr, das mind. dreistellig sein sollte; also 100 - 9999. Ist der Wert für den Tag plötzlich größer als der Wert für das Jahr, dann sind beide offensichtlich vertauscht worden und es werden die beiden Werte wieder zurückgetauscht.

Da ein direkter Tausch nicht möglich ist, benötigt man die Variable c als Zwischenspeicher. Zuerst wird der Wert von d in c abgelegt und d übernimmt dann direkt den Wert von y. Dann kann y den Wert von c und somit den ursprünglichen Wert von d übernehmen.


Verbesserungen an den Funktionen

Mit zunehmenden Programmierkenntnissen versucht man auch bereits vorhandene Funktionen zu verbessern. Die Funktion "dayofweek" wurde so zusammengekürzt:

int dayofweek(int d, int m, int y)

{
  int s = m % 2; if (m==2) s-=2; if (m>7) s=1-s;
  if (m==2 && !(y % 4)) {s++; if (y>1582 && !(y % 100) && y % 400) s--;}
  if (y<1 || y>10000 || m<1 || m>12 || d<1 || d>s+30 ||
     (y==1582 && m==10 && d>4 && d<15)) return(0);
  if (m++ < 3) {m += 12; y--;}
  s = d + 26*m/10 + y + y/4 + 4;  // julianisch
  if (y>1582 || (y==1582 && m>11) || (y==1582 && m==11 && d>14))
     s += y/400 - y/100 + 2;      // gregorianisch
  return(s % 7 + 1);
}

Der erste Schritt war hier, die Anzahl der Tage eines Monats nicht mehr aus einer Tabelle zu entnehmen, sondern zu berechnen. Bis Juli sind alle ungeraden Monate lang und alle geraden Monate kurz, ab August ist es umgekehrt. Der Februar braucht sowieso eine eigene Behandlung mit Schaltjahresermittlung.

if (m < 3) { m += 13; y--; } else m++;
if (m++ < 3) {m += 12; y--;}  // gekürzte Version

ist sowohl bei Julianisch als auch Gregorianisch gleich und kann außerdem noch gekürzt werden.
Weiters wird zuerst der Wochentag so berechnet, wie wenn es nur einen Julianischen Kalender gäbe und dann erst auf Gregorianisch angepasst:

if (greg)
  s = d + 26*m/10 + y + y/4 - y/100 + y/400 + 6;

else
  s = d + 26*m/10 + y + y/4 + 4;

wird zu

  s = d + 26*m/10 + y + y/4 + 4;  // julianisch
  if (y>1582 || (y==1582 && m>11) || (y==1582 && m==11 && d>14))
     s += y/400 - y/100 + 2;      // gregorianisch

wobei man bei der Erkennung, ob es sich um ein Gregorianisches Datum handelt aufpassen muss, dass das Monat bereits um 1 von 10 auf 11 erhöht worden ist!

Und hier die Funktion, die den letzten Tag (also auch die Anzahl der Tage) eines Monats ermittelt:

int lastdayofmonth(int m, int y)

{
  int s = m % 2; if (m>7) s=1-s;
  if (m==2) {s-=2; if (!(y % 4)) {s++;
    if (y>1582 && !(y % 100) && y % 400) s--;}}
  if (m<1 || m>12 || y<-32000 || y>32000)
    return(0); else return(s+30);
}

Diese Funktion wird zum Herzstück für einige folgende Funktionen werden. Zum Beispiel für diese beiden:

int checkschaltjahr(int y)

{
  return(lastdayofmonth(2, y)-28);
} // 1 wenn Schaltjahr, sonst 0

int dayofyear(int d, int m, int y)

{
  int i,s,mtag[13];
  for (i=1; i<13; i++)
  {
    s = i % 2; if (i==2) s-=2;
    if (i>7) s=1-s; mtag[i]=s+30;
  }
  mtag[2]+=checkschaltjahr(y);
  if (d<1 || d>lastdayofmonth(m, y)) return(0);
  if (y==1582) {mtag[10]=21; if (m==10 && d>4) d-=10;}
  mtag[0]=s=0; do {s += mtag[--m];} while (m);
  return(s+d);
}

Auch die Funktion weekofyear(), welche die Kalenderwoche liefert wurde verbessert:

void weekofyearhelp(int d, int m, int y, int *w, int *s)

{
  *w=((dayofweek(1,1,y)+1)%7)-3;
  *s=((dayofyear(d,m,y)-1+*w)/7)+1;
}

int weekofyear(int d, int m, int y)

{
  int s,w,x;
  if (y<1583 || d<1 || d>lastdayofmonth(m, y)) return(0);
  weekofyearhelp(d, m, y, &w, &s);
  // falls KW 1 checken ob nicht doch letzte KW Vorjahr
  if (s==1) {if (-w>=d) weekofyearhelp(31, 12, y-1, &x, &s);}
  // falls KW 53 checken ob nicht doch erste KW nächstes Jahr
  else if (s==53 && (w!=2 || !checkschaltjahr(y)) && w!=3) s=1;
  return(s);
}

Sieht etwas zusammengestaucht aus, liefert aber die gleichen Ergebnisse wie die Funktion weiter oben in diesem Artikel.

Im englischsprachigem Raum (Großbritannien und USA) wird die Kalenderwoche noch einem anderen Schema ermittelt. Dort beginnt die Woche mit Sonntag und der 1. Jänner ist immer in der Woche 1, egal an welchem Wochentag er beginnt. Somit kann da Jahr im Extremfall 54 Kalenderwochen haben, wenn es an einem Samstag beginnt und ein Schaltjahr ist (2028 ist so ein Jahr).

int weekofyearukus(int d, int m, int y)

{
  if (y<1583 || d<1 || d>lastdayofmonth(m, y)) return(0);
  return (dayofyear(d,m,y) + dayofweek(1,1,y)-2)/7 + 1;
}

Ein etwas "verrückter" Ansatz ist, den Ostersonntag als Basis für die Wochentagsberechnung eines Jahres zu verwenden. Da ja der Ostersonntag immer ein Sonntag ist, kann man ausrechnen, an welchen Tag des Jahres dieser fällt und an welchen Tag des Jahres der Tag des zu ermittelnden Wochentages. Aus der Differenz der beiden Werte kann der Wochentag abgeleitet werden:

int dayofweek(int d, int m, int y)

{
  int e,o=3,s,z;
  s=dayofyear(d, m, y);
  // verbotener Bereich 5.10. - 14.10.1582
  if (!s || (y==1582 && m==10 && d>4 && d<15)) return(0);
  e=eastersunday(y);
  if (e>31) {o++; e-=31;}
  z=dayofyear(e, o, y);
  return((s-z+350) % 7 + 1);
}

Hier überprüft dayofyear() gleichzeitig, ob das Datum gültig ist. Nur für den unzulässigen Bereich vom 5.10. - 14.10. 1582 ist eine eigene Prüfung erforderlich. Wichtig ist auch, dass zur Differenz zwischen dem Wert s (Tag des Jahres des zu suchenden Tages) und dem Wert z (Tag des Jahres des Ostersonntags) ein ausreichend hoher durch 7 ohne Rest teilbarer Wert addiert wird, damit die Modulo-Funktion für Tage vor dem Ostersonntag keine negativen Werte liefert. (Da der Ostersonntag max. der 25. April sein kann und dieser in einem Schaltjahr der Tag Nummer 31 + 29 + 31 + 25 = 116 wäre, ist die nächste ganz durch 7 teilbare Zahl 119 als kleinster zu addierender Wert ausreichend.)


Ermittlung des Wochentages von historischen Daten

Bisher wurde nur der Wochentag von Jahren n. Chr. ([n.] u.Z.) ermittelt. Um auch den Wochentag von Daten v. Chr. (v.u.Z.) braucht man spezielle Funktionen und Informationen. Zuerst wird angenommen, dass es den Julianischen Kalender in der bekannten Abfolge der Monate schon immer gegeben hat.Auf das Jahr 1 v.u.Z. folgt direkt das Jahr 1 n.u.Z., ein Jahr 0 gibt es nicht.

Eine gute Erklärung, dass es eigentlich kein Jahr 0 gibt ist: Eine Minute vor dem Fußballspiel wurde der letzte Schnee vom Spielfeld entfernt, eine Minute nach Beginn des Spiels fiel bereits das erste Tor.

Die Astronomen aber verwenden ein Jahr 0, daher gilt:

2 v.u.Z. = Astronomisches Jahr -1
1 v.u.Z. = Astronomisches Jahr 0
1 n.u.Z  = Astronomisches Jahr 1

Die Wochentagsberechnung beruht auf das (Modifizierte) Julianische Datum, das die Astronomen verwenden:

int gregorianisch(int y, int m, int d)

{
  int g=0;
  if (y>1582 || (y==1582 && m>10) || (y==1582 && m==10 && d>14)) g++; // g=1;
  return(g);
}

/*
 *  jmt2mjd liefert zum Datum das modifizierte Julianische Datum,
 *  also die Anzahl der Tage seit dem 17.11.1858.
 *  Dabei ist ajahr das astronomische Jahr.
 *  Ungültige Datumsangaben führen zu undefinierten Ergebnissen.
 *  Erlaubt sind Daten
 *  vom     1.3.-32768 = 1.03.32769 vC --> mjd = -12647395
 *  bis zum             31.12.32767 nC --> mjd =  11289324.
 */

long jmt2mjd(int ajahr, int monat, int tag)

{
  static int m[] = { 0, 428, 459, 122, 153, 183, 214,
                        244, 275, 306, 336, 367, 397 };
  int hjahr,s;
  hjahr=ajahr;
  if (monat<3) hjahr--;
  if (gregorianisch(ajahr, monat, tag))
    s = hjahr/400 - hjahr/100 + hjahr/4;
  else
    if (hjahr < 0L) s = -(-(hjahr+1))/4-1 - 2;
    else            s =  hjahr       /4   - 2;
  return(365L * (long) hjahr - 679004L + (long)(s + m[monat] + tag));
}

int lastdayofmonth(int m, int y)

{
  int s = m % 2;
  if (m>7) s=1-s;
  if (m==2) {s-=2; if (!(y % 4)) {s++;
    if (y>1582 && !(y % 100) && y % 400) s--;}}
  if (m<1 || m>12 || y<-32765 || y>32765)
    return(0);
  else
    return(s+30);
}

// 1 .. Sonntag, 2 .. Montag usw.
// 0 .. ungültiger Tag

int dayofweekmjd(int tag, int monat, int ajahr)

{
  long mjd = jmt2mjd(ajahr, monat, tag);
  if (tag<1 || tag>lastdayofmonth(monat, ajahr)) return(0);
  return (int) ((mjd+14000003L) % 7 + 1);
}

Beispiel: Julius Cäsar wurde am 15. März 44 v.u.Z. ermordet. Das war astronomisch der 15. März -43 und ergibt das Modifizierte Julianische Datum -694575. Addiert man dazu 2400001 erhält man das (Historische) Julianische Datum von 1705426. Dieses modulo 7 ergibt als Rest 2 + 1 dazu addiert = Mittwoch. In der Funktion wird durch Zusätze eine Modulo-Funktion von negativen Werten, welche auch wieder negative Zahlen ergibt, vermieden. Daher wird 14 Mio. zum Modifizierten Julianischem Datum addiert, damit es sicher positiv ist. Die 3 wird als Offset addiert, weil der 17. November 1858 ein Mittwoch war. Jetzt kann das Ergebnis wieder Modulo 7 genommen werden und 1 wird dazu addiert (weil die 0 ja für ein ungültiges Datum reserviert ist.)

Im Beispiel: -694575 + 14000003 = 13305428 modulo 7 = 3. Dazu noch 1 addiert = 4 also ein Mittwoch

Übrigens: Das (Modifizierte) Julianische Datum eignet sich auch dafür, Tagesdifferenzen zwischen zwei Tagen zu ermitteln. Einfach von jeden der beiden Tage das Julianische Datum ausrechnen und dann einfach die Differenz der beiden ermitteln.

Alle C-Beispielprogramme inkl. Sourcecode können hier heruntergeladen werden:

Neu: Auch als 32Bit-Windows-Programm namens "Osterdatum und Kalender"
und noch mehr Astronomie mit dem Programm "AstroFritz"
(sollte aber auch unter Linux mit wine funktionieren)

Franz Bachler (07.02.2008 / 28.11.2008 / 05.03.2009 / 18.02.2010 / 16.09.2010)

 zurück