农历算法
撰写时间:2020-04-23
修订时间:2023-05-16
十天干
甲、乙、丙、丁、戊、己、庚、辛、壬、癸
十二地支
子、丑、寅、卯、辰、巳、午、未、申、酉、戌、亥
六十甲子纳音表
每行各取天干、地支各10个,余2个地支,顺排在下一行。
甲 | 乙 | 丙 | 丁 | 戊 | 己 | 庚 | 辛 | 壬 | 癸 | |
---|---|---|---|---|---|---|---|---|---|---|
第一甲 | 甲子 | 乙丑 | 丙寅 | 丁卯 | 戊辰 | 己巳 | 庚午 | 辛未 | 壬申 | 癸酉 |
子 | 丑 | 寅 | 卯 | 辰 | 巳 | 午 | 未 | 申 | 酉 |
甲 | 乙 | 丙 | 丁 | 戊 | 己 | 庚 | 辛 | 壬 | 癸 | |
---|---|---|---|---|---|---|---|---|---|---|
第二甲 | 甲戌 | 乙亥 | 丙子 | 丁丑 | 戊寅 | 己卯 | 庚辰 | 辛巳 | 壬午 | 癸未 |
戌 | 亥 | 子 | 丑 | 寅 | 卯 | 辰 | 巳 | 午 | 未 |
甲 | 乙 | 丙 | 丁 | 戊 | 己 | 庚 | 辛 | 壬 | 癸 | |
---|---|---|---|---|---|---|---|---|---|---|
第三甲 | 甲申 | 乙酉 | 丙戌 | 丁亥 | 戊子 | 己丑 | 庚寅 | 辛卯 | 壬辰 | 癸巳 |
申 | 酉 | 戌 | 亥 | 子 | 丑 | 寅 | 卯 | 辰 | 巳 |
甲 | 乙 | 丙 | 丁 | 戊 | 己 | 庚 | 辛 | 壬 | 癸 | |
---|---|---|---|---|---|---|---|---|---|---|
第四甲 | 甲午 | 乙未 | 丙申 | 丁酉 | 戊戌 | 己亥 | 庚子 | 辛丑 | 壬寅 | 癸卯 |
午 | 未 | 申 | 酉 | 戌 | 亥 | 子 | 丑 | 寅 | 卯 |
甲 | 乙 | 丙 | 丁 | 戊 | 己 | 庚 | 辛 | 壬 | 癸 | |
---|---|---|---|---|---|---|---|---|---|---|
第五甲 | 甲辰 | 乙巳 | 丙午 | 丁未 | 戊申 | 己酉 | 庚戌 | 辛亥 | 壬子 | 癸丑 |
辰 | 巳 | 午 | 未 | 申 | 酉 | 戌 | 亥 | 子 | 丑 |
甲 | 乙 | 丙 | 丁 | 戊 | 己 | 庚 | 辛 | 壬 | 癸 | |
---|---|---|---|---|---|---|---|---|---|---|
第六甲 | 甲寅 | 乙卯 | 丙辰 | 丁巳 | 戊午 | 己未 | 庚申 | 辛酉 | 壬戌 | 癸亥 |
寅 | 卯 | 辰 | 巳 | 午 | 未 | 申 | 酉 | 戌 | 亥 |
则可演变为以下的六十甲子纳音表:
第一甲 | 甲子 | 乙丑 | 丙寅 | 丁卯 | 戊辰 | 己巳 | 庚午 | 辛未 | 壬申 | 癸酉 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
第二甲 | 甲戌 | 乙亥 | 丙子 | 丁丑 | 戊寅 | 己卯 | 庚辰 | 辛巳 | 壬午 | 癸未 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | |
第三甲 | 甲申 | 乙酉 | 丙戌 | 丁亥 | 戊子 | 己丑 | 庚寅 | 辛卯 | 壬辰 | 癸巳 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | |
第四甲 | 甲午 | 乙未 | 丙申 | 丁酉 | 戊戌 | 己亥 | 庚子 | 辛丑 | 壬寅 | 癸卯 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | |
第五甲 | 甲辰 | 乙巳 | 丙午 | 丁未 | 戊申 | 己酉 | 庚戌 | 辛亥 | 壬子 | 癸丑 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | |
第六甲 | 甲寅 | 乙卯 | 丙辰 | 丁巳 | 戊午 | 己未 | 庚申 | 辛酉 | 壬戌 | 癸亥 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
规律:
- 从横行来看,每隔10年,为一甲。
- 每一列的天干都是相同的。也即是说,任取一年,每到10年,都会遇到相同的天干循环来管你。
- 每12年,地支都是一样的。
- 从竖行来看,并非每个天干都能与每个地支有机会组合。每个天干都依序与地支左循环隔一位组合,故每天干都与固定的6个地支组合。
- 60一循环。每60年,天干与地支又循环回来了。
农历的表示方法
历法的不同表示
公历:2020年5月3日
农历:二〇二〇年四月初九
四柱:庚子 庚辰 丙午 辰时
农历的规律
- 农历新年始于正月初一,即日历上所标注的
春节
,也为二十四节气中的立春
。 - 从春节开始,年的干支开始变为新的年支,如从"庚子"年变为""辛丑"年。
- 每月分为大月和小月,大月的天数为30天,小月的天数为29天。
- 农历2月较为特殊,闰年2月有29天,平年2月有28天。
- 一年可能有一个闰月。如闰五月,月建同属五月。但日干支不会相同,而是按六十甲子纳音表在走。
2000年1月1日的农历为:
己卯年 丙子月 戊午日 (十一月廿五)
更可简单地表示为:
己卯 丙子 戊午 (十一月廿五)
而2000年1月2日的农历为:
己卯 丙子 己未 (十一月廿六)
戊午的次序为55,己未的次序为56,则说明每天都以一个纳音表元素来表示。所以,如果知道某一天的日干支,则过了特定的天数后,其日干支是可以求得出来的。
例如,2000年1月1日的日干支为戊午,28日,其日干支是什么?
我们先看值为0时,该时间的北京时间的日干支是什么。
我们也可以用字符串的方式来构造同一时间:
查万年历,其日干支为辛巳。纳音值为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:0 | 1970/1/1 上午8:00:00 |
现在,将北京时间调为1969/12/31 下午23:00:00(往前数9个小时)。毕竟,周易的时间以北京时间为准。
日干支开始 | 格林尼治时间 | 北京时间 |
---|---|---|
辛巳 | 1969/12/31 下午15:00:00 | 1969/12/31 下午23: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)的缩写。但它也同时是下面标准时间的缩写:
- Cuba Standard Time: 古巴标准时间
- Central Standard Time (USA): 美国中部标准时间
- Central Standard Time (Australia): 澳大利亚中部标准时间
Gregorian Calender, 格列高利历,即公历,亦称为阳历。
Lunar Calender, 农历,是以六十甲子纳音表为纪年依据的历法,融合了中国古人基于对宇宙世界的长期观察而总结出来的历法,是中华周易文化在历法中的的伟大应用。它是一种阴阳历,但在百姓间习惯称为阴历。
JavaScript日期的显示
下面的代码:
打印出当天的日期。可以看出,2020年4月23日的GMT时间是2020年4月23日,CST北京标准时间需要将GMT时间加上8个小时。
它与代码:
的效果是一样的。
函数toDateString()
则显示为:
函数toTimeString()
则显示为:
这是东八区的时间,而非GMT时间。
函数toISOString()
则显示为:
函数toUTCString()
则显示为:
GMT表示这是格林尼治标准时间,而不是本地时间。
函数toLocaleDateString()
则显示为:
此方法还可带有两个可选的参数:locales, options.
函数toLocaleTimeString()
则显示为:
此方法还可带有两个可选的参数:reserved1, reserved2.
函数toLocaleString()
则显示为:
此方法还可带有两个可选的参数:reserved1, reserved2.
JavaScript对农历的支持
先看这一段代码:
在六十甲子纳音表中,序号为37的干支是"庚子"。4代表农历四月,27代表农历廿七。合起来,则表示"农历庚子年四月廿七"。Wow!
没错,庚子年闰四月初五。
梦里寻她千百度,蓦然回首,那人却在灯火阑珊处。
就问你JavaScript够不够狠!
JavaScript日期的本质
对于Date对象,其内部存储了一个数值。该数值相对于1970年1月1日的格林尼治天文台旧址所在的零时区的零时。
同一时间的不同显示
构造一个值为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!
相隔一天的时间值是多少
对于1970年1月2日的零时,此值为86,400,000。这两个日期相差1天。
1天有多少毫秒?var ms = 24 * 60 * 60 * 1000;
给出了答案:也是86,400,000。这说明了,此值以毫秒作为单位而加1。每经过1毫秒,此值加1。
时间相减
日期相减的意义在于,可求出这两个日期相差多少毫秒。将此毫秒转化为相应的秒、分、时、天,就可以知道两个日期相差多少个相应的单位。
时间的比较
时间可以使用逻辑比较。比如大于,小于,等等。
JavaScript Date构造方法
无参数的构造
返回现在的时间。
参数为一个数值的构造
构造一个时间,其值为该参数。
参数所属:GMT时间。
参数为0, 对应于1970/1/1 上午8:00:00所存储的值。8:00,这是因为北京在于东八区,多了8小时。此时,格林尼治天文台的标准时间为1970/1/1 上午0:00:00。
此参数表示毫秒。如果需要构造上面日期的第二天,则需要将此值转换为多少毫秒。
参数为2个数值以上的构造
构造一个时间,参数格式为year, month, date, hours, minutes, seconds, ms。
参数所属:本地时间。
注意表示月份的参数。实际的月份需要加在表示月份的参数数值上加1。这是因为其内部表示月份的数值是从0开始的。所以0就表示1月份。
Date()
有一个比较隐蔽的特点:
第3个参数表示日期,在这里传入0值,将得到当月的最后一天的日期。这个特性,很有妙用!
参数为字符串的构造
返回以字符串表示的时间的日期。
参数所属:GMT时间或本地时间。
注意,当使用2020/04/01
时,时间改变为上午12:00
。上午12:00即当日的零时。如果是中午12:00,则显示为下午12:00
。因此,如果需要以本地时间的零时,则应以2020/04/01
的方式。
可以使用完整的字符串来表示:
其格式是YYYY-MM-DDTHH:mm:ss.sssZ
。T
用以表示随后部分是时间。Z
, 只能是大写,也可以省略。这种格式创以UTC时间来创建。
还有另外一种字符串的构造方法。
这种格式以本地时间来创建。但不能指定毫秒。
参数为另一Date的构造
虽然值相同,但却分属于两个不同的对象。
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月份。
数值 月份 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 11 11 12 - 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时间来创建的。
对于第一个例子,我们希望以本地时间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个小时。
对于上面的这些情况,需要格外小心,否则将因疏忽大意而意外踩雷。
以本地时间来创建日期
如无特殊原因,在绝大多数情况下,应是使用本地时间来创建日期。下面的方法以本地时间来创建。
第一个例子也是容易犯错的。我们传入3
,拟表示3月份,但由于在JavaScript内部,月份的索引值从0
开始,因此参数3
最终变成了4
月。
第二个例子,参数为2020/04/01
这种格式,省略了后面的时分秒,拟均视为0
值。结果本地时间也确实均为0
值。
第三个例子,参数同样采用了2020/04/01
这种格式,参数后面空一格后带时分秒,结果本地时间均按参数值生成。
结合上一节中的例子,我们似乎可以得出结论,即:以2020-04-01
的格式作为参数,将以GMT时间来创建时间;以2020/04/01
的格式作为参数,将以本地时间即CST时间来创建时间。
但糟糕的是,情况却非如此。
上面的例子,参数为2020-04-01
这种格式,后面同时跟有时分秒,结果是以本地时间来创建的。参数如果改为2020-04-01T00:02:56
,结果也一样,也是以本地时间来创建。
PHP在读取表单的时间值时,就以这种方式来解析时间,但少了秒:2020-04-01T00:02
。若以其值直接作为参数生成日期,结果与上面一样,以本地时间创建。
下面再看一个显式指定时区的情况。
这次,参数最后没有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,商为1,余2。这是单个数的取模。因此不难理解。
取模有一重要应用,即可实现当超过特定的数字时,自动反转,即将值限定在取值范围内。
可以看出,其值都在[1, 2, 3, 4, 0]范围内翻转。即当值为5的倍数时,结果就变为0。也可以说,其结果不会超过4。
一般的,当用n来取模时,其值在[0, 1, 2, 3, ..., n-1]内翻转。
这是仅从值域范围来看。但有时我们还需要精确地控制,当原值为多少,转换值应为多少。
例如,对于十六进制的转换:
注意变量i
,从0值开始,而不是从1值开始。这时,0转换为0,其他值转换为[1, 15]内的值,16的倍数转换为0,非常完美地应用于十六进制的转换。
看看月份的用例。对于特定的一个数值,它是多少月?
由于不会出现0月,因此,变量i
从1值开始,先写出以下代码:
值域为[0, 12]。注意,每个周期共有13个数,这不对。此外,13的倍数变为0。结果中出现了0月,逻辑不对。13的倍数应变为1。
先将13调为12。
结果是,值域为[0, 11],12变为0。13变为1, 14变为2。除了12的倍数不对,其他的均符合要求。这种情况下,只需将结果0变为12就行了。
转换星期几的方法与上面的方法完全一致。只需将12改为7即可。
小时:[0, 23]。分钟、秒钟:[0, 59]。由于可以允许0值出现,此时可非常完美地应用简单地取模。
总结如下:
- y = x % n, y的值域为[0, 1, 2, 3, ..., n-1]。
- 如果值域允许0出现,简单地取模进行。
- 如果值域不允许0出现,只需简单地将0值设为值域最大值即可。