// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace System.Globalization { // Gregorian Calendars use Era Info internal sealed class EraInfo { internal int era; // The value of the era. internal long ticks; // The time in ticks when the era starts internal int yearOffset; // The offset to Gregorian year when the era starts. // Gregorian Year = Era Year + yearOffset // Era Year = Gregorian Year - yearOffset internal int minEraYear; // Min year value in this era. Generally, this value is 1, but this may // be affected by the DateTime.MinValue; internal int maxEraYear; // Max year value in this era. (== the year length of the era + 1) internal string? eraName; // The era name internal string? abbrevEraName; // Abbreviated Era Name internal string? englishEraName; // English era name internal EraInfo(int era, int startYear, int startMonth, int startDay, int yearOffset, int minEraYear, int maxEraYear) { this.era = era; this.yearOffset = yearOffset; this.minEraYear = minEraYear; this.maxEraYear = maxEraYear; this.ticks = new DateTime(startYear, startMonth, startDay).Ticks; } internal EraInfo(int era, int startYear, int startMonth, int startDay, int yearOffset, int minEraYear, int maxEraYear, string eraName, string abbrevEraName, string englishEraName) { this.era = era; this.yearOffset = yearOffset; this.minEraYear = minEraYear; this.maxEraYear = maxEraYear; // codeql[cs/leap-year/unsafe-date-construction-from-two-elements] - A DateTime object is created using values obtained from the machine configuration. this.ticks = new DateTime(startYear, startMonth, startDay).Ticks; this.eraName = eraName; this.abbrevEraName = abbrevEraName; this.englishEraName = englishEraName; } } // This calendar recognizes two era values: // 0 CurrentEra (AD) // 1 BeforeCurrentEra (BC) internal sealed class GregorianCalendarHelper { // // This is the max Gregorian year can be represented by DateTime class. The limitation // is derived from DateTime class. // internal int MaxYear => m_maxYear; private readonly int m_maxYear; private readonly int m_minYear; private readonly Calendar m_Cal; private readonly EraInfo[] m_EraInfo; private readonly long _minSupportedTicks; private readonly long _maxSupportedTicks; // Construct an instance of gregorian calendar. internal GregorianCalendarHelper(Calendar cal, EraInfo[] eraInfo) { m_Cal = cal; m_EraInfo = eraInfo; m_maxYear = eraInfo[0].maxEraYear; m_minYear = eraInfo[0].minEraYear; _minSupportedTicks = cal.MinSupportedDateTime.Ticks; _maxSupportedTicks = cal.MaxSupportedDateTime.Ticks; } // EraInfo.yearOffset: The offset to Gregorian year when the era starts. Gregorian Year = Era Year + yearOffset // Era Year = Gregorian Year - yearOffset // EraInfo.minEraYear: Min year value in this era. Generally, this value is 1, but this may be affected by the DateTime.MinValue; // EraInfo.maxEraYear: Max year value in this era. (== the year length of the era + 1) private int GetYearOffset(int year, int era, bool throwOnError) { if (year < 0) { if (throwOnError) { throw new ArgumentOutOfRangeException(nameof(year), SR.ArgumentOutOfRange_NeedNonNegNum); } return -1; } if (era == Calendar.CurrentEra) { era = m_Cal.CurrentEraValue; } var eras = m_EraInfo; for (int i = 0; i < eras.Length; i++) { EraInfo eraInfo = eras[i]; if (era == eraInfo.era) { if (year >= eraInfo.minEraYear) { if (year <= eraInfo.maxEraYear) { return eraInfo.yearOffset; } else if (!LocalAppContextSwitches.EnforceJapaneseEraYearRanges) { // If we got the year number exceeding the era max year number, this still possible be valid as the date can be created before // introducing new eras after the era we are checking. we'll loop on the eras after the era we have and ensure the year // can exist in one of these eras. otherwise, we'll throw. // Note, we always return the offset associated with the requested era. // // Here is some example: // if we are getting the era number 4 (Heisei) and getting the year number 32. if the era 4 has year range from 1 to 31 // then year 32 exceeded the range of era 4 and we'll try to find out if the years difference (32 - 31 = 1) would lay in // the subsequent eras (e.g era 5 and up) int remainingYears = year - eraInfo.maxEraYear; for (int j = i - 1; j >= 0; j--) { if (remainingYears <= eras[j].maxEraYear) { return eraInfo.yearOffset; } remainingYears -= eras[j].maxEraYear; } } } if (throwOnError) { throw new ArgumentOutOfRangeException( nameof(year), SR.Format( SR.ArgumentOutOfRange_Range, eraInfo.minEraYear, eraInfo.maxEraYear)); } break; // no need to iterate more on eras. } } if (throwOnError) { throw new ArgumentOutOfRangeException(nameof(era), SR.ArgumentOutOfRange_InvalidEraValue); } return -1; } /*=================================GetGregorianYear========================== **Action: Get the Gregorian year value for the specified year in an era. **Returns: The Gregorian year value. **Arguments: ** year the year value in Japanese calendar ** era the Japanese emperor era value. **Exceptions: ** ArgumentOutOfRangeException if year value is invalid or era value is invalid. ============================================================================*/ internal int GetGregorianYear(int year, int era) { return GetYearOffset(year, era, throwOnError: true) + year; } internal bool IsValidYear(int year, int era) { return GetYearOffset(year, era, throwOnError: false) >= 0; } internal void CheckTicksRange(long ticks) { if (ticks < _minSupportedTicks || ticks > _maxSupportedTicks) ThrowOutOfRange(); void ThrowOutOfRange() { throw new ArgumentOutOfRangeException( "time", SR.Format( CultureInfo.InvariantCulture, SR.ArgumentOutOfRange_CalendarRange, m_Cal.MinSupportedDateTime, m_Cal.MaxSupportedDateTime)); } } // Returns the DateTime resulting from adding the given number of // months to the specified DateTime. The result is computed by incrementing // (or decrementing) the year and month parts of the specified DateTime by // value months, and, if required, adjusting the day part of the // resulting date downwards to the last day of the resulting month in the // resulting year. The time-of-day part of the result is the same as the // time-of-day part of the specified DateTime. // // In more precise terms, considering the specified DateTime to be of the // form y / m / d + t, where y is the // year, m is the month, d is the day, and t is the // time-of-day, the result is y1 / m1 / d1 + t, // where y1 and m1 are computed by adding value months // to y and m, and d1 is the largest value less than // or equal to d that denotes a valid day in month m1 of year // y1. // public DateTime AddMonths(DateTime time, int months) { if (months < -120000 || months > 120000) { throw new ArgumentOutOfRangeException( nameof(months), SR.Format( SR.ArgumentOutOfRange_Range, -120000, 120000)); } CheckTicksRange(time.Ticks); time.GetDate(out int y, out int m, out int d); int i = m - 1 + months; if (i >= 0) { m = i % 12 + 1; y += i / 12; } else { m = 12 + (i + 1) % 12; y += (i - 11) / 12; } ReadOnlySpan<int> daysArray = (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? GregorianCalendar.DaysToMonth366 : GregorianCalendar.DaysToMonth365; int days = (daysArray[m] - daysArray[m - 1]); if (d > days) { d = days; } long ticks = GregorianCalendar.DateToTicks(y, m, d) + time.TimeOfDay.Ticks; Calendar.CheckAddResult(ticks, m_Cal.MinSupportedDateTime, m_Cal.MaxSupportedDateTime); return new DateTime(ticks); } // Returns the DateTime resulting from adding the given number of // years to the specified DateTime. The result is computed by incrementing // (or decrementing) the year part of the specified DateTime by value // years. If the month and day of the specified DateTime is 2/29, and if the // resulting year is not a leap year, the month and day of the resulting // DateTime becomes 2/28. Otherwise, the month, day, and time-of-day // parts of the result are the same as those of the specified DateTime. // public DateTime AddYears(DateTime time, int years) { return AddMonths(time, years * 12); } // Returns the day-of-month part of the specified DateTime. The returned // value is an integer between 1 and 31. // public int GetDayOfMonth(DateTime time) { CheckTicksRange(time.Ticks); return time.Day; } // Returns the day-of-week part of the specified DateTime. The returned value // is an integer between 0 and 6, where 0 indicates Sunday, 1 indicates // Monday, 2 indicates Tuesday, 3 indicates Wednesday, 4 indicates // Thursday, 5 indicates Friday, and 6 indicates Saturday. // public DayOfWeek GetDayOfWeek(DateTime time) { CheckTicksRange(time.Ticks); return time.DayOfWeek; } // Returns the day-of-year part of the specified DateTime. The returned value // is an integer between 1 and 366. // public int GetDayOfYear(DateTime time) { CheckTicksRange(time.Ticks); return time.DayOfYear; } // Returns the number of days in the month given by the year and // month arguments. // public int GetDaysInMonth(int year, int month, int era) { // // Convert year/era value to Gregorain year value. // year = GetGregorianYear(year, era); if (month < 1 || month > 12) { ThrowHelper.ThrowArgumentOutOfRange_Month(month); } ReadOnlySpan<int> days = ((year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? GregorianCalendar.DaysToMonth366 : GregorianCalendar.DaysToMonth365); return days[month] - days[month - 1]; } // Returns the number of days in the year given by the year argument for the current era. // public int GetDaysInYear(int year, int era) { // // Convert year/era value to Gregorain year value. // year = GetGregorianYear(year, era); return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 366 : 365; } // Returns the era for the specified DateTime value. public int GetEra(DateTime time) { long ticks = time.Ticks; // The assumption here is that m_EraInfo is listed in reverse order. foreach (EraInfo eraInfo in m_EraInfo) { if (ticks >= eraInfo.ticks) { return eraInfo.era; } } throw new ArgumentOutOfRangeException(nameof(time), SR.ArgumentOutOfRange_Era); } public int[] Eras { get { EraInfo[] eraInfo = m_EraInfo; var eras = new int[eraInfo.Length]; for (int i = 0; i < eraInfo.Length; i++) { eras[i] = eraInfo[i].era; } return eras; } } // Returns the month part of the specified DateTime. The returned value is an // integer between 1 and 12. // public int GetMonth(DateTime time) { CheckTicksRange(time.Ticks); return time.Month; } // Returns the number of months in the specified year and era. // Always return 12. public int GetMonthsInYear(int year, int era) { ValidateYearInEra(year, era); return 12; } // Returns the year part of the specified DateTime. The returned value is an // integer between 1 and 9999. // public int GetYear(DateTime time) { long ticks = time.Ticks; CheckTicksRange(ticks); foreach (EraInfo eraInfo in m_EraInfo) { if (ticks >= eraInfo.ticks) { return time.Year - eraInfo.yearOffset; } } throw new ArgumentException(SR.Argument_NoEra); } // Returns the year that match the specified Gregorian year. The returned value is an // integer between 1 and 9999. // public int GetYear(int year, DateTime time) { long ticks = time.Ticks; foreach (EraInfo eraInfo in m_EraInfo) { // while calculating dates with JapaneseLuniSolarCalendar, we can run into cases right after the start of the era // and still belong to the month which is started in previous era. Calculating equivalent calendar date will cause // using the new era info which will have the year offset equal to the year we are calculating year = m_EraInfo[i].yearOffset // which will end up with zero as calendar year. // We should use the previous era info instead to get the right year number. Example of such date is Feb 2nd 1989 if (ticks >= eraInfo.ticks && year > eraInfo.yearOffset) { return year - eraInfo.yearOffset; } } throw new ArgumentException(SR.Argument_NoEra); } // Checks whether a given day in the specified era is a leap day. This method returns true if // the date is a leap day, or false if not. // public bool IsLeapDay(int year, int month, int day, int era) { // year/month/era checking is done in GetDaysInMonth() if (day < 1 || day > GetDaysInMonth(year, month, era)) { throw new ArgumentOutOfRangeException( nameof(day), SR.Format( SR.ArgumentOutOfRange_Range, 1, GetDaysInMonth(year, month, era))); } if (!IsLeapYear(year, era)) { return false; } if (month == 2 && day == 29) { return true; } return false; } // Giving the calendar year and era, ValidateYearInEra will validate the existence of the input year in the input era. // This method will throw if the year or the era is invalid. public void ValidateYearInEra(int year, int era) => GetYearOffset(year, era, throwOnError: true); // Returns the leap month in a calendar year of the specified era. // This method always returns 0 as all calendars using this method don't have leap months. public int GetLeapMonth(int year, int era) { ValidateYearInEra(year, era); return 0; } // Checks whether a given month in the specified era is a leap month. // This method always returns false as all calendars using this method don't have leap months. public bool IsLeapMonth(int year, int month, int era) { ValidateYearInEra(year, era); if (month < 1 || month > 12) { throw new ArgumentOutOfRangeException( nameof(month), SR.Format( SR.ArgumentOutOfRange_Range, 1, 12)); } return false; } // Checks whether a given year in the specified era is a leap year. This method returns true if // year is a leap year, or false if not. // public bool IsLeapYear(int year, int era) { year = GetGregorianYear(year, era); return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); } // Returns the date and time converted to a DateTime value. Throws an exception if the n-tuple is invalid. // public DateTime ToDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int era) { year = GetGregorianYear(year, era); long ticks = GregorianCalendar.DateToTicks(year, month, day) + Calendar.TimeToTicks(hour, minute, second, millisecond); CheckTicksRange(ticks); return new DateTime(ticks); } public int GetWeekOfYear(DateTime time, CalendarWeekRule rule, DayOfWeek firstDayOfWeek) { CheckTicksRange(time.Ticks); // Use GregorianCalendar to get around the problem that the implementation in Calendar.GetWeekOfYear() // can call GetYear() that exceeds the supported range of the Gregorian-based calendars. return GregorianCalendar.GetDefaultInstance().GetWeekOfYear(time, rule, firstDayOfWeek); } public int ToFourDigitYear(int year, int twoDigitYearMax) { ArgumentOutOfRangeException.ThrowIfNegative(year); if (year < 100) { return (twoDigitYearMax / 100 - (year > twoDigitYearMax % 100 ? 1 : 0)) * 100 + year; } if (year < m_minYear || year > m_maxYear) { throw new ArgumentOutOfRangeException( nameof(year), SR.Format(SR.ArgumentOutOfRange_Range, m_minYear, m_maxYear)); } // If the year value is above 100, just return the year value. Don't have to do // the TwoDigitYearMax comparison. return year; } } } |