How to implement calendar (Gregorian calendar + Lunar Calendar)

Technical documents, reproduced please indicate the source!

On the calendar

In our daily work and life, the calendar usually refers to the Gregorian calendar, also known as “solar calendar”, “solar calendar”. The Gregorian calendar is based on the movement cycle of the Earth around the sun. A Gregorian year is approximately equal to a tropical year, and the average length of the calendar year is only 26 seconds different from the tropical year, which is only one day in 3300 years. At present for the world’s most countries in general, and to the AD as the year.

The rule of the Gregorian calendar is obvious, we can use the built-in Date object is very easy to implement a perpetual calendar. Here, we mainly discuss the lunar calendar implementation method, the lunar calendar is a kind of Chinese calendar, also known as “summer calendar”, “Chinese calendar”, “old calendar”, commonly known as “lunar calendar”. The method of determining the month is given by the lunar month cycle. The day on which the new moon is located is the first day, and the lunar month is about 29 and a half days long, so the lunar month has 30 days and the lunar month 29 days. An ordinary lunar year has 12 months, 354 or 355 days in a year. Thirteen months are used in leap years. One month is named according to the name of the previous month (for example, the month before August is a leap month) and 383 or 384 days are used in a leap year. The size of a month, depending on astronomical observations, has nothing to do with the rules.

Lunar renderings

We need to set a small goal first, the goal is clear, we just need to move towards the goal.

Implementation method

The implementation of the Gregorian calendar

Every calendar, there is a default state, ours is no exception, default current year month day. The Gregorian calendar can be divided into three modules, calendar day of the last month, calendar day of the current month and calendar day of the next month (as shown in the picture).

We start with the default value new Date() to obtain the start Date, end Date, and week of the start Date. Then we can obtain the basic information of the current month’s day, the current month’s day, the next month’s day.

/** * Creates each day of the current month table (for convenience, we've introduced moment.js here) **@param { string } Date The date passed in (for example, '2021-3-18') *@returns { array }  Days returns an array of all days */ in the month table for which the day was passed
function createMonthDays(date) {
	let day = moment(date) || moment();
	let days = []; 
    let start = day.clone().startOf('month');
    let end = day.clone().endOf('month');
    let weekday = start.weekday();
    
    // Get last month's day data
    for (let i = 1; i <= weekday; i++) {
        let tempDay = moment([
            start.year(),
            start.month(),
            i
        ]).subtract(weekday, 'days');
        days.push(createDay(tempDay));
    }
    // Get the date of the month
    let dateIterator = start.clone();
    while (dateIterator.isBefore(end) || dateIterator.isSame(end, 'day')) {
        days.push(
            createDay(dateIterator.clone())
        );
        dateIterator.add(1.'days');
    }
    // Get the next month's day data
    while (days.length % 7! = =0) {
        days.push(
            createDay(dateIterator.clone())
        );
        dateIterator.add(1.'days');
    }
    
    return days;
}

/** * Creates the current day information that can be used to customize the day object data to implement the customization information in the renderings **@param { Object } Day moment Time object *@returns { object}  Day returns processed data */
function createDay(day) {
    return {
        moment: day,
        day: day.date()
    }
}
Copy the code

At this point, we can successfully complete the main core code of the Gregorian calendar.

The Realization of the Lunar Calendar

About the lunar calendar, read a lot of information, for the lunar calendar, have to admire the wisdom of our ancestors. Since the rules of our lunar reckoning are not as obvious as those of the Gregorian calendar, some of the source data are derived from astronomical observations and are not related to the rules, as described below. Here would like to thank the Hong Kong observatory (www.hko.gov.hk/tc/gts/time…

/* Source data description: * lunarYear data are derived from the source data provided by the Hong Kong Observatory in reverse order, including 201 items corresponding to 1900-2100 respectively. * the sample: 2021-0 ╭ x06aa0 * -- -- -- -- -- -- -- ┰ -- -- -- -- -- -- -- ┰ -- -- -- -- -- -- -- ┰ -- -- -- -- -- -- -- ┰ -- -- -- -- -- -- -- -- ╮ * 0000 ┆ ┆ ┆ ┆ ┆ ┆ * 0000 1010 1010 0110 ╊ ┠ -- -- -- -- -- -- -- -- -- -- -- - ╊ -- -- -- -- -- -- -- ╊ -- -- -- -- -- -- -- ╊ -- -- -- -- -- -- -- -- 17 ┆ ┆ ┨ * 20-16-12 12-9 ┆ ┆ 8-5 ┆ ┆ * 4-1 ╰ -- -- -- -- -- -- -- ┸ -- -- -- -- -- -- - ┸ -- -- -- -- -- -- -- ┸ -- -- -- -- -- -- -- ┸ -- -- -- -- -- -- -- -- ╯ * 1-4: says the presence of a leap year, so, for the leap month month, if not, is zero. In 2021, no leap months * 5-16: indicates whether normal months other than leap months are large or small, with 1 being 30 days and 0 being 29 days. From January to December 16th is corresponding to the fifth, 2021 months days,30,30,29,30,29,30,29,30,29,30,29 [29] * 17-20: said leap month is monthly or abortion, only when the presence of leap month sense. (0/1, leap big/small month) */

const lunarYears = [
    0x04bd8./ / 1901-2000
    0x04ae0.0x0a570.0x054d5.0x0d260.0x0d950.0x16554.0x056a0.0x09ad0.0x055d2.0x04ae0.0x0a5b6.0x0a4d0.0x0d250.0x1d255.0x0b540.0x0d6a0.0x0ada2.0x095b0.0x14977.0x04970.0x0a4b0.0x0b4b5.0x06a50.0x06d40.0x1ab54.0x02b60.0x09570.0x052f2.0x04970.0x06566.0x0d4a0.0x0ea50.0x16a95.0x05ad0.0x02b60.0x186e3.0x092e0.0x1c8d7.0x0c950.0x0d4a0.0x1d8a6.0x0b550.0x056a0.0x1a5b4.0x025d0.0x092d0.0x0d2b2.0x0a950.0x0b557.0x06ca0.0x0b550.0x15355.0x04da0.0x0a5b0.0x14573.0x052b0.0x0a9a8.0x0e950.0x06aa0.0x0aea6.0x0ab50.0x04b60.0x0aae4.0x0a570.0x05260.0x0f263.0x0d950.0x05b57.0x056a0.0x096d0.0x04dd5.0x04ad0.0x0a4d0.0x0d4d4.0x0d250.0x0d558.0x0b540.0x0b6a0.0x195a6.0x095b0.0x049b0.0x0a974.0x0a4b0.0x0b27a.0x06a50.0x06d40.0x0af46.0x0ab60.0x09570.0x04af5.0x04970.0x064b0.0x074a3.0x0ea50.0x06b58.0x05ac0.0x0ab60.0x096d5.0x092e0.0x0c960./ / 2001-2100
    0x0d954.0x0d4a0.0x0da50.0x07552.0x056a0.0x0abb7.0x025d0.0x092d0.0x0cab5.0x0a950.0x0b4a0.0x0baa4.0x0ad50.0x055d9.0x04ba0.0x0a5b0.0x15176.0x052b0.0x0a930.0x07954.0x06aa0.0x0ad50.0x05b52.0x04b60.0x0a6e6.0x0a4e0.0x0d260.0x0ea65.0x0d530.0x05aa0.0x076a3.0x096d0.0x04afb.0x04ad0.0x0a4d0.0x1d0b6.0x0d250.0x0d520.0x0dd45.0x0b5a0.0x056d0.0x055b2.0x049b0.0x0a577.0x0a4b0.0x0aa50.0x1b255.0x06d20.0x0ada0.0x14b63.0x09370.0x049f8.0x04970.0x064b0.0x168a6.0x0ea50.0x06b20.0x1a6c4.0x0aae0.0x092e0.0x0d2e3.0x0c960.0x0d557.0x0d4a0.0x0da50.0x05d55.0x056a0.0x0a6d0.0x055d4.0x052d0.0x0a9b8.0x0a950.0x0b4a0.0x0b6a6.0x0ad50.0x055a0.0x0aba4.0x0a5b0.0x052b0.0x0b273.0x06930.0x07337.0x06aa0.0x0ad50.0x14b55.0x04b60.0x0a570.0x054e4.0x0d160.0x0e968.0x0d520.0x0daa0.0x16aa6.0x056d0.0x04ae0.0x0a9d4.0x0a2d0.0x0d150.0x0f252.0x0d520
]
Copy the code

With the raw data of the lunar calendar year, we can obtain the leap month, the number of months, the size of the month, etc. of any year in the Gregorian calendar year (1901-2100) according to the explanation of the source data above. Now the core problem we have to solve is how to calculate the lunar information on a certain day in the Gregorian calendar year. Here, to determine a reference point is the core, the general idea:

  • Define the Chinese characters used in the lunar calendar, provide transformation methods, and write and parse source data methods according to the rules
  • Set the date of reference1901-02-19In 1901, it was the first day of the first lunar month, also known as the Spring Festival.
  • Calculate the difference between the conversion date and the base date
  • Using the source data and the days calculated in the previous step, calculate the lunar year/month/day of the date to be converted
  • Calculate the lunar information corresponding to each day in the Gregorian calendar. To avoid calculating the lunar information of each day in the calendar, we need to calculate the information of each day in the entire month based on the lunar information of the first data in the calendar.
/ / [' month ', 'is',' a ', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'winter', 'la'].
const ChinaMonths = ["\u6708"."\u6b63"."\u4e8c"."\u4e09"."\u56db"."\u4e94"."\u516d"."\u4e03"."\u516b"."\u4e5d"."\u5341"."\u51ac"."\u814a"]
// [' day ',' one ',' two ',' three ',' four ',' five ',' six ',' seven ',' eight ',' nine ',' ten ']
const ChinaDay = ["\u65e5"."\u4e00"."\u4e8c"."\u4e09"."\u56db"."\u4e94"."\u516d"."\u4e03"."\u516b"."\u4e5d"."\u5341"]
// [' chu ',' ten ',' 20 ',' 30 ',' leap ']
const ChinaElement = ["\u521d"."\u5341"."\u5eff"."\u5345"."\u95f0"]

// Lunar day is displayed in Chinese, and the parameter is day
const toChinaDay = function(day) {
    let str = ' ';
    switch(day) {
        case 10: 
            str = '\u521d\u5341';break; // "day 10"
        case 20:
            str = '\u5eff\u5341';break; // "twenty"
        case 30: 
            str = '\u5345\u5341';break; // "卅十"
        default: 
            str = ChinaElement[Math.floor(day/10)] + ChinaDay[day%10];
    }
    return str
}
// The first day of the lunar month displays the Chinese month (e.g., the first day of the second lunar month -> the second month, the first day of the fourth lunar month -> the fourth lunar month)
const toChinaMonth = function(month, isleap) {
    isleap = isleap || false;
    return isleap ? (ChinaElement[4] + ChinaMonths[month] + ChinaMonths[0]) : (ChinaMonths[month] + ChinaMonths[0]);
}

const nowInfo = function() {
    let now = new Date(a);return {
        y: now.getFullYear(),
        m: now.getMonth()+1.d: now.getDate()
    };
}
// A lunar leap month month
const leapMonth = function(year) {
    year = year || nowInfo().y;
    return lunarYears[year - 1900] & 0xF;
}
// The number of leap months in a lunar calendar year
const leapDays = function(year) {
    year = year || nowInfo().y;
    if(leapMonth(year)) {
        return (lunarYears[year-1900] & 0x10000)?30 : 29;
    }
    return 0;
}
// Days of a lunar month in a certain year
const lunarMonthDays = function(year) {
    year = year || nowInfo().y;
    let lunarYear = lunarYears[year - 1900];
    let monthDays = [];
    for(let i = 4; i< 16; i++) {
        let monthDay = (lunarYear >> i & 0x1)?30 :29;
        monthDays.push(monthDay);
    }
    monthDays.reverse();
    // Add a leap month
    let leapM = leapMonth(year);
    if(leapM) monthDays.splice(leapM, 0 , leapDays(year));
    return monthDays;
}
// The number of days in a lunar calendar year
const lunarYearDays = function(year) {
    year = year || nowInfo().y;
    let num = 0;
    lunarMonthDays(yar).forEach(item= > {
        num += item;
    });
    return num;
}
Copy the code

Calculate a calendar day corresponding to the lunar calendar, the logic is clear, the code is not difficult.

const solarToLunar = function(y,m,d) {
    if(y < 1901 || y > 2100) return -1;
    let date;
    if(! y) { date =new Date(a); }else {
        date = new Date(y,m-1,d);
    }

    // Refer to 1901-02-19, the first day of the first lunar month
    let offset = (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(1901.1.19)) /86400000;
    let temp = 0, i;
    for(i = 1901; i < 2101 && offset > 0; i++ ){
        temp = lunarYearDays(i);
        offset -= temp;
    }
    if(offset < 0) {
        offset += temp;
        i--;
    }

    // Lunar year, month and day
    let isLeap = false, j;
    let monthDays = lunarMonthDays(i);
    let leapM = leapMonth(i);

    if(offset > 0) {
        for(j = 0; j < monthDays.length && offset > 0; j++) {
            temp = monthDays[j];
            offset -=temp;
        }
        if(offset == 0) {
            j++;
        }
        if(offset < 0) { offset += temp; }}else {
        // Compensate for the lunar information of February 1901 in the Gregorian calendar
        if(offset == -23) {
            return {
                lunarY: i,
                lunarM: 12.lunarD: 8.isLeap: false}}}// Correct leap year month
    if(leapM) {
        if(j == leapM + 1) {
            isLeap = true
        }
        if(j >= leapM + 1) {
            j--
        }
    }

    return {
        lunarY: i,
        lunarM: j,
        lunarD: ++offset,
        isLeap: isLeap
    }
}
Copy the code

At this point, we should be able to convert all the days from 1900 to 2100, but if you think about it, it doesn’t make a lot of sense to iterate over 200 years of lunar source data for each day. Therefore, for the monthly calendar, we should only calculate the first data, and the following daily calculations should be reasonable. On how to calculate, this actually has some logic, here I carefully sorted out.

The code is sorted as follows:

// Gregorian calendar month table calculation table of each lunar day
const solarToLunarMonthTable = function(firstDay, days) {
  let firstMoonDay = solarToLunar(firstDay.years, firstDay.months + 1, firstDay.date);
  let curY = firstMoonDay.lunarY,
    curM = firstMoonDay.lunarM,
    curD = firstMoonDay.lunarD,
    leap = firstMoonDay.isLeap;

  // Check whether the current leap year leap month
  let leap_m = leapMonth(curY);
  let isleap = false;
  if(leap_m === curM) {
    isleap = true;
  }

  // Get the number of days of each lunar month in the current year
  let moonMonthDays = lunarMonthDays(curY);
  let moonMonthTotal;
  if(moonMonthDays.length === 12 || (moonMonthDays.length > 12 && curM < leap_m) || (moonMonthDays.length > 12&& curM === leap_m && ! leap)) { moonMonthTotal = moonMonthDays[curM -1];
  } else {
    moonMonthTotal = moonMonthDays[curM];
  }

  for(let i = 0, len = days.length; i < len; i++) {
    if(moonMonthTotal < curD) {
      if(! isleap || leap) { curM++; } curD =1;
      if(curM > 12) {
        curY++;
        curM = 1;
        curD = 1;
        moonMonthTotal = lunarMonthDays(curY)[0];
      }
      if(isleap) leap = ! leap; } days[i].lunarY = curY; days[i].lunarM = curM; days[i].lunarD = curD ===1 ? toChinaMonth(curM, leap) : toChinaDay(curD);
    days[i].isLeap = leap;
    curD++;
  }
  return days;
}
Copy the code

With the above method of converting lunar calendar information, we can insert the above method of createMonthDays to create the lunar calendar information, and the lunar calendar is completed. Careful friends may notice that there is a compensation code of February 1901 in the solarToLunar method in the conversion of gregarian-day to lunar day. This is because we set the date of the base point, and the initial value of offset is negative and is calculated (i.e. the lunar calendar of 1901.2.19 is not calculated, so when calling the method, The lunar calendar is undefined before February 19, 1901.therefore, we need to supplement the lunar data of the first day in the calendar of February 19, 1901.2. We are not going to supplement the lunar calendar information of 1901.1. We can manually control the selection of January 1901 when setting the calendar. If January is selected first and then 1901 is selected, we can forcibly switch to February. It should be especially noted that since we initially set the weekly display mode as Sunday, the data need to be adjusted if it is On Monday.)