17. Apr 2023
Lesedauer 6 Min.
Genau genommen
Kalenderwoche in .NET
In vielen Branchen läuft die Planung hauptsächlich über die Kalenderwoche. Doch wie berechnet man sie richtig?

Die Kalenderwoche (KW) ist eine Woche, die mit dem Montag beginnt und mit dem Sonntag endet. Das Jahr hat insgesamt meist 52, nie weniger, aber manchmal auch 53 Kalenderwochen. Die nächsten Jahre mit 53 KW sind 2026, 2032, 2037, 2043, 2048 und 2054. Klingt kompliziert – und ist es irgendwie auch. Sehen wir uns die Definition an.Die erste Kalenderwoche eines Jahres ist die Woche, die mindestens vier Tage des neuen Jahres beinhaltet. Fällt also beispielsweise der 1. Januar auf einen Dienstag, beginnt die erste Kalenderwoche mit Montag, dem 31.12., da diese Woche sechs Tage des neuen Jahres enthält (Dienstag, Mittwoch, Donnerstag, Freitag, Samstag und Sonntag).Fällt der 1. Januar hingegen auf einen Freitag, dann beginnt die erste Kalenderwoche des neuen Jahres mit Montag, dem 4. Januar, da die Vorwoche nur drei Tage des neuen Jahres enthält (Freitag, Samstag, Sonntag). Geregelt ist das Ganze in der ISO 8601, gut aufbereitet ist es in der deutschen Wikipedia unter [1].Wichtig zu wissen ist außerdem, dass die Definitionen von Kalenderwochen je nach Kulturkreis unterschiedlich sind, beispielsweise gelten in Nordamerika und Australien andere Regeln. Insbesondere gilt dort der Sonntag als der erste Tag der Woche.Wie berechnet man die Kalenderwoche zu einem Datum mit .NET? Sucht man danach im Netz, findet man schnell eine Lösung über die .NET-Funktion GetWeekOfYear, die sich in der Klasse System.Globalization.CultureInfo.CurrentCulture.Calendar befindet und die oben erwähnte Vier-Tage-Regel (hier: CalendarWeekRule.FirstFourDayWeek) benötigt sowie die Info, dass hierzulande der Montag als der erste Tag der Woche gilt. Das sieht dann so aus:
Public Function Kalenderwoche(
ByVal d As Date) As Integer
Dim culture = System.Globalization.CultureInfo.
CurrentCulture
Return culture.Calendar.GetWeekOfYear(
d, System.Globalization.
CalendarWeekRule.FirstFourDayWeek,
DayOfWeek.Monday)
End Function
Doch halt: Wäre es nicht fatal, die Rechenergebnisse seines Programms von der auf dem Rechner eingestellten Culture abhängig zu machen? Man stelle sich vor, dieselbe Anwendung würde auf dem Rechner eines deutschen Angestellten andere Ergebnisse liefern als auf dem Rechner des amerikanischen Firmenchefs. Das will niemand, eine Abhängigkeit von CurrentCulture sollte tabu sein, auch wenn in diesem Beispiel wegen der Vorgaben FirstFourDayWeek und Monday keine anderen Ergebnisse berechnet werden.Noch einen weiteren Grund gibt es, die .NET-Funktion Calender.GetWeekOfYear nicht zu verwenden: Ein kurzer Test mit Vergleichsdaten von der Webseite aktuelle-kalenderwoche.org zeigt, dass Microsofts Algorithmus zumindest beim Berechnen der hiesigen Kalenderwochenzählung Fehler aufweist (getestet unter .NET 6): Immer dann nämlich, wenn das Jahresende in die erste Kalenderwoche des Folgejahres fällt, meldet er für den 31.12. fälschlicherweise die KW 53.Die Fehler betreffen allerdings nur die „zwischen den Jahren“ genannten Tage, genauer die Tage vom 29.12. bis einschließlich dem 31.12. Viele können mit diesem Fehler also durchaus leben.
ISOWeek oder externe Bibliotheken
Eine mögliche Lösung bieten spezielle Date-Time-Bibliotheken. Infrage kommen beispielsweise die Bibliotheken Noda Time [2] oder TimePeriod [3], welche kostenfrei zu haben sind und jeweils zusätzliche Möglichkeiten eröffnen. Aber wer intensiver sucht (oder diesen Text liest ;-), der findet auch in der .NET-Dokumentation eine Lösung.Seit .NET Core 3 und seither in allen Versionen der Core-Schiene, also auch in .NET 5, 6 und 7, gibt es die Klasse ISOWeek im Namensraum System.Globalization [4]. Diese Klasse gehört zum .NET Standard 2.1 und stellt die folgenden Methoden zur Verfügung, welche das ISO-Wochendatumssystem unterstützen und richtig rechnen:- GetWeekOfYear liefert die Kalenderwoche zu einem übergebenen Datum.
- GetWeeksInYear berechnet die Anzahl der Wochen eines Jahres gemäß der ISO-Wochennummerierung.
- GetYear berechnet das Jahr gemäß der ISO-Wochennummerierung (informell auch als ISO-Jahr bezeichnet) für das übergebene Datum.
- GetYearStart berechnet das Datum, an dem das ISO-Jahr beginnt.
- GetYearEnd berechnet das Datum, an dem das ISO-Jahr endet.
- ToDateTime ordnet das ISO-Wochendatum in Form eines angegebenen ISO-Jahres, der Wochenzahl und des Wochentags dem entsprechenden Datum zu.
Public Function MS_ISO_Kalenderwoche(
ByVal d As Date) As String
Return System.Globalization.ISOWeek.
GetWeekOfYear(d).ToString.PadLeft(2, "0") + "." +
System.Globalization.ISOWeek.GetYear(d).ToString
End Function
Ein Versuch, die Methode durch Umstellen der CurrentCulture (war de-DE) auf us-US zu irritieren, war übrigens erfolglos; die Ergebnisse blieben dieselben. Offenbar besteht keine Abhängigkeit von der eingestellten Culture.
Selbst gebaute Funktion
Selten werden Artikel geschrieben, weil der Autor alles schon vorher weiß. So auch hier. Am Anfang stand das Entdecken des Fehlers, es folgte ein (erfolgreicher) Korrekturversuch, nur damit der Ehrgeiz geweckt wurde, eine eigene Funktion Kalenderwoche zu schreiben. Erst nachdem diese fertig war, entdeckte ich die ISOWeek-Funktion. Für alle, die es interessiert, hier eine Kurzfassung der selbst gebauten Version einer Kalenderwochen-Methode.Im Rückblick war das gar nicht so schwer, wenn man die zwischenzeitlichen Irrwege ausblendet. Die hier gezeigte Lösung setzt auf folgende Erkenntnisse [1]:- Eine Kalenderwoche hat sieben Tage und beginnt mit einem Montag.
- Jedes Jahr hat 52 oder 53 Kalenderwochen.
- Der 29., 30. und 31. Dezember können schon zur Kalenderwoche 1 des Folgejahres gehören.
- Der 1., 2. und 3. Januar können noch zur letzten Kalenderwoche des Vorjahres gehören.
- Der 4. Januar liegt immer in Kalenderwoche 1.
- Jahre, die mit einem Donnerstag beginnen oder enden, haben 53 Kalenderwochen.
Listing 1: Kalenderwoche ermitteln, Abschnitt 1
Public Function Kalenderwoche(
ByVal d As Date) As String
Dim erg As String = ""
Dim kwJahr As Integer = d.Year
' Testen für d.Year, d.Year-1 und d.Year + 1
' ob der gesuchte Tag darin liegt.
If d.DateIsBetween(StartTagKW1(d.Year),
EndTagKW5x(d.Year)) Then
' Das Datum gehört in eine KW im Jahr d.Year
kwJahr = d.Year
ElseIf d.DateIsBetween(StartTagKW1(d.Year - 1),
EndTagKW5x(d.Year - 1)) Then
' Das Datum gehört in eine KW im Jahr d.Year - 1
kwJahr = d.Year - 1
ElseIf d.DateIsBetween(StartTagKW1(d.Year + 1),
EndTagKW5x(d.Year + 1)) Then
' Das Datum gehört in eine KW im Jahr d.Year + 1
kwJahr = d.Year + 1
End If
DateIsBetween ist dabei eine Erweiterungsfunktion, die schlicht prüft, ob ein Datum in der übergebenen Zeitspanne liegt.StartTagKW1(Jahr) ermittelt den ersten Tag der ersten Kalenderwoche des Jahres. Dazu werden die Funktionen MoDerWoche sowie DayNrOfWeek_DE benutzt.MoDerWoche ermittelt den Montag der Woche, in welcher das übergebene Datum liegt, und DayNrOfWeek_DE sorgt dafür, dass der Sonntag nicht als Tag 0, sondern als Tag 7 behandelt wird.EndTagKW5x ermittelt den letzten Tag des Kalenderwochenrasters zum übergebenen Jahr. 2023 ist das der 31.12.2023, im Folgejahr 2024 dann der 29.12.2024.
Private Function StartTagKW1(
ByVal Jahr As Integer) As Date
Return MoDerWoche(CDate("04.01." + Jahr.ToString))
End Function
Private Function MoDerWoche(ByVal d As Date) As Date
Return d.AddDays(-(DayNrOfWeek_DE(d) - 1))
End Function
Private Function DayNrOfWeek_DE(
ByVal d As Date) As Integer
Dim dow As Integer = d.DayOfWeek
If dow = 0 Then dow = 7 ‚ Sonntag: US=0, DE=7
Return dow
End Function
Private Function EndTagKW5x(
ByVal Jahr As Integer) As Date
Return StartTagKW1(Jahr + 1).AddDays(-1)
End Function
Damit steht jetzt fest, welches Kalenderwochenraster für die Auswertung zu benutzen ist. Die Auswertung ermittelt die laufende Nummer des übergebenen Tages im Jahr, korrigiert diesen Wert um die Zahl der Tage, die aus dem Vor- beziehungsweise Folgejahr zu berücksichtigen sind, teilt diesen Wert, weil jede Woche sieben Tage hat, einfach durch sieben, rundet gegebenenfalls auf, baut den Ergebnisstring zusammen und schickt ihn an den Aufrufer, siehe Listing 2.
Listing 2: Kalenderwoche ermitteln, Abschnitt 2
Public Function Kalenderwoche(
ByVal d As Date) As String
Dim erg As String = ""
Dim kwJahr As Integer = d.Year
' hierher gehört der Code aus Listing 1
If kwJahr = d.Year Then
Dim TagNr = d.DayOfYear +
dayOfYearKorrektur(d.Year)
Dim kw As Integer
' Lässt sich die TagNr nicht ohne Rest
' durch 7 teilen, liegt der Tag in der
' nächsten KW, also + 1
If (TagNr Mod 7) = 0 Then
kw = TagNr / 7
Else
kw = Math.Truncate(TagNr / 7) + 1
End If
erg = kw.ToString.PadLeft(2, "0") +
"." + d.Year.ToString
ElseIf kwJahr = d.Year - 1 Then
' KW ist die letzte KW des Vorjahres
erg = AnzKw(d.Year - 1).ToString +
"." + (d.Year - 1).ToString
ElseIf kwJahr = d.Year + 1 Then
' KW ist die erste KW des Folgejahres
erg = "01." + (d.Year + 1).ToString
End If
Return erg
End Function
Ganz einfach ist die Zuordnung, wenn das Datum im Vor- oder Folgejahr liegt, dann ist die gesuchte Woche nämlich die letzte KW des Vorjahres (die durch die Donnerstag-Regel ermittelt wird, siehe oben) oder die KW 1 des Folgejahres.
Private Function AnzKw(
ByVal Jahr As Integer) As Integer
If CDate("01.01." + Jahr.ToString).DayOfWeek = 4 Or
CDate("31.12." + Jahr.ToString).DayOfWeek = 4 Then
Return 53 Else Return 52
End Function
Aufwendiger ist die Funktion dayOfYearKorrektur – ich denke, dass das noch einfacher gehen muss, aber derzeit sieht die Funktion, welche die Zahl der Tage aus Vor- oder Folgejahr ermittelt, die hinzugefügt beziehungsweise abgezogen werden müssen, so aus wie in Listing 3.
Listing 3: Korrekturwerte für dayOfYear ermitteln
Private Function dayOfYearKorrektur(
ByVal Jahr As Integer) As Integer
Dim erg As Integer = 0
Dim UltimoVJ As Date = CDate("31.12." +
(Jahr - 1).ToString)
Dim d0 As Date = StartTagKW1(Jahr)
Dim d1 As Date = CDate("01.01." + Jahr.ToString)
Dim diff As Integer = 0
If d0 <> d1 Then
If d0 <= UltimoVJ Then
' die ersten Tage von KW 1 (max 3) gehören
' zum Vorjahr, ermitteln wie viele es sind
Dim dx As Date = d0
For i = 1 To 3
dx = dx.AddDays(1)
If dx = d1 Then
erg = i
Exit For
End If
Next
Else
' KW1 startet mit dem 1., 2., 3. oder
' dem 4. Januar
Dim dx As Date = d1 ' 1. Januar
For i = 1 To 3
dx = dx.AddDays(1)
If dx = d0 Then
erg = -i
Exit For
End If
Next
End If
End If
Return erg
End Function
In Bild 1 finden Sie die Ergebnisse eines Testlaufs für alle drei Methoden, der unter anderem ein paar der kritischen Datumswerte umfasst. Im Bild rot hervorgehoben sind die Fehler der Methode CurrentCulture.Calendar.GetWeekOfYear beim Berechnen der Kalenderwoche für den 31.12. der Jahre 2024, 2025, 2031 und 2036.

Die .NET-FunktionCurrentCulture.Calendar.GetWeekOfYear liefert teilweise falsche Ergebnisse(Bild 1)
Autor
Fazit
Mit der Berechnung der europäischen Kalenderwochen gibt sich die in .NET verbaute Funktion CurrentCulture.Calendar.GetWeekOfYear wenig Mühe. Außerdem sollte man die Rechenergebnisse seines Programms nicht von den Culture-Einstellungen des Rechners abhängig machen. Wer korrekte Angaben auch für Datumswerte „zwischen den Jahren“ haben möchte, kann die zum .NET Standard 2.1 gehörende Methode ISOWeek.GetWeekOfYear aus dem Namensraum System.Globalisation nutzen, sich eine eigene Routine dafür bauen oder auf eine Date-Time-Bibliothek zurückgreifen.Fussnoten
- Wikipedia zur ISO 6801 sowie zur Kalenderwoche, http://www.dotnetpro.de/SL2305Kalenderwoche1
- Noda Time, https://nodatime.org
- TimePeriod, http://www.dotnetpro.de/SL2305Kalenderwoche2
- ISOWeek in der .NET-Dokumentation, http://www.dotnetpro.de/SL2305Kalenderwoche3