// 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;
}
}
}