如何实现日历(公历+农历)

吐槽君 分类:javascript

如何实现日历(公历+农历)

喜欢,请让我知道 ^-^,可以给大家整理一些有关农历的相关知识。

关于日历

​ 在我们现在的日常工作、生活中,日历通常指代公历,又称为“阳历”、“太阳历”。公历是以地球绕太阳公转的运动周期为基础而制定的历法,一个公历年近似等于一个回归年,公历的历年平均长度与回归年只有26秒之差,要累积3300年才差一日。目前为世界上大多数国家通用,并以公元作为纪年。

​ 公历的规律很明显,我们可以利用内置的Date对象很容易实现一个万年历。这里,我们主要探讨农历实现方法,农历是我国的一种历法,又称“夏历”、“中历”、“旧历”,俗称“阴历”。定月的方法是用朔望月周期给出,朔所在日为初一,朔望月长约29天半,所以农历大月30天,小月29天。农历平年有十二个月,全年354天或355天,闰年为十三个月,其中某一月为闰月,月名依前一月名而定(例如:前月是八月,闰月则为闰八月),闰年全年383天或384天。一个月的大小,取决于天文观测的结果,与规则无关。

农历的效果图

我们先要定一个小目标,目标明确了,我们只要朝着目标靠近就行了。

日历.png

实现方法

公历的实现

​ 每一台日历,都有一个默认状态,我们也不例外,默认当前年月日。公历月历表可分为三个模块,上月日历日,本月日历日,下月日历日(如图所示:)。

公历.png

​ 我们这里从默认值new Date()着手,获取当前月份的起始日、终止日,以及起始日的星期,接下来我们就可以分别获取当前月历表的上月日,当月日,下月日的基本信息情况。

/**
 * 创建当前月表包含的每一天(为了方便,这里我们引入了moment.js)
 * 
 * @param { string } date 传入的日期 (如:'2021-3-18')
 * @returns { array }  days 返回传入日所在月份表的所有日的数组
 */
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();
    
    // 获取上月日数据
    for (let i = 1; i <= weekday; i++) {
        let tempDay = moment([
            start.year(),
            start.month(),
            i
        ]).subtract(weekday, 'days');
        days.push(createDay(tempDay));
    }
    // 获取本月日数据
    let dateIterator = start.clone();
    while (dateIterator.isBefore(end) || dateIterator.isSame(end, 'day')) {
        days.push(
            createDay(dateIterator.clone())
        );
        dateIterator.add(1, 'days');
    }
    // 获取下月日数据
    while (days.length % 7 !== 0) {
        days.push(
            createDay(dateIterator.clone())
        );
        dateIterator.add(1, 'days');
    }
    
    return days;
}

/**
 * 创建当前日的信息,可用来对日对象数据进行定制化,以用来实现效果图中的哪些定制信息
 * 
 * @param { Object } day moment时间对象
 * @returns { object}  day 返回处理后的数据
 */
function createDay(day) {
    return {
        moment: day,
        day: day.date()
    }
}
 

至此,我们便可以顺利的完成公历的主要核心代码。

农历的实现

​ 关于农历,翻阅了不少资料,对于农历的产生,不得不佩服我们先人的智慧。由于我们农历推算规律不像公历那么明显,一些源数据是来自天文观测的结果,与规则无关,如下文所述。(这里要感谢香港天文台(www.hko.gov.hk/tc/gts/time…

/* 源数据说明:
*   lunarYear数据来自香港天文台提供的源数据反向推导得出,其中201项数据分别对应1900-2100年。
*   示例: 2021年 -- 0x06aa0
*   ╭-------┰-------┰-------┰-------┰--------╮
*   ┆ 0000  ┆ 0110  ┆ 1010  ┆ 1010  ┆ 0000   ┆
*   ┠-------╊-------╊-------╊-------╊--------┨
*   ┆ 20-17 ┆ 16-12 ┆ 12-9  ┆  8-5  ┆  4-1   ┆
*   ╰-------┸-------┸-------┸-------┸--------╯
*   1-4: 表示当年有无闰年,有的话,为闰月的月份,没有的话,为0。 2021年无闰月
*   5-16:为除了闰月外的正常月份是大月还是小月,1为30天,0为29天。从1月到12月对应的是第16位到第5位,2021年各月天数[29,30,30,29,30,29,30,29,30,29,30,29]
*   17-20: 表示闰月是大月还是小月,仅当存在闰月的情况下有意义。(0/1,即闰大/小月)
*/
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
]

​ 有了农历历年的原始据,根据上面对源数据的解析说明,我们可以获取公历年(1901-2100)中的任何一年的闰月、月份天数、大小月等情况。现在我们要解决的核心问题是如何让公历年中的某一天推算出农历信息。这里,确定一个基准点是核心,大致思路:

  • 对农历中用到的中文汉字进行定义,转化方法提供,按照规律编写解析源数据方法
  • 定参照基准日期 1901-02-19 ,之所以定公历的这一天,是因为该日是1901年的农历第一天,正月初一(即春节)
  • 计算待转化日与基准日相差的天数
  • 利用源数据以及上一步计算出的天数,推算出待转化日的农历年/月/日
  • 计算公历月历表中每一日对应的农历信息,为了避免计算月历表中的每一天的农历信息,我们需要根据月历表中第一条数据的农历信息推算整月各日信息。
// ['月','正','一','二','三','四','五','六','七','八','九','十','冬','腊'];
const ChinaMonths = ["\u6708","\u6b63","\u4e8c","\u4e09","\u56db","\u4e94","\u516d","\u4e03","\u516b","\u4e5d","\u5341","\u51ac","\u814a"]
// ['日','一','二','三','四','五','六','七','八','九','十']
const ChinaDay = ["\u65e5","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d","\u4e03","\u516b","\u4e5d","\u5341"]
// ['初','十','廿','卅','闰']
const ChinaElement = ["\u521d","\u5341","\u5eff","\u5345", "\u95f0"]
// 农历日中文显示,参数日期day
const toChinaDay = function(day) {
let str = '';
switch(day) {
case 10: 
str = '\u521d\u5341';break; // "初十"
case 20:
str = '\u5eff\u5341';break; // "廿十"
case 30: 
str = '\u5345\u5341';break; // "卅十"
default: 
str = ChinaElement[Math.floor(day/10)] + ChinaDay[day%10];
}
return str
}
// 农历月初一中文月显示(如农历二月初一 -> 二月,农历闰四月初一 ->闰四月)
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();
return {
y: now.getFullYear(),
m: now.getMonth()+1,
d: now.getDate()
};
}
// 某年农历闰月月份
const leapMonth = function(year) {
year = year || nowInfo().y;
return lunarYears[year - 1900] & 0xF;
}
// 某年农历闰月天数
const leapDays = function(year) {
year = year || nowInfo().y;
if(leapMonth(year)) {
return (lunarYears[year-1900] & 0x10000) ? 30 : 29;
}
return 0;
}
// 某年份农历各月天数
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();
// 添加闰月
let leapM = leapMonth(year);
if(leapM) monthDays.splice(leapM, 0 , leapDays(year));
return monthDays;
}
// 某年农历天数
const lunarYearDays = function(year) {
year = year || nowInfo().y;
let num = 0;
lunarMonthDays(yar).forEach(item => {
num += item;
});
return num;
}

推算公历某日对应的农历日,逻辑清楚了,代码也就不难了。

const solarToLunar = function(y,m,d) {
if(y < 1901 || y > 2100) return -1;
let date;
if(!y) {
date = new Date();
} else {
date = new Date(y,m-1,d);
}
// 参照日期 1901-02-19 正月初一
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--;
}
// 农历年、月、日
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 {
// 补偿公历1901年2月的农历信息
if(offset == -23) {
return {
lunarY: i,
lunarM: 12,
lunarD: 8,
isLeap: false
}
}
}
// 矫正闰年月
if(leapM) {
if(j == leapM + 1) {
isLeap = true
}
if(j >= leapM + 1) {
j--
}
}
return {
lunarY: i,
lunarM: j,
lunarD: ++offset,
isLeap: isLeap
}
}

​ 至此,按理我们本应该能够实现1900-2100年的所有日的转化,但是仔细想一下,如果每一日都要不断循环200年的农历源数据去计算的话,未免不太像话。因此,对于月历表,我们应该只计算第一条数据,后面的每日应该去逐个推算才比较合理。关于如何推算,这其中其实还是有一些逻辑,这里我细细的整理了一下。

公历月对等转化农历.png

代码整理如下:

// 公历月表推算表中各农历日
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;
// 判断当前是否为闰年闰月
let leap_m = leapMonth(curY);
let isleap = false;
if(leap_m === curM) {
isleap = true;
}
// 获取当前年份各农历月天数, 首先获取当前月在当前农历年份中的天数
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;
}

有了上述月历表农历信息转化方法,就可以插入上文中创建月历表公历信息的方法 createMonthDays ,这个农历才算完成。有细心的朋友可能会注意到公历日转农历日中的 solarToLunar 方法中一段1901年2月的补偿代码,这是由于我们定了基准点日期,且对 offset 初始值为负数时并为计算(即对1901.2.19的农历没有进行推算,因此调用方法时,1901.2.19日前农历日都是 undefined, 因此我们需要补充1901.2月月历表中第一日的农历数据。对于1901.1月份的农历信息我们就不打算补充了,我们可以在日历的选择设置的时候人为控制不给选1901年1月,如果先选定了1月再选1901年,可以强制切换到2月,当然如果有必要,还是可以的 。 特别要注意的时,由于我们最初定的星期展示方式为每周开始日期为周日,如果时周一,这里需要对数据进行调整

作者:掘金-如何实现日历(公历+农历)

一线大厂高级前端编写,前端初中阶面试题,帮助初学者应聘,需要联系微信:javadudu

回复

我来回复
  • 暂无回复内容