WebGL Tutorial
and more

农历算法

撰写时间:2020-04-23

修订时间:2023-05-16

十天干

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

十二地支

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

六十甲子纳音表

每行各取天干、地支各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值设为值域最大值即可。

参考资源

  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