ISOWeek.cs 6.9 KB

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