Search code examples
c#algorithmdatecalendargregorian-calendar

Algorithm to find the Gregorian date of the Chinese New Year of a certain Gregorian year


I am making a driver to calculate various holidays in a given time span. So, I need to find the Gregorian dates of all the Chinese Holidays (Chinese New Year, QingMing Festival, Dragon Boat Festival, etc). I used the famous 'Easter algorithm' for Good Friday, Easter Monday, Ascension Day, and Whit Monday calculations; however, I don't understand it well enough to adapt it for the Chinese calendar.

I have found similar questions, but they often go from Gregorian to Chinese:

Moon / Lunar Phase Algorithm

Calculating lunar/lunisolar holidays in python

http://www.herongyang.com/year/program.html

http://www.hermetic.ch/cal_stud/ch_year.htm

The last link was exremely helpful but I'm still not sure how to implement that algorithm in a way that can help me. Any advice or code would be greatly appreciated!

Here is my Good Friday Algorithm:

private void GetGoodFridayOccurances(DateTime startDate, DateTime endDate,      List<ObservedHoliday> observedHolidays, StandardHoliday holiday)
    {
        for (DateTime date = startDate; date <= endDate; date = date.AddYears(1))
        {
            #region Finding the Day of Easter Algorithm
            int day, month;
            int firstTwo = date.Year / 100;
            int remainderMod = date.Year % 19;
            int pfmDate = (firstTwo - 15) / 2 + 202 - 11 * remainderMod;
            #region switches
            switch (firstTwo)
            {
                case 21:
                case 24:
                case 25:
                case 27:
                case 28:
                case 29:
                case 30:
                case 31:
                case 32:
                case 34:
                case 35:
                case 38:
                    pfmDate = pfmDate - 1;
                    break;
                case 33:
                case 36:
                case 37:
                case 39:
                case 40:
                    pfmDate = pfmDate - 2;
                    break;
            }
            #endregion
            pfmDate = pfmDate % 30;

            int tA = pfmDate + 21;
            if (pfmDate == 29)
                tA = tA - 1;
            if (pfmDate == 29 && remainderMod > 10)
                tA = tA - 1;
            //Find next sunday
            int tB = (tA - 19) % 7;

            int tC = (40 - firstTwo) % 4;
            if (tC == 3 || tC > 1)
                tC = tC + 1;

            pfmDate = date.Year % 100;
            int tD = (pfmDate + pfmDate / 4) % 7;
            int tE = ((20 - tB - tC - tD) % 7) + 1;
            day = tA + tE;

            if (day > 31)
            {
                day = day - 31;
                month = 4;
            }
            else
            {
                month = 3;
            }
            #endregion

            DateTime observed = new DateTime(date.Year, month, day).AddDays(-2);
            ObservedHoliday obsdate = new ObservedHoliday(holiday);
            if (startDate == endDate && startDate.Day == observed.Day)
            {
                obsdate.DateObserved = observed;
                observedHolidays.Add(obsdate);
            }
            else if (startDate != endDate && observed >= startDate)
            {
                obsdate.DateObserved = observed;
                observedHolidays.Add(obsdate);
            }
        }  

Solution

  • For Chinese New Year, I think this would work:

    using System;
    using System.Globalization;
    
    public static ( Int32 year, Int32 month, Int32 day ) GetDateOfChineseNewYear()
    {
        ChineseLunisolarCalendar chinese   = new ChineseLunisolarCalendar();
        GregorianCalendar        gregorian = new GregorianCalendar();
    
        DateTime utcNow = DateTime.UtcNow;
    
        // Get Chinese New Year of current UTC date/time
        DateTime chineseNewYear = chinese.ToDateTime( utcNow.Year, 1, 1, 0, 0, 0, 0 );
    
        // Convert back to Gregorian (you could just query properties of `chineseNewYear` directly, but I prefer to use `GregorianCalendar` for consistency:
    
        Int32 year  = gregorian.GetYear( chineseNewYear );
        Int32 month = gregorian.GetMonth( chineseNewYear );
        Int32 day   = gregorian.GetDayOfMonth( chineseNewYear );
    
        return ( year, month, day );
    }
    

    .NET 6 Support:

    Now that .NET 6 (finally) has the DateOnly type (after we've been asking for it for 20 years now...), this works:

    (Unfortunately .NET 6's Calendar class hasn't yet been updated to support DateOnly, but it's straightforward to handle manually):

    private static readonly ChineseLunisolarCalendar _chineseCal   = new ChineseLunisolarCalendar();
    private static readonly GregorianCalendar        _gregorianCal = new GregorianCalendar();
    
    public static DateOnly GetGregorianDateOfChineseNewYear()
    {
        return GetGregorianDateOfChineseNewYear( DateTime.UtcNow.Year );
    }
    
    public static DateOnly GetGregorianDateOfChineseNewYear( Int32 gregorianYear )
    {
        // Get Chinese New Year of current UTC date/time
        DateTime chineseNewYear = _chineseCal.ToDateTime( year: gregorianYear, month: 1, day: 1, /*hms:*/ 0, 0, 0, 0 );
    
        // Convert back to Gregorian (you could just query properties of `chineseNewYear` directly, but I prefer to use `GregorianCalendar` for consistency:
    
        Int32 year  = _gregorianCal.GetYear( chineseNewYear );
        Int32 month = _gregorianCal.GetMonth( chineseNewYear );
        Int32 day   = _gregorianCal.GetDayOfMonth( chineseNewYear );
    
        return new DateOnly( year, month, day, _gregorianCal );
    }
    

    So running this...

    Console.WriteLine( "Gregorian year: {0}, Chinese New Year: {1:ddd} {1}", 2021, GetGregorianDateOfChineseNewYear( 2021 ) );
    Console.WriteLine();
    Console.WriteLine( "Next 10 years:" );
    
    for( Int32 i = 2022; i < 2030; i++ )
    {
        Console.WriteLine( "Gregorian year: {0}, Chinese New Year: {1:ddd} {1}", i, GetGregorianDateOfChineseNewYear( i ) );
    }
    

    ...gives me this output:

    Gregorian year: 2021, Chinese New Year: Sat 2021-02-12
    
    Next 10 years:
    Gregorian year: 2022, Chinese New Year: Tue 2022-02-01
    Gregorian year: 2023, Chinese New Year: Sun 2023-01-22
    Gregorian year: 2024, Chinese New Year: Sat 2024-02-10
    Gregorian year: 2025, Chinese New Year: Wed 2025-01-29
    Gregorian year: 2026, Chinese New Year: Tue 2026-02-17
    Gregorian year: 2027, Chinese New Year: Sat 2027-02-06
    Gregorian year: 2028, Chinese New Year: Wed 2028-01-26
    Gregorian year: 2029, Chinese New Year: Tue 2029-02-13
    Gregorian year: 2030, Chinese New Year: Sun 2030-02-03
    Gregorian year: 2031, Chinese New Year: Thu 2031-01-23