| | | 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 | | |
| | | 4 | | using System.Runtime.CompilerServices; |
| | | 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 | | { |
| | 0 | 19 | | int week = GetWeekNumber(date); |
| | | 20 | | |
| | 0 | 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. |
| | 0 | 25 | | return GetWeeksInYear(date.Year - 1); |
| | | 26 | | } |
| | | 27 | | |
| | 0 | 28 | | if (week > WeeksInShortYear && GetWeeksInYear(date.Year) == WeeksInShortYear) |
| | | 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. |
| | 0 | 32 | | return MinWeek; |
| | | 33 | | } |
| | | 34 | | |
| | 0 | 35 | | return week; |
| | | 36 | | } |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// Calculates the ISO week number of a given Gregorian date. |
| | | 40 | | /// </summary> |
| | | 41 | | /// <param name="date">A date in the Gregorian calendar.</param> |
| | | 42 | | /// <returns>A number between 1 and 53 representing the ISO week number of the given Gregorian date.</returns> |
| | 0 | 43 | | public static int GetWeekOfYear(DateOnly date) => GetWeekOfYear(date.GetEquivalentDateTime()); |
| | | 44 | | |
| | | 45 | | public static int GetYear(DateTime date) |
| | | 46 | | { |
| | 0 | 47 | | int week = GetWeekNumber(date); |
| | 0 | 48 | | int year = date.Year; |
| | | 49 | | |
| | 0 | 50 | | if (week < MinWeek) |
| | | 51 | | { |
| | | 52 | | // If the week number obtained equals 0, it means that the |
| | | 53 | | // given date belongs to the preceding (week-based) year. |
| | 0 | 54 | | year--; |
| | | 55 | | } |
| | 0 | 56 | | else if (week > WeeksInShortYear && GetWeeksInYear(year) == WeeksInShortYear) |
| | | 57 | | { |
| | | 58 | | // If a week number of 53 is obtained, one must check that |
| | | 59 | | // the date is not actually in week 1 of the following year. |
| | 0 | 60 | | year++; |
| | | 61 | | } |
| | | 62 | | |
| | 0 | 63 | | return year; |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | /// <summary> |
| | | 67 | | /// Calculates the ISO week-numbering year (also called ISO year informally) mapped to the input Gregorian date. |
| | | 68 | | /// </summary> |
| | | 69 | | /// <param name="date">A date in the Gregorian calendar.</param> |
| | | 70 | | /// <returns>The ISO week-numbering year, between 1 and 9999</returns> |
| | 0 | 71 | | public static int GetYear(DateOnly date) => GetYear(date.GetEquivalentDateTime()); |
| | | 72 | | |
| | | 73 | | // The year parameter represents an ISO week-numbering year (also called ISO year informally). |
| | | 74 | | // Each week's year is the Gregorian year in which the Thursday falls. |
| | | 75 | | // The first week of the year, hence, always contains 4 January. |
| | | 76 | | // ISO week year numbering therefore slightly deviates from the Gregorian for some days close to 1 January. |
| | | 77 | | public static DateTime GetYearStart(int year) |
| | | 78 | | { |
| | 0 | 79 | | return ToDateTime(year, MinWeek, DayOfWeek.Monday); |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | // The year parameter represents an ISO week-numbering year (also called ISO year informally). |
| | | 83 | | // Each week's year is the Gregorian year in which the Thursday falls. |
| | | 84 | | // The first week of the year, hence, always contains 4 January. |
| | | 85 | | // ISO week year numbering therefore slightly deviates from the Gregorian for some days close to 1 January. |
| | | 86 | | public static DateTime GetYearEnd(int year) |
| | | 87 | | { |
| | 0 | 88 | | return ToDateTime(year, GetWeeksInYear(year), DayOfWeek.Sunday); |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | // From https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year: |
| | | 92 | | // |
| | | 93 | | // The long years, with 53 weeks in them, can be described by any of the following equivalent definitions: |
| | | 94 | | // |
| | | 95 | | // - Any year starting on Thursday and any leap year starting on Wednesday. |
| | | 96 | | // - Any year ending on Thursday and any leap year ending on Friday. |
| | | 97 | | // - Years in which 1 January and 31 December (in common years) or either (in leap years) are Thursdays. |
| | | 98 | | // |
| | | 99 | | // All other week-numbering years are short years and have 52 weeks. |
| | | 100 | | public static int GetWeeksInYear(int year) |
| | | 101 | | { |
| | 0 | 102 | | if (year < MinYear || year > MaxYear) |
| | | 103 | | { |
| | 0 | 104 | | ThrowHelper.ThrowArgumentOutOfRange_Year(); |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 108 | | static uint P(uint y) |
| | | 109 | | { |
| | 0 | 110 | | uint cent = y / 100; |
| | 0 | 111 | | return (y + (y / 4) - cent + cent / 4) % 7; |
| | | 112 | | } |
| | | 113 | | |
| | 0 | 114 | | if (P((uint)year) == 4 || P((uint)year - 1) == 3) |
| | | 115 | | { |
| | 0 | 116 | | return WeeksInLongYear; |
| | | 117 | | } |
| | | 118 | | |
| | 0 | 119 | | return WeeksInShortYear; |
| | | 120 | | } |
| | | 121 | | |
| | | 122 | | // From https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year,_week_number_and_weekday: |
| | | 123 | | // |
| | | 124 | | // This method requires that one know the weekday of 4 January of the year in question. |
| | | 125 | | // Add 3 to the number of this weekday, giving a correction to be used for dates within this year. |
| | | 126 | | // |
| | | 127 | | // Multiply the week number by 7, then add the weekday. From this sum subtract the correction for the year. |
| | | 128 | | // The result is the ordinal date, which can be converted into a calendar date. |
| | | 129 | | // |
| | | 130 | | // If the ordinal date thus obtained is zero or negative, the date belongs to the previous calendar year. |
| | | 131 | | // If greater than the number of days in the year, to the following year. |
| | | 132 | | public static DateTime ToDateTime(int year, int week, DayOfWeek dayOfWeek) |
| | | 133 | | { |
| | 0 | 134 | | if (year < MinYear || year > MaxYear) |
| | | 135 | | { |
| | 0 | 136 | | ThrowHelper.ThrowArgumentOutOfRange_Year(); |
| | | 137 | | } |
| | | 138 | | |
| | 0 | 139 | | if (week < MinWeek || week > MaxWeek) |
| | | 140 | | { |
| | 0 | 141 | | throw new ArgumentOutOfRangeException(nameof(week), SR.ArgumentOutOfRange_Week_ISO); |
| | | 142 | | } |
| | | 143 | | |
| | | 144 | | // We allow 7 for convenience in cases where a user already has a valid ISO |
| | | 145 | | // day of week value for Sunday. This means that both 0 and 7 will map to Sunday. |
| | | 146 | | // The GetWeekday method will normalize this into the 1-7 range required by ISO. |
| | 0 | 147 | | if ((int)dayOfWeek < 0 || (int)dayOfWeek > 7) |
| | | 148 | | { |
| | 0 | 149 | | throw new ArgumentOutOfRangeException(nameof(dayOfWeek), SR.ArgumentOutOfRange_DayOfWeek); |
| | | 150 | | } |
| | | 151 | | |
| | 0 | 152 | | var jan4 = new DateTime(year, month: 1, day: 4); |
| | | 153 | | |
| | 0 | 154 | | int correction = GetWeekday(jan4.DayOfWeek) + 3; |
| | | 155 | | |
| | 0 | 156 | | int ordinal = (week * 7) + GetWeekday(dayOfWeek) - correction; |
| | | 157 | | |
| | 0 | 158 | | return jan4.AddTicks((ordinal - 4) * TimeSpan.TicksPerDay); |
| | | 159 | | } |
| | | 160 | | |
| | | 161 | | |
| | | 162 | | /// <summary> |
| | | 163 | | /// Maps the ISO week date represented by a specified ISO year, week number, and day of week to the equivalent G |
| | | 164 | | /// </summary> |
| | | 165 | | /// <param name="year">An ISO week-numbering year (also called an ISO year informally).</param> |
| | | 166 | | /// <param name="week">The ISO week number in the given ISO week-numbering year.</param> |
| | | 167 | | /// <param name="dayOfWeek">The day of week inside the given ISO week.</param> |
| | | 168 | | /// <returns>The Gregorian date equivalent to the input ISO week date.</returns> |
| | 0 | 169 | | public static DateOnly ToDateOnly(int year, int week, DayOfWeek dayOfWeek) => DateOnly.FromDateTime(ToDateTime(y |
| | | 170 | | |
| | | 171 | | // From https://en.wikipedia.org/wiki/ISO_week_date#Calculating_the_week_number_of_a_given_date: |
| | | 172 | | // |
| | | 173 | | // Using ISO weekday numbers (running from 1 for Monday to 7 for Sunday), |
| | | 174 | | // subtract the weekday from the ordinal date, then add 10. Divide the result by 7. |
| | | 175 | | // Ignore the remainder; the quotient equals the week number. |
| | | 176 | | // |
| | | 177 | | // If the week number thus obtained equals 0, it means that the given date belongs to the preceding (week-based) |
| | | 178 | | // If a week number of 53 is obtained, one must check that the date is not actually in week 1 of the following y |
| | | 179 | | private static int GetWeekNumber(DateTime date) |
| | | 180 | | { |
| | 0 | 181 | | return (int)((uint)(date.DayOfYear - GetWeekday(date.DayOfWeek) + 10) / 7); |
| | | 182 | | } |
| | | 183 | | |
| | | 184 | | // Day of week in ISO is represented by an integer from 1 through 7, beginning with Monday and ending with Sunda |
| | | 185 | | // This matches the underlying values of the DayOfWeek enum, except for Sunday, which needs to be converted. |
| | | 186 | | private static int GetWeekday(DayOfWeek dayOfWeek) |
| | | 187 | | { |
| | 0 | 188 | | return dayOfWeek == DayOfWeek.Sunday ? 7 : (int)dayOfWeek; |
| | | 189 | | } |
| | | 190 | | } |
| | | 191 | | } |
| | | 192 | | |