Added ISOWeek to System.Globalization (dotnet/coreclr#18456)
authorKristian Hellang <kristian@hellang.com>
Wed, 13 Jun 2018 23:28:02 +0000 (01:28 +0200)
committerTarek Mahmoud Sayed <tarekms@microsoft.com>
Wed, 13 Jun 2018 23:28:02 +0000 (16:28 -0700)
* 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

Commit migrated from https://github.com/dotnet/coreclr/commit/79b8f94cc52b6b79945074b748508a0b3cbb8be6

src/coreclr/src/System.Private.CoreLib/Resources/Strings.resx
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
src/libraries/System.Private.CoreLib/src/System/Globalization/GregorianCalendar.cs
src/libraries/System.Private.CoreLib/src/System/Globalization/ISOWeek.cs [new file with mode: 0644]

index 7c208cf..cb3dc3f 100644 (file)
   <data name="Arg_MustBeNullTerminatedString" xml:space="preserve">
     <value>The string must be null-terminated.</value>
   </data>
-</root>
+  <data name="ArgumentOutOfRange_Week_ISO" xml:space="preserve">
+    <value>The week parameter must be in the range 1 through 53.</value>
+  </data>
+</root>
\ No newline at end of file
index 437f427..4bdcb24 100644 (file)
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\HijriCalendar.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\IdnMapping.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\InternalGlobalizationHelper.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\ISOWeek.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\JapaneseCalendar.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\JapaneseLunisolarCalendar.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\JulianCalendar.cs" />
index a018216..6b08ee5 100644 (file)
@@ -20,8 +20,14 @@ namespace System.Globalization
         public const int ADEra = 1;
 
         //
-        // This is the max Gregorian year can be represented by DateTime class.  The limitation
-        // is derived from DateTime class.
+        // This is the min Gregorian year can be represented by the DateTime class.
+        // The limitation is derived from the DateTime class.
+        //
+        internal const int MinYear = 1;
+
+        //
+        // This is the max Gregorian year can be represented by the DateTime class.
+        // The limitation is derived from the DateTime class.
         //
         internal const int MaxYear = 9999;
 
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/ISOWeek.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/ISOWeek.cs
new file mode 100644 (file)
index 0000000..3b3ba15
--- /dev/null
@@ -0,0 +1,162 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using static System.Globalization.GregorianCalendar;
+
+namespace System.Globalization
+{
+    public static class ISOWeek
+    {
+        private const int WeeksInLongYear = 53;
+        private const int WeeksInShortYear = 52;
+
+        private const int MinWeek = 1;
+        private const int MaxWeek = WeeksInLongYear;
+
+        public static int GetWeekOfYear(DateTime date)
+        {
+            int week = GetWeekNumber(date);
+
+            if (week < MinWeek)
+            {
+                // If the week number obtained equals 0, it means that the
+                // given date belongs to the preceding (week-based) year.
+                return GetWeeksInYear(date.Year - 1);
+            }
+
+            if (week > GetWeeksInYear(date.Year))
+            {
+                // If a week number of 53 is obtained, one must check that
+                // the date is not actually in week 1 of the following year.
+                return MinWeek;
+            }
+
+            return week;
+        }
+
+        public static int GetYear(DateTime date)
+        {
+            int week = GetWeekNumber(date);
+
+            if (week < MinWeek)
+            {
+                // If the week number obtained equals 0, it means that the
+                // given date belongs to the preceding (week-based) year.
+                return date.Year - 1;
+            }
+
+            if (week > GetWeeksInYear(date.Year))
+            {
+                // If a week number of 53 is obtained, one must check that
+                // the date is not actually in week 1 of the following year.
+                return date.Year + 1;
+            }
+
+            return date.Year;
+        }
+
+        // The year parameter represents an ISO week-numbering year (also called ISO year informally).
+        // Each week's year is the Gregorian year in which the Thursday falls.
+        // The first week of the year, hence, always contains 4 January.
+        // ISO week year numbering therefore slightly deviates from the Gregorian for some days close to 1 January.
+        public static DateTime GetYearStart(int year)
+        {
+            return ToDateTime(year, MinWeek, DayOfWeek.Monday);
+        }
+
+        // The year parameter represents an ISO week-numbering year (also called ISO year informally).
+        // Each week's year is the Gregorian year in which the Thursday falls.
+        // The first week of the year, hence, always contains 4 January.
+        // ISO week year numbering therefore slightly deviates from the Gregorian for some days close to 1 January.
+        public static DateTime GetYearEnd(int year)
+        {
+            return ToDateTime(year, GetWeeksInYear(year), DayOfWeek.Sunday);
+        }
+
+        // From https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year:
+        //
+        // The long years, with 53 weeks in them, can be described by any of the following equivalent definitions:
+        //
+        // - Any year starting on Thursday and any leap year starting on Wednesday.
+        // - Any year ending on Thursday and any leap year ending on Friday.
+        // - Years in which 1 January and 31 December (in common years) or either (in leap years) are Thursdays.
+        //
+        // All other week-numbering years are short years and have 52 weeks.
+        public static int GetWeeksInYear(int year)
+        {
+            if (year < MinYear || year > MaxYear)
+            {
+                throw new ArgumentOutOfRangeException(nameof(year), SR.ArgumentOutOfRange_Year);
+            }
+
+            int P(int y) => (y + (y / 4) - (y / 100) + (y / 400)) % 7;
+
+            if (P(year) == 4 || P(year - 1) == 3)
+            {
+                return WeeksInLongYear;
+            }
+
+            return WeeksInShortYear;
+        }
+
+        // From https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year,_week_number_and_weekday:
+        //
+        // This method requires that one know the weekday of 4 January of the year in question.
+        // Add 3 to the number of this weekday, giving a correction to be used for dates within this year.
+        //
+        // Multiply the week number by 7, then add the weekday. From this sum subtract the correction for the year.
+        // The result is the ordinal date, which can be converted into a calendar date.
+        //
+        // If the ordinal date thus obtained is zero or negative, the date belongs to the previous calendar year.
+        // If greater than the number of days in the year, to the following year.
+        public static DateTime ToDateTime(int year, int week, DayOfWeek dayOfWeek)
+        {
+            if (year < MinYear || year > MaxYear)
+            {
+                throw new ArgumentOutOfRangeException(nameof(year), SR.ArgumentOutOfRange_Year);
+            }
+
+            if (week < MinWeek || week > MaxWeek)
+            {
+                throw new ArgumentOutOfRangeException(nameof(week), SR.ArgumentOutOfRange_Week_ISO);
+            }
+
+            // We allow 7 for convenience in cases where a user already has a valid ISO
+            // day of week value for Sunday. This means that both 0 and 7 will map to Sunday.
+            // The GetWeekday method will normalize this into the 1-7 range required by ISO.
+            if ((int)dayOfWeek < 0 || (int)dayOfWeek > 7)
+            {
+                throw new ArgumentOutOfRangeException(nameof(dayOfWeek), SR.ArgumentOutOfRange_DayOfWeek);
+            }
+
+            var jan4 = new DateTime(year, month: 1, day: 4);
+
+            int correction = GetWeekday(jan4.DayOfWeek) + 3;
+
+            int ordinal = (week * 7) + GetWeekday(dayOfWeek) - correction;
+                
+            return new DateTime(year, month: 1, day: 1).AddDays(ordinal - 1);
+        }
+
+        // From https://en.wikipedia.org/wiki/ISO_week_date#Calculating_the_week_number_of_a_given_date:
+        //
+        // Using ISO weekday numbers (running from 1 for Monday to 7 for Sunday),
+        // subtract the weekday from the ordinal date, then add 10. Divide the result by 7.
+        // Ignore the remainder; the quotient equals the week number.
+        //
+        // If the week number thus obtained equals 0, it means that the given date belongs to the preceding (week-based) year.
+        // If a week number of 53 is obtained, one must check that the date is not actually in week 1 of the following year.
+        private static int GetWeekNumber(DateTime date)
+        {
+            return (date.DayOfYear - GetWeekday(date.DayOfWeek) + 10) / 7;
+        }
+
+        // Day of week in ISO is represented by an integer from 1 through 7, beginning with Monday and ending with Sunday.
+        // This matches the underlying values of the DayOfWeek enum, except for Sunday, which needs to be converted.
+        private static int GetWeekday(DayOfWeek dayOfWeek)
+        {
+            return dayOfWeek == DayOfWeek.Sunday ? 7 : (int) dayOfWeek;
+        }
+    }
+}