Skip to content

Commit 79b8f94

Browse files
khellangtarekgh
authored andcommitted
Added ISOWeek to System.Globalization (dotnet#18456)
* Added ISOWeek to System.Globalization * Revert DateTime changes. Use constants from GregorianCalendar instead. * Add more comments * Also allow 7 as value for day of week * Add note about ISO week-numbering year parameters * Add note about allowing 7 for day of week
1 parent c485d3f commit 79b8f94

File tree

4 files changed

+175
-3
lines changed

4 files changed

+175
-3
lines changed

src/System.Private.CoreLib/Resources/Strings.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3706,4 +3706,7 @@
37063706
<data name="Arg_MustBeNullTerminatedString" xml:space="preserve">
37073707
<value>The string must be null-terminated.</value>
37083708
</data>
3709-
</root>
3709+
<data name="ArgumentOutOfRange_Week_ISO" xml:space="preserve">
3710+
<value>The week parameter must be in the range 1 through 53.</value>
3711+
</data>
3712+
</root>

src/System.Private.CoreLib/shared/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\HijriCalendar.cs" />
175175
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\IdnMapping.cs" />
176176
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\InternalGlobalizationHelper.cs" />
177+
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\ISOWeek.cs" />
177178
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\JapaneseCalendar.cs" />
178179
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\JapaneseLunisolarCalendar.cs" />
179180
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\JulianCalendar.cs" />

src/System.Private.CoreLib/shared/System/Globalization/GregorianCalendar.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ public class GregorianCalendar : Calendar
2020
public const int ADEra = 1;
2121

2222
//
23-
// This is the max Gregorian year can be represented by DateTime class. The limitation
24-
// is derived from DateTime class.
23+
// This is the min Gregorian year can be represented by the DateTime class.
24+
// The limitation is derived from the DateTime class.
25+
//
26+
internal const int MinYear = 1;
27+
28+
//
29+
// This is the max Gregorian year can be represented by the DateTime class.
30+
// The limitation is derived from the DateTime class.
2531
//
2632
internal const int MaxYear = 9999;
2733

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using static System.Globalization.GregorianCalendar;
6+
7+
namespace System.Globalization
8+
{
9+
public static class ISOWeek
10+
{
11+
private const int WeeksInLongYear = 53;
12+
private const int WeeksInShortYear = 52;
13+
14+
private const int MinWeek = 1;
15+
private const int MaxWeek = WeeksInLongYear;
16+
17+
public static int GetWeekOfYear(DateTime date)
18+
{
19+
int week = GetWeekNumber(date);
20+
21+
if (week < MinWeek)
22+
{
23+
// If the week number obtained equals 0, it means that the
24+
// given date belongs to the preceding (week-based) year.
25+
return GetWeeksInYear(date.Year - 1);
26+
}
27+
28+
if (week > GetWeeksInYear(date.Year))
29+
{
30+
// If a week number of 53 is obtained, one must check that
31+
// the date is not actually in week 1 of the following year.
32+
return MinWeek;
33+
}
34+
35+
return week;
36+
}
37+
38+
public static int GetYear(DateTime date)
39+
{
40+
int week = GetWeekNumber(date);
41+
42+
if (week < MinWeek)
43+
{
44+
// If the week number obtained equals 0, it means that the
45+
// given date belongs to the preceding (week-based) year.
46+
return date.Year - 1;
47+
}
48+
49+
if (week > GetWeeksInYear(date.Year))
50+
{
51+
// If a week number of 53 is obtained, one must check that
52+
// the date is not actually in week 1 of the following year.
53+
return date.Year + 1;
54+
}
55+
56+
return date.Year;
57+
}
58+
59+
// The year parameter represents an ISO week-numbering year (also called ISO year informally).
60+
// Each week's year is the Gregorian year in which the Thursday falls.
61+
// The first week of the year, hence, always contains 4 January.
62+
// ISO week year numbering therefore slightly deviates from the Gregorian for some days close to 1 January.
63+
public static DateTime GetYearStart(int year)
64+
{
65+
return ToDateTime(year, MinWeek, DayOfWeek.Monday);
66+
}
67+
68+
// The year parameter represents an ISO week-numbering year (also called ISO year informally).
69+
// Each week's year is the Gregorian year in which the Thursday falls.
70+
// The first week of the year, hence, always contains 4 January.
71+
// ISO week year numbering therefore slightly deviates from the Gregorian for some days close to 1 January.
72+
public static DateTime GetYearEnd(int year)
73+
{
74+
return ToDateTime(year, GetWeeksInYear(year), DayOfWeek.Sunday);
75+
}
76+
77+
// From https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year:
78+
//
79+
// The long years, with 53 weeks in them, can be described by any of the following equivalent definitions:
80+
//
81+
// - Any year starting on Thursday and any leap year starting on Wednesday.
82+
// - Any year ending on Thursday and any leap year ending on Friday.
83+
// - Years in which 1 January and 31 December (in common years) or either (in leap years) are Thursdays.
84+
//
85+
// All other week-numbering years are short years and have 52 weeks.
86+
public static int GetWeeksInYear(int year)
87+
{
88+
if (year < MinYear || year > MaxYear)
89+
{
90+
throw new ArgumentOutOfRangeException(nameof(year), SR.ArgumentOutOfRange_Year);
91+
}
92+
93+
int P(int y) => (y + (y / 4) - (y / 100) + (y / 400)) % 7;
94+
95+
if (P(year) == 4 || P(year - 1) == 3)
96+
{
97+
return WeeksInLongYear;
98+
}
99+
100+
return WeeksInShortYear;
101+
}
102+
103+
// From https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year,_week_number_and_weekday:
104+
//
105+
// This method requires that one know the weekday of 4 January of the year in question.
106+
// Add 3 to the number of this weekday, giving a correction to be used for dates within this year.
107+
//
108+
// Multiply the week number by 7, then add the weekday. From this sum subtract the correction for the year.
109+
// The result is the ordinal date, which can be converted into a calendar date.
110+
//
111+
// If the ordinal date thus obtained is zero or negative, the date belongs to the previous calendar year.
112+
// If greater than the number of days in the year, to the following year.
113+
public static DateTime ToDateTime(int year, int week, DayOfWeek dayOfWeek)
114+
{
115+
if (year < MinYear || year > MaxYear)
116+
{
117+
throw new ArgumentOutOfRangeException(nameof(year), SR.ArgumentOutOfRange_Year);
118+
}
119+
120+
if (week < MinWeek || week > MaxWeek)
121+
{
122+
throw new ArgumentOutOfRangeException(nameof(week), SR.ArgumentOutOfRange_Week_ISO);
123+
}
124+
125+
// We allow 7 for convenience in cases where a user already has a valid ISO
126+
// day of week value for Sunday. This means that both 0 and 7 will map to Sunday.
127+
// The GetWeekday method will normalize this into the 1-7 range required by ISO.
128+
if ((int)dayOfWeek < 0 || (int)dayOfWeek > 7)
129+
{
130+
throw new ArgumentOutOfRangeException(nameof(dayOfWeek), SR.ArgumentOutOfRange_DayOfWeek);
131+
}
132+
133+
var jan4 = new DateTime(year, month: 1, day: 4);
134+
135+
int correction = GetWeekday(jan4.DayOfWeek) + 3;
136+
137+
int ordinal = (week * 7) + GetWeekday(dayOfWeek) - correction;
138+
139+
return new DateTime(year, month: 1, day: 1).AddDays(ordinal - 1);
140+
}
141+
142+
// From https://en.wikipedia.org/wiki/ISO_week_date#Calculating_the_week_number_of_a_given_date:
143+
//
144+
// Using ISO weekday numbers (running from 1 for Monday to 7 for Sunday),
145+
// subtract the weekday from the ordinal date, then add 10. Divide the result by 7.
146+
// Ignore the remainder; the quotient equals the week number.
147+
//
148+
// If the week number thus obtained equals 0, it means that the given date belongs to the preceding (week-based) year.
149+
// If a week number of 53 is obtained, one must check that the date is not actually in week 1 of the following year.
150+
private static int GetWeekNumber(DateTime date)
151+
{
152+
return (date.DayOfYear - GetWeekday(date.DayOfWeek) + 10) / 7;
153+
}
154+
155+
// Day of week in ISO is represented by an integer from 1 through 7, beginning with Monday and ending with Sunday.
156+
// This matches the underlying values of the DayOfWeek enum, except for Sunday, which needs to be converted.
157+
private static int GetWeekday(DayOfWeek dayOfWeek)
158+
{
159+
return dayOfWeek == DayOfWeek.Sunday ? 7 : (int) dayOfWeek;
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)