Web编程技术营地
研究、演示、创新

农历算法

撰写时间:2020-04-23

修订时间:2025-06-15

十天干

甲、乙、丙、丁、戊、己、庚、辛、壬、癸

十二地支

子、丑、寅、卯、辰、巳、午、未、申、酉、戌、亥

六十甲子纳音表

每行各取天干、地支各10个,余2个地支,顺排在下一行。

第一甲甲子乙丑丙寅丁卯戊辰己巳庚午辛未壬申癸酉

第二甲甲戌乙亥丙子丁丑戊寅己卯庚辰辛巳壬午癸未

第三甲甲申乙酉丙戌丁亥戊子己丑庚寅辛卯壬辰癸巳

第四甲甲午乙未丙申丁酉戊戌己亥庚子辛丑壬寅癸卯

第五甲甲辰乙巳丙午丁未戊申己酉庚戌辛亥壬子癸丑

第六甲甲寅乙卯丙辰丁巳戊午己未庚申辛酉壬戌癸亥

则可演变为以下的六十甲子纳音表:

第一甲甲子乙丑丙寅丁卯戊辰己巳庚午辛未壬申癸酉
12345678910
第二甲甲戌乙亥丙子丁丑戊寅己卯庚辰辛巳壬午癸未
11121314151617181920
第三甲甲申乙酉丙戌丁亥戊子己丑庚寅辛卯壬辰癸巳
21222324252627282930
第四甲甲午乙未丙申丁酉戊戌己亥庚子辛丑壬寅癸卯
31323334353637383940
第五甲甲辰乙巳丙午丁未戊申己酉庚戌辛亥壬子癸丑
41424344454647484950
第六甲甲寅乙卯丙辰丁巳戊午己未庚申辛酉壬戌癸亥
51525354555657585960

规律:

  1. 从横行来看,每隔10年,为一甲。
  2. 每一列的天干都是相同的。也即是说,任取一年,每到10年,都会遇到相同的天干循环来管你。
  3. 每12年,地支都是一样的。
  4. 从竖行来看,并非每个天干都能与每个地支有机会组合。每个天干都依序与地支左循环隔一位组合,故每天干都与固定的6个地支组合。
  5. 60一循环。每60年,天干与地支又循环回来了。

农历的表示方法

历法的不同表示

公历:2020年5月3日

农历:二〇二〇年四月初九

四柱:庚子 庚辰 丙午 辰时

农历的规律

  1. 农历新年始于正月初一,即日历上所标注的春节,也为二十四节气中的立春
  2. 从春节开始,年的干支开始变为新的年支,如从"庚子"年变为""辛丑"年。
  3. 每月分为大月和小月,大月的天数为30天,小月的天数为29天。
  4. 农历2月较为特殊,闰年2月有29天,平年2月有28天。
  5. 一年可能有一个闰月。如闰五月,月建同属五月。但日干支不会相同,而是按六十甲子纳音表在走。

2000年1月1日的农历为:

己卯年 丙子月 戊午日 (十一月廿五)

更可简单地表示为:

己卯 丙子 戊午 (十一月廿五)

而2000年1月2日的农历为:

己卯 丙子 己未 (十一月廿六)

戊午的次序为55,己未的次序为56,则说明每天都以一个纳音表元素来表示。所以,如果知道某一天的日干支,则过了特定的天数后,其日干支是可以求得出来的。

例如,2000年1月1日的日干支为戊午,28日,其日干支是什么?

我们先看值为0时,该时间的北京时间的日干支是什么。

var date = new Date(0); console.log(date.toLocaleString()); // 1970/1/1 上午8:00:00

我们也可以用字符串的方式来构造同一时间:

var date = new Date("1970-01-01"); console.log(date.toLocaleString()); // 1970/1/1 上午8:00:00

查万年历,其日干支为辛巳。纳音值为18.

但这有个问题。日干支何时起变化?

公历1970年1月1日0:0-1970年1月1日23:59是1月1日。再加1秒,则变成1970年1月2日的0:0了。而在农历中,一日的开始并非始于0:0,而是始于子时,即公历的前一天的23:00。例如,1970年1月1日,其日干支为辛巳,则辛巳始于前一日1969年12月31日的23:00,终于1月1日的22:59。

由此可见,对于时间值为0的时间,其北京时间应是1969年12月31日23:00。

先列出时间值为0的格林尼治标准时间及对应的北京标准时间。

时间值为0的时间格林尼治时间北京时间
new Date(0)1970年1月1日 上午0:0:01970/1/1 上午8:00:00

现在,将北京时间调为1969/12/31 下午23:00:00(往前数9个小时)。毕竟,周易的时间以北京时间为准。

日干支开始格林尼治时间北京时间
辛巳1969/12/31 下午15:00:001969/12/31 下午23:00:00

现在,以此时间来创建一个日期,并进行验证:

var date = new Date("1969-12-31T15:00:00.000Z"); console.log(date.toUTCString()); // Wed, 31 Dec 1969 15:00:00 GMT console.log(date.toLocaleString()); // 1969/12/31 下午11:00:00

可见,以字符串1969-12-31T15:00:00.000Z所创建的时间,正对应于北京时间1969/12/31 下午11:00:00

日干支的变化

每个时辰2个小时,不会随着农历初几的变化而变化。

农历2月的天数

农历2月较为特殊,闰年2月有29天,平年2月有28天。

农历闰月的干支

闰月的干支与本月的干支相同。例如,四月的干支为壬辰,则闰四月的干支也为壬辰。

历法常用术语

GMT, Greenwich Mean Time, 格林尼治标准时间, 是指位于伦敦郊区的皇家格林尼治天文台的标准时间,通过那里的经线称为本初子午线。以格林尼治天文台的经线为0度经线,将世界分为24个时区。北京位于为东八区,因此要比GMT早8个小时。

GMT的正午是指当太阳横穿格林尼治子午线时的时间。由于地球自转有些不规则,因此此时间会有变化,故现在GMT已经不再作为标准时间使用,而是使用UTC时间。UTC, Universal Time Coordinated, 协调世界时,是以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。自1924年2月5日开始,格林尼治天文台每隔一小时会向全世界发送UTC调时信息。在不要求精确到秒的情况下,在概念上GMT可以与UTC通用。

CST中国标准时间China Standard Time)的缩写。但它也同时是下面标准时间的缩写:

  1. Cuba Standard Time: 古巴标准时间
  2. Central Standard Time (USA): 美国中部标准时间
  3. Central Standard Time (Australia): 澳大利亚中部标准时间

Gregorian Calender, 格列高利历,即公历,亦称为阳历

Lunar Calender, 农历,是以六十甲子纳音表为纪年依据的历法,融合了中国古人基于对宇宙世界的长期观察而总结出来的历法,是中华周易文化在历法中的的伟大应用。它是一种阴阳历,但在百姓间习惯称为阴历

JavaScript日期的显示

下面的代码:

let date = new date() console.log(date); // Thu Apr 23 2020 07:41:13 GMT+0800 (CST)

打印出当天的日期。可以看出,2020年4月23日的GMT时间是2020年4月23日,CST北京标准时间需要将GMT时间加上8个小时。

它与代码:

var date = new date() console.log(date.toString());

的效果是一样的。

函数toDateString()则显示为:

Sat Apr 25 2020

函数toTimeString()则显示为:

01:43:21 GMT+0800 (CST)

这是东八区的时间,而非GMT时间。

函数toISOString()则显示为:

2020-04-24T17:34:43.314Z

函数toUTCString()则显示为:

Fri, 24 Apr 2020 17:45:03 GMT

GMT表示这是格林尼治标准时间,而不是本地时间。

函数toLocaleDateString()则显示为:

2020/4/25

此方法还可带有两个可选的参数:locales, options.

var dateOptions = {year: 'numeric', month: '2-digit', day: '2-digit'}; console.log(date.toLocaleDateString("zh-Hans-CN", dateOptions));

函数toLocaleTimeString()则显示为:

上午1:40:50

此方法还可带有两个可选的参数:reserved1, reserved2.

函数toLocaleString()则显示为:

2020/4/25 上午1:39:38

此方法还可带有两个可选的参数:reserved1, reserved2.

JavaScript对农历的支持

先看这一段代码:

var date = new Date(); console.log(new Intl.DateTimeFormat("zh-Hans-CN-u-ca-chinese").format(date)); // 37/4/27

在六十甲子纳音表中,序号为37的干支是"庚子"。4代表农历四月,27代表农历廿七。合起来,则表示"农历庚子年四月廿七"。Wow!

var date = new Date("2020/05/27 09:15:00"); console.log(new Intl.DateTimeFormat("zh-Hans-CN-u-ca-chinese").format(date)); // 37/闰4/5

没错,庚子年闰四月初五。

梦里寻她千百度,蓦然回首,那人却在灯火阑珊处。就问你JavaScript够不够狠!

JavaScript日期的本质

对于Date对象,其内部存储了一个数值。该数值相对于1970年1月1日的格林尼治天文台旧址所在的零时区的零时。

同一时间的不同显示

var date = new Date(0); console.log(date.valueOf()); // 0 console.log(date.toISOString()); // 1970-01-01T00:00:00.000Z console.log(date.toUTCString()); // Thu, 01 Jan 1970 00:00:00 GMT console.log(date.toLocaleString()); // 1970/1/1 上午8:00:00

构造一个值为0的时间。valueOf()方法可取出存储对Date对象中的这个值。

toISOString()方法,可精确地显示该时间的毫秒级。即是说,对于值为0的时间,为1970年1月1日00:00:00.000。

toUTCString()方法,可显示星期几,以及精确到秒的时间。GMT,表示这是格林尼治标准时间,即零时区的时间。

toLocaleString()方法,以本地表示时间的方法来显示时间。这里显示1970/1/1 上午8:00:00,表明,当格林尼治标准时间为1970/1/1 上午0:00:00时,北京时间在东8区,多8个小时,因此本地的北京为1970/1/1 上午8:00:00

所以这里应注意,1970年1月1日上午08:00的北京时间,才是值为0的时间。而北京时间为1970年1月1日上午0:00的时间,其时间并不等于0!

相隔一天的时间值是多少

var date1 = new Date("1970-01-01"); var date2 = new Date("1970-01-02"); console.log(date1.valueOf()); // 0 console.log(date2.valueOf()); // 86,400,000 var ms = 24 * 60 * 60 * 1000; console.log(ms); // 86,400,000

对于1970年1月2日的零时,此值为86,400,000。这两个日期相差1天。

1天有多少毫秒?var ms = 24 * 60 * 60 * 1000;给出了答案:也是86,400,000。这说明了,此值以毫秒作为单位而加1。每经过1毫秒,此值加1。

时间相减

var date1 = new Date("1970-01-01"); var date2 = new Date("1970-01-02"); var diff = date2 - date1; console.log(diff); // 86,400,000

日期相减的意义在于,可求出这两个日期相差多少毫秒。将此毫秒转化为相应的秒、分、时、天,就可以知道两个日期相差多少个相应的单位。

时间的比较

时间可以使用逻辑比较。比如大于,小于,等等。

var date1 = new Date("1970-01-01"); var date2 = new Date("1970-01-02"); if (date1 < date2) { ... }

JavaScript Date构造方法

无参数的构造

var date = new Date(); console.log(date.toLocaleString()); // 2020/4/25 上午2:51:09

返回现在的时间。

参数为一个数值的构造

var date = new Date(0); console.log(date.toLocaleString()); // 1970/1/1 上午8:00:00

构造一个时间,其值为该参数。

参数所属:GMT时间。

参数为0, 对应于1970/1/1 上午8:00:00所存储的值。8:00,这是因为北京在于东八区,多了8小时。此时,格林尼治天文台的标准时间为1970/1/1 上午0:00:00。

此参数表示毫秒。如果需要构造上面日期的第二天,则需要将此值转换为多少毫秒。

var date = new Date(24 * 60 * 60 * 1000); // 1天=24小时,1小时=60分,1分=60秒,1秒=1000毫秒 console.log(date.toLocaleString()); // 1970/1/2 上午8:00:00

参数为2个数值以上的构造

构造一个时间,参数格式为year, month, date, hours, minutes, seconds, ms。

var date = new Date(2020, 1, 3, 23, 15, 37, 735); // 本地时间 console.log(date.toLocaleString()); // 2020/2/3 下午11:15:37

参数所属:本地时间。

注意表示月份的参数。实际的月份需要加在表示月份的参数数值上加1。这是因为其内部表示月份的数值是从0开始的。所以0就表示1月份。

Date()有一个比较隐蔽的特点:

var date = new Date(2020, 1, 0); // 第3个参数传入0 console.log(date.toLocaleString()); // 2020/1/31 上午12:00:00

第3个参数表示日期,在这里传入0值,将得到当月的最后一天的日期。这个特性,很有妙用!

参数为字符串的构造

var date = new Date("2020-04-01"); // GMT console.log(date.toLocaleString()); // 2020/4/1 上午8:00:00 var date = new Date("2020/04/01"); // 本地时间 console.log(date.toLocaleString()); // 2020/4/1 上午12:00:00

返回以字符串表示的时间的日期。

参数所属:GMT时间或本地时间。

注意,当使用2020/04/01时,时间改变为上午12:00。上午12:00即当日的零时。如果是中午12:00,则显示为下午12:00。因此,如果需要以本地时间的零时,则应以2020/04/01的方式。

可以使用完整的字符串来表示:

var date = new Date("2020-07-22T00:00:00.000Z"); console.log(date.toLocaleString()); // 2020/7/22 上午8:00:00

其格式是YYYY-MM-DDTHH:mm:ss.sssZT用以表示随后部分是时间。Z, 只能是大写,也可以省略。这种格式创以UTC时间来创建。

还有另外一种字符串的构造方法。

var date = new Date("2020/04/01 09:00:15"); console.log(date.toLocaleString()); // 2020/4/1 上午9:00:15

这种格式以本地时间来创建。但不能指定毫秒。

参数为另一Date的构造

var date = new Date("2020-07-22"); var newDate = new Date(date); console.log(date === newDate); // false

虽然值相同,但却分属于两个不同的对象。

Date的方法

get

  • getDate(): 返回本地时间的多少日。如2020年4月26日,返回26。
  • getDay(): 返回本地时间的星期几。如2020年4月26日,返回0,代表星期天。
    数值星期几
    0星期日
    1星期一
    2星期二
    3星期三
    4星期四
    5星期五
    6星期六
  • getFullYear(): 返回本地时间的年数。如2020年4月26日,返回2020。
  • getHours(): 返回本地时间的小时。如2020年4月26日15:00,返回15。
  • getMilliseconds(): 返回本地时间的微秒。如2020年4月26日15:00:00.823,返回823。
  • getMinutes(): 返回本地时间的分钟。如2020年4月26日15:40:00,返回40。
  • getMonth(): 返回本地时间的月份。如2020年4月26日,返回3,代表4月份。
    数值月份
    01
    12
    23
    34
    45
    56
    67
    78
    89
    910
    1011
    1112
  • getSeconds(): 返回本地时间的秒数。如2020年4月26日15:40:25,返回25。
  • getTime(): 返回时间值。如1970年1月1日上午8:00,返回0。
  • getTimezoneOffset(): 返回零时区与本地时区的差,即零时区 - 本地时区,以分钟为单位。如东八区的北京时间,返回-480,代表相差480分钟。将其值除以60,得到-8小时。
  • getUTCDate(): 返回UTC的多少日。如2020年4月26日00:00:00,返回31。
  • getUTCDay(): 返回UTC的星期几。如2020年4月26日00:00:00,返回6,代表星期六。
    数值星期几
    0星期日
    1星期一
    2星期二
    3星期三
    4星期四
    5星期五
    6星期六
  • getUTCFullYear(): 返回UTC的年数。如2020年4月26日,返回2020。
  • getUTCHours(): 返回UTC的小时。如2020年4月26日00:00,返回16。

Date的不便利的地方

由于存在GMT时间与本地时间两种时间,开发者需时刻注意你在与哪个时间打交道。

以GMT时间来创建日期

在创建一个新日期时,下面的方法是以GMT时间来创建的。

var date = new Date(0); console.log(date.toLocaleString()); // 1970/1/1 08:00:00 var date = new Date("2020-04-01"); console.log(date.toLocaleString()); // 2020/4/1 08:00:00 var date = new Date("2020-04-01T00:00:00.000Z"); // ending with 'Z' console.log(date.toLocaleString()); // 2020/4/1 08:00:00

对于第一个例子,我们希望以本地时间1970年1月1日 00:00:00来创建时间,但结果本地时间却为1970年1月1日 08:00:00。这是因为参数表示的是GMT的时间,而不是本地时间。

第二个例子,参数为2020-04-01这种格式,省略了后面的时分秒,我们会想当然地认为将创建本地时间2020-04-01 00:00:00。但结果本地时间却为2020-04-01 08:00:00

第三个例子,参数字符串中带有T,且最后以Z结尾,表示以0时区的时间作为参数。因此,本地时间自然要加8个小时。

对于上面的这些情况,需要格外小心,否则将因疏忽大意而意外踩雷。

以本地时间来创建日期

如无特殊原因,在绝大多数情况下,应是使用本地时间来创建日期。下面的方法以本地时间来创建。

var date = new Date(2020, 3, 1); console.log(date.toLocaleString()); // 2020/4/1 00:00:00 var date = new Date("2020/04/01"); console.log(date.toLocaleString()); // 2020/4/1 00:00:00 var date = new Date("2020/04/01 09:00:15"); console.log(date.toLocaleString()); // 2020/4/1 09:00:15

第一个例子也是容易犯错的。我们传入3,拟表示3月份,但由于在JavaScript内部,月份的索引值从0开始,因此参数3最终变成了4月。

第二个例子,参数为2020/04/01这种格式,省略了后面的时分秒,拟均视为0值。结果本地时间也确实均为0值。

第三个例子,参数同样采用了2020/04/01这种格式,参数后面空一格后带时分秒,结果本地时间均按参数值生成。

结合上一节中的例子,我们似乎可以得出结论,即:以2020-04-01的格式作为参数,将以GMT时间来创建时间;以2020/04/01的格式作为参数,将以本地时间即CST时间来创建时间。

但糟糕的是,情况却非如此。

var date = new Date("2020-04-01 00:02:56"); console.log(date.toLocaleString()); // 2020/4/1 00:00:00

上面的例子,参数为2020-04-01这种格式,后面同时跟有时分秒,结果是以本地时间来创建的。参数如果改为2020-04-01T00:02:56,结果也一样,也是以本地时间来创建。

PHP在读取表单的时间值时,就以这种方式来解析时间,但少了秒:2020-04-01T00:02。若以其值直接作为参数生成日期,结果与上面一样,以本地时间创建。

下面再看一个显式指定时区的情况。

var date = new Date("2020-04-01T00:00:00.000+08:00"); // ending with '+08:00' console.log(date.toLocaleString()); // 2020/4/1 00:00:00

这次,参数最后没有Z了,代之以+08:00,表示参数是东八区的00:00:00.000,因此结果与本地时间一致。

个人偏好

综合上面各种情况来看,本人偏向于以2020-04-01 00:02:56的格式作为参数来生成日期。一是因为表示日期时,2020-04-01这种格式比较醒目,日常使用较多。二是2020-04-01 00:02:56也是PHP解析表单时间值的格式,这样不容易冲突。

取模算法在涉及日期计算中的应用

从数学上,取模%即求余数。如:

5 % 3 = 2

5除以3,商为1,余2。这是单个数的取模。因此不难理解。

取模有一重要应用,即可实现当超过特定的数字时,自动反转,即将值限定在取值范围内。

for (var i = 1; i <= 30; i++) { var result = i % 5; console.log(result); }

可以看出,其值都在[1, 2, 3, 4, 0]范围内翻转。即当值为5的倍数时,结果就变为0。也可以说,其结果不会超过4。

一般的,当用n来取模时,其值在[0, 1, 2, 3, ..., n-1]内翻转。

这是仅从值域范围来看。但有时我们还需要精确地控制,当原值为多少,转换值应为多少。

例如,对于十六进制的转换:

for (var i = 0; i <= 100; i++) { var result = i % 16; console.log(result); }

注意变量i,从0值开始,而不是从1值开始。这时,0转换为0,其他值转换为[1, 15]内的值,16的倍数转换为0,非常完美地应用于十六进制的转换。

看看月份的用例。对于特定的一个数值,它是多少月?

由于不会出现0月,因此,变量i从1值开始,先写出以下代码:

for (var i = 1; i <= 100; i++) { var result = i % 13; console.log(result); }

值域为[0, 12]。注意,每个周期共有13个数,这不对。此外,13的倍数变为0。结果中出现了0月,逻辑不对。13的倍数应变为1。

先将13调为12。

for (var i = 1; i <= 100; i++) { var result = i % 12; console.log(result); }

结果是,值域为[0, 11],12变为0。13变为1, 14变为2。除了12的倍数不对,其他的均符合要求。这种情况下,只需将结果0变为12就行了。

for (var i = 1; i <= 100; i++) { var result = i % 12; if (result === 0) { result = 12; } console.log(result); }

转换星期几的方法与上面的方法完全一致。只需将12改为7即可。

小时:[0, 23]。分钟、秒钟:[0, 59]。由于可以允许0值出现,此时可非常完美地应用简单地取模。

for (var i = 0; i <= 100; i++) { var result = i % 24; console.log(result); } for (var i = 0; i <= 100; i++) { var result = i % 60; console.log(result); }

总结如下:

  1. y = x % n, y的值域为[0, 1, 2, 3, ..., n-1]。
  2. 如果值域允许0出现,简单地取模进行。
  3. 如果值域不允许0出现,只需简单地将0值设为值域最大值即可。

新特性:Temporal

2025年,JavaScript新出了Temporal对象,支持农历算法。但目前正处于实验阶段,Chrome及Safari均尚未支持。

const dateTime = Temporal.Now.plainDateTimeISO(); pc.log(dateTime); // expected: 2025-01-22T11:46:36.144

本页面将自动跟踪所用浏览器是否支持该特性。如果有一天,看到上面的代码没有抛出异常,则说明所用浏览器已经支持该特性。

介绍:JavaScript Temporal is coming

文档:MDN: Temporal

规范:ECMAScript 2026: Temporal

参考资源

  1. ECMA262 Date Objects, 20.4 Date Objects
  2. 十二地支中正月为何建寅不建子
  3. 简单了解JavaScript操作XPath的一些基本方法
  4. 四柱预测学中如何月上起日?有什么捷诀?
  5. MDN, Intl.DateTimeFormat() constructor
  6. MDN, Intl
  7. The ECMAScript Internationalization API
  8. 《梅花易数》全本内容
  9. 关于前端:前端时间国际化入门
  10. BCP: 47
  11. MDN: JavaScript Temporal is coming