File: src\libraries\System.Private.CoreLib\src\System\Globalization\EastAsianLunisolarCalendar.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// 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
{
    public abstract class EastAsianLunisolarCalendar : Calendar
    {
        private const int LeapMonth = 0;
        private const int Jan1Month = 1;
        private const int Jan1Date = 2;
        private const int nDaysPerMonth = 3;
 
        // # of days so far in the solar year
        private static ReadOnlySpan<int> DaysToMonth365 => [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
        private static ReadOnlySpan<int> DaysToMonth366 => [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
 
        public override CalendarAlgorithmType AlgorithmType => CalendarAlgorithmType.LunisolarCalendar;
 
        /// <summary>
        /// Return the year number in the 60-year cycle.
        /// </summary>
        public virtual int GetSexagenaryYear(DateTime time)
        {
            CheckTicksRange(time.Ticks);
 
            TimeToLunar(time, out int year, out _, out _);
            return ((year - 4) % 60) + 1;
        }
 
        /// <summary>
        /// Return the celestial year from the 60-year cycle.
        /// The returned value is from 1 ~ 10.
        /// </summary>
        public int GetCelestialStem(int sexagenaryYear)
        {
            if (sexagenaryYear < 1 || sexagenaryYear > 60)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(sexagenaryYear),
                    sexagenaryYear,
                    SR.Format(SR.ArgumentOutOfRange_Range, 1, 60));
            }
 
            return ((sexagenaryYear - 1) % 10) + 1;
        }
 
        /// <summary>
        /// Return the Terrestial Branch from the 60-year cycle.
        /// The returned value is from 1 ~ 12.
        /// </summary>
        public int GetTerrestrialBranch(int sexagenaryYear)
        {
            if (sexagenaryYear < 1 || sexagenaryYear > 60)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(sexagenaryYear),
                    sexagenaryYear,
                    SR.Format(SR.ArgumentOutOfRange_Range, 1, 60));
            }
 
            return ((sexagenaryYear - 1) % 12) + 1;
        }
 
        internal abstract int GetYearInfo(int LunarYear, int Index);
        internal abstract int GetYear(int year, DateTime time);
        internal abstract int GetGregorianYear(int year, int era);
 
        internal abstract int MinCalendarYear { get; }
        internal abstract int MaxCalendarYear { get; }
        internal abstract EraInfo[]? CalEraInfo { get; }
        internal abstract DateTime MinDate { get; }
        internal abstract DateTime MaxDate { get; }
 
        internal const int MaxCalendarMonth = 13;
        internal const int MaxCalendarDay = 30;
 
        internal int MinEraCalendarYear(int era)
        {
            EraInfo[]? eraInfo = CalEraInfo;
            if (eraInfo == null)
            {
                return MinCalendarYear;
            }
 
            if (era == CurrentEra)
            {
                era = CurrentEraValue;
            }
 
            // Era has to be in the supported range otherwise we will throw exception in CheckEraRange()
            if (era == GetEra(MinDate))
            {
                return GetYear(MinCalendarYear, MinDate);
            }
 
            for (int i = 0; i < eraInfo.Length; i++)
            {
                if (era == eraInfo[i].era)
                {
                    return eraInfo[i].minEraYear;
                }
            }
 
            throw new ArgumentOutOfRangeException(nameof(era), era, SR.ArgumentOutOfRange_InvalidEraValue);
        }
 
        internal int MaxEraCalendarYear(int era)
        {
            EraInfo[]? eraInfo = CalEraInfo;
            if (eraInfo == null)
            {
                return MaxCalendarYear;
            }
 
            if (era == CurrentEra)
            {
                era = CurrentEraValue;
            }
 
            // Era has to be in the supported range otherwise we will throw exception in CheckEraRange()
            if (era == GetEra(MaxDate))
            {
                return GetYear(MaxCalendarYear, MaxDate);
            }
 
            for (int i = 0; i < eraInfo.Length; i++)
            {
                if (era == eraInfo[i].era)
                {
                    return eraInfo[i].maxEraYear;
                }
            }
 
            throw new ArgumentOutOfRangeException(nameof(era), era, SR.ArgumentOutOfRange_InvalidEraValue);
        }
 
        internal EastAsianLunisolarCalendar()
        {
        }
 
        internal void CheckTicksRange(long ticks)
        {
            if (ticks < MinSupportedDateTime.Ticks || ticks > MaxSupportedDateTime.Ticks)
            {
                throw new ArgumentOutOfRangeException(
                                "time",
                                ticks,
                                SR.Format(CultureInfo.InvariantCulture, SR.ArgumentOutOfRange_CalendarRange,
                                MinSupportedDateTime, MaxSupportedDateTime));
            }
        }
 
        internal void CheckEraRange(int era)
        {
            if (era == CurrentEra)
            {
                era = CurrentEraValue;
            }
 
            if (era < GetEra(MinDate) || era > GetEra(MaxDate))
            {
                throw new ArgumentOutOfRangeException(nameof(era), era, SR.ArgumentOutOfRange_InvalidEraValue);
            }
        }
 
        internal int CheckYearRange(int year, int era)
        {
            CheckEraRange(era);
            year = GetGregorianYear(year, era);
 
            if (year < MinCalendarYear || year > MaxCalendarYear)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(year),
                    year,
                    SR.Format(SR.ArgumentOutOfRange_Range, MinEraCalendarYear(era), MaxEraCalendarYear(era)));
            }
            return year;
        }
 
        internal int CheckYearMonthRange(int year, int month, int era)
        {
            year = CheckYearRange(year, era);
 
            if (month == 13)
            {
                // Reject if there is no leap month this year
                if (GetYearInfo(year, LeapMonth) == 0)
                {
                    ThrowHelper.ThrowArgumentOutOfRange_Month(month);
                }
            }
 
            if (month < 1 || month > 13)
            {
                ThrowHelper.ThrowArgumentOutOfRange_Month(month);
            }
 
            return year;
        }
 
        internal int InternalGetDaysInMonth(int year, int month)
        {
            int mask = 0x8000;
 
            // convert the lunar day into a lunar month/date
            mask >>= (month - 1);
            if ((GetYearInfo(year, nDaysPerMonth) & mask) == 0)
            {
                return 29;
            }
 
            return 30;
        }
 
        /// <summary>
        /// Returns the number of days in the month given by the year and
        /// month arguments.
        /// </summary>
        public override int GetDaysInMonth(int year, int month, int era)
        {
            year = CheckYearMonthRange(year, month, era);
            return InternalGetDaysInMonth(year, month);
        }
 
        private static bool GregorianIsLeapYear(int y)
        {
            if ((y % 4) != 0)
            {
                return false;
            }
            if ((y % 100) != 0)
            {
                return true;
            }
 
            return (y % 400) == 0;
        }
 
        /// <summary>
        /// Returns the date and time converted to a DateTime value.
        /// Throws an exception if the n-tuple is invalid.
        /// </summary>
        public override DateTime ToDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int era)
        {
            year = CheckYearMonthRange(year, month, era);
            int daysInMonth = InternalGetDaysInMonth(year, month);
            if (day < 1 || day > daysInMonth)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(day),
                    day,
                    SR.Format(SR.ArgumentOutOfRange_Day, daysInMonth, month));
            }
 
            if (!LunarToGregorian(year, month, day, out int gy, out int gm, out int gd))
            {
                throw new ArgumentOutOfRangeException(null, SR.ArgumentOutOfRange_BadYearMonthDay);
            }
 
            return new DateTime(gy, gm, gd, hour, minute, second, millisecond);
        }
 
        /// <summary>
        /// Calculates lunar calendar info for the given gregorian year, month, date.
        /// The input date should be validated before calling this method.
        /// </summary>
        private void GregorianToLunar(int solarYear, int solarMonth, int solarDate, out int lunarYear, out int lunarMonth, out int lunarDate)
        {
            bool isLeapYear = GregorianIsLeapYear(solarYear);
            int jan1Month;
            int jan1Date;
 
            // Calculate the day number in the solar year.
            int solarDay = isLeapYear ? DaysToMonth366[solarMonth - 1] : DaysToMonth365[solarMonth - 1];
            solarDay += solarDate;
 
            // Calculate the day number in the lunar year.
            int lunarDay = solarDay;
            lunarYear = solarYear;
            if (lunarYear == (MaxCalendarYear + 1))
            {
                lunarYear--;
                lunarDay += (GregorianIsLeapYear(lunarYear) ? 366 : 365);
                jan1Month = GetYearInfo(lunarYear, Jan1Month);
                jan1Date = GetYearInfo(lunarYear, Jan1Date);
            }
            else
            {
                jan1Month = GetYearInfo(lunarYear, Jan1Month);
                jan1Date = GetYearInfo(lunarYear, Jan1Date);
 
                // check if this solar date is actually part of the previous
                // lunar year
                if ((solarMonth < jan1Month) ||
                    (solarMonth == jan1Month && solarDate < jan1Date))
                {
                    // the corresponding lunar day is actually part of the previous
                    // lunar year
                    lunarYear--;
 
                    // add a solar year to the lunar day #
                    lunarDay += (GregorianIsLeapYear(lunarYear) ? 366 : 365);
 
                    // update the new start of year
                    jan1Month = GetYearInfo(lunarYear, Jan1Month);
                    jan1Date = GetYearInfo(lunarYear, Jan1Date);
                }
            }
 
            // convert solar day into lunar day.
            // subtract off the beginning part of the solar year which is not
            // part of the lunar year.  since this part is always in Jan or Feb,
            // we don't need to handle Leap Year (LY only affects March
            // and later).
            lunarDay -= DaysToMonth365[jan1Month - 1];
            lunarDay -= (jan1Date - 1);
 
            // convert the lunar day into a lunar month/date
            int mask = 0x8000;
            int yearInfo = GetYearInfo(lunarYear, nDaysPerMonth);
            int days = ((yearInfo & mask) != 0) ? 30 : 29;
            lunarMonth = 1;
            while (lunarDay > days)
            {
                lunarDay -= days;
                lunarMonth++;
                mask >>= 1;
                days = ((yearInfo & mask) != 0) ? 30 : 29;
            }
            lunarDate = lunarDay;
        }
 
        /// <summary>
        /// Convert from Lunar to Gregorian
        /// </summary>
        /// <remarks>
        /// Highly inefficient, but it works based on the forward conversion
        /// </remarks>
        private bool LunarToGregorian(int lunarYear, int lunarMonth, int lunarDate, out int solarYear, out int solarMonth, out int solarDay)
        {
            if (lunarDate < 1 || lunarDate > 30)
            {
                solarYear = 0;
                solarMonth = 0;
                solarDay = 0;
                return false;
            }
 
            int numLunarDays = lunarDate - 1;
 
            // Add previous months days to form the total num of days from the first of the month.
            for (int i = 1; i < lunarMonth; i++)
            {
                numLunarDays += InternalGetDaysInMonth(lunarYear, i);
            }
 
            // Get Gregorian First of year
            int jan1Month = GetYearInfo(lunarYear, Jan1Month);
            int jan1Date = GetYearInfo(lunarYear, Jan1Date);
 
            // calc the solar day of year of 1 Lunar day
            bool isLeapYear = GregorianIsLeapYear(lunarYear);
            ReadOnlySpan<int> days = isLeapYear ? DaysToMonth366 : DaysToMonth365;
 
            solarDay = jan1Date;
 
            if (jan1Month > 1)
            {
                solarDay += days[jan1Month - 1];
            }
 
            // Add the actual lunar day to get the solar day we want
            solarDay += numLunarDays;
 
            if (solarDay > (365 + (isLeapYear ? 1 : 0)))
            {
                solarYear = lunarYear + 1;
                solarDay -= (365 + (isLeapYear ? 1 : 0));
            }
            else
            {
                solarYear = lunarYear;
            }
 
            for (solarMonth = 1; solarMonth < 12; solarMonth++)
            {
                if (days[solarMonth] >= solarDay)
                {
                    break;
                }
            }
 
            solarDay -= days[solarMonth - 1];
            return true;
        }
 
        private DateTime LunarToTime(DateTime time, int year, int month, int day)
        {
            LunarToGregorian(year, month, day, out int gy, out int gm, out int gd);
            time.GetTime(out int hour, out int minute, out int second, out int millisecond);
            return GregorianCalendar.GetDefaultInstance().ToDateTime(gy, gm, gd, hour, minute, second, millisecond);
        }
 
        private void TimeToLunar(DateTime time, out int year, out int month, out int day)
        {
            Calendar gregorianCalendar = GregorianCalendar.GetDefaultInstance();
            int gy = gregorianCalendar.GetYear(time);
            int gm = gregorianCalendar.GetMonth(time);
            int gd = gregorianCalendar.GetDayOfMonth(time);
 
            GregorianToLunar(gy, gm, gd, out year, out month, out day);
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        public override DateTime AddMonths(DateTime time, int months)
        {
            if (months < -120000 || months > 120000)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(months),
                    months,
                    SR.Format(SR.ArgumentOutOfRange_Range, -120000, 120000));
            }
 
            CheckTicksRange(time.Ticks);
            TimeToLunar(time, out int y, out int m, out int d);
 
            int i = m + months;
            if (i > 0)
            {
                int monthsInYear = InternalIsLeapYear(y) ? 13 : 12;
 
                while (i - monthsInYear > 0)
                {
                    i -= monthsInYear;
                    y++;
                    monthsInYear = InternalIsLeapYear(y) ? 13 : 12;
                }
                m = i;
            }
            else
            {
                int monthsInYear;
                while (i <= 0)
                {
                    monthsInYear = InternalIsLeapYear(y - 1) ? 13 : 12;
                    i += monthsInYear;
                    y--;
                }
                m = i;
            }
 
            int days = InternalGetDaysInMonth(y, m);
            if (d > days)
            {
                d = days;
            }
            DateTime dt = LunarToTime(time, y, m, d);
 
            CheckAddResult(dt.Ticks, MinSupportedDateTime, MaxSupportedDateTime);
            return dt;
        }
 
        public override DateTime AddYears(DateTime time, int years)
        {
            CheckTicksRange(time.Ticks);
            TimeToLunar(time, out int y, out int m, out int d);
 
            y += years;
 
            if (m == 13 && !InternalIsLeapYear(y))
            {
                m = 12;
                d = InternalGetDaysInMonth(y, m);
            }
            int daysInMonths = InternalGetDaysInMonth(y, m);
            if (d > daysInMonths)
            {
                d = daysInMonths;
            }
 
            DateTime dt = LunarToTime(time, y, m, d);
            CheckAddResult(dt.Ticks, MinSupportedDateTime, MaxSupportedDateTime);
            return dt;
        }
 
        /// <summary>
        /// Returns the day-of-year part of the specified DateTime. The returned value
        /// is an integer between 1 and [354|355 |383|384].
        /// </summary>
        public override int GetDayOfYear(DateTime time)
        {
            CheckTicksRange(time.Ticks);
            TimeToLunar(time, out int y, out int m, out int d);
 
            for (int i = 1; i < m; i++)
            {
                d += InternalGetDaysInMonth(y, i);
            }
            return d;
        }
 
        /// <summary>
        /// Returns the day-of-month part of the specified DateTime. The returned
        /// value is an integer between 1 and 29 or 30.
        /// </summary>
        public override int GetDayOfMonth(DateTime time)
        {
            CheckTicksRange(time.Ticks);
 
            TimeToLunar(time, out _, out _, out int d);
 
            return d;
        }
 
        /// <summary>
        /// Returns the number of days in the year given by the year argument for the current era.
        /// </summary>
        public override int GetDaysInYear(int year, int era)
        {
            year = CheckYearRange(year, era);
 
            int days = 0;
            int monthsInYear = InternalIsLeapYear(year) ? 13 : 12;
 
            while (monthsInYear != 0)
            {
                days += InternalGetDaysInMonth(year, monthsInYear--);
            }
 
            return days;
        }
 
        /// <summary>
        /// Returns the month part of the specified DateTime.
        /// The returned value is an integer between 1 and 13.
        /// </summary>
        public override int GetMonth(DateTime time)
        {
            CheckTicksRange(time.Ticks);
 
            TimeToLunar(time, out _, out int m, out _);
 
            return m;
        }
 
        /// <summary>
        /// Returns the year part of the specified DateTime.
        /// The returned value is an integer between 1 and MaxCalendarYear.
        /// </summary>
        public override int GetYear(DateTime time)
        {
            CheckTicksRange(time.Ticks);
 
            TimeToLunar(time, out int y, out _, out _);
 
            return GetYear(y, time);
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        public override DayOfWeek GetDayOfWeek(DateTime time)
        {
            CheckTicksRange(time.Ticks);
            return time.DayOfWeek;
        }
 
        /// <summary>
        /// Returns the number of months in the specified year and era.
        /// </summary>
        public override int GetMonthsInYear(int year, int era)
        {
            year = CheckYearRange(year, era);
            return InternalIsLeapYear(year) ? 13 : 12;
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        public override bool IsLeapDay(int year, int month, int day, int era)
        {
            year = CheckYearMonthRange(year, month, era);
            int daysInMonth = InternalGetDaysInMonth(year, month);
 
            if (day < 1 || day > daysInMonth)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(day),
                    day,
                    SR.Format(SR.ArgumentOutOfRange_Day, daysInMonth, month));
            }
 
            int m = GetYearInfo(year, LeapMonth);
            return (m != 0) && (month == (m + 1));
        }
 
        /// <summary>
        /// Checks whether a given month in the specified era is a leap month.
        /// This method returns true if month is a leap month, or false if not.
        /// </summary>
        public override bool IsLeapMonth(int year, int month, int era)
        {
            year = CheckYearMonthRange(year, month, era);
            int m = GetYearInfo(year, LeapMonth);
            return (m != 0) && (month == (m + 1));
        }
 
        /// <summary>
        /// Returns  the leap month in a calendar year of the specified era. This method returns 0
        /// if this year is not a leap year.
        /// </summary>
        public override int GetLeapMonth(int year, int era)
        {
            year = CheckYearRange(year, era);
            int month = GetYearInfo(year, LeapMonth);
            return month > 0 ? month + 1 : 0;
        }
 
        internal bool InternalIsLeapYear(int year)
        {
            return GetYearInfo(year, LeapMonth) != 0;
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        public override bool IsLeapYear(int year, int era)
        {
            year = CheckYearRange(year, era);
            return InternalIsLeapYear(year);
        }
 
        private const int DefaultGregorianTwoDigitYearMax = 2049;
 
        public override int TwoDigitYearMax
        {
            get
            {
                if (_twoDigitYearMax == -1)
                {
                    _twoDigitYearMax = GetSystemTwoDigitYearSetting(BaseCalendarID, GetYear(new DateTime(DefaultGregorianTwoDigitYearMax, 1, 1)));
                }
 
                return _twoDigitYearMax;
            }
            set
            {
                VerifyWritable();
                if (value < 99 || value > MaxCalendarYear)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        value,
                        SR.Format(SR.ArgumentOutOfRange_Range, 99, MaxCalendarYear));
                }
 
                _twoDigitYearMax = value;
            }
        }
 
        public override int ToFourDigitYear(int year)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(year);
 
            year = base.ToFourDigitYear(year);
            CheckYearRange(year, CurrentEra);
            return year;
        }
    }
}