|
| 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