萬年歷大家肯定都用過,一般都有陽歷、農歷、節氣等信息,但是你是否想過農歷日期是如何獲取的?
陽歷日期的獲取很簡單,以 Javascript 為例,有 Date 對象,可以調用它的 API 獲取年、月、日信息,但是農歷日期並不像陽歷一樣有規律,更別談 API 了。所以,對於農歷日期的獲取我們只能打表。
納尼,打表?豈不是 1 年 365 天要打 365 個值的表么?非也。我們先來了解下農歷的一些基本信息,農歷一年有 12 個月或者 13 個月,每個月 29 天或者 30 天。如果哪一年有 13 個月,那么多出來的月叫做閏月,比方說農歷 2001 年有兩個四月,那么多出來的四月就叫做閏四月,緊跟在四月后,通常每 19 年就會有 7 個閏月,於是你可能在某一年過過兩次農歷生日。(詳見 歷法)
如果今天是 2000 年 1 月 1 日(陽歷),讓你求 100 天后的日期(陽歷),相信並不會很困難(甚至可以直接用 setDate() 方法)。如果今天是 2000 年 1 月 1 日(農歷),讓你求 100 天后的日期(農歷),怎么做?沒錯,我們需要的僅僅只是農歷月份的一些數據而已,而這些數據我們不得不通過一些渠道去獲得。
這樣,求解當天農歷日期的過程似乎可以呼之欲出了。我們以某天為基准,比方說 2000 年 2 月 5 日(農歷正月初一),我們先算得今天和基准日期的時間天數間隔(假設為 n 天),然后再去求農歷 2000 年正月初一之后 n 天的日期。
將每一年的農歷月份信息存儲下來也需要一定的技巧,這里采用二進制進行存儲。用 12 位二進制代表 12 個月的年份,用 13 位代表 13 個月的年份。在每個位置上,用 1 表示當月有 30 天,0 表示 29天。
比方說 2000 年我們可以用 110010010110
表示,它表示一月、二月、五月、八月、十月以及十一月有30天,其余 29 天,為了方便我們將該數用十六機制 0xc96 表示。有閏月的年份怎么表示呢?比方說 2001 年有個閏四月,我們用 13 個二進制數 1101010010101
表示該年,其中第 5 個數字 0 表示閏四月有 29 天,但是因為 13 個二進制數表示十六進制不方便,我們再在前面加上 3 個 0,0001, 1010, 1001, 0101
,最后我們還得知道閏月是在幾月份,我們將其加在最前面,比如 2001 年是閏四月,四的二進制表示為 0100
,我們將它加在最前面,0100, 0001, 1010, 1001, 0101
,用十六進制表示為 0x41A95。
代碼很簡答,都有注釋:
// 僅使用於陽歷2001年-2020年之間陽歷-農歷轉換
function getLunarDay(y, m, d) {
// 2000 - 2020
// 0xC96 110010010110
var data = [0xc96, 0x41A95, 0xD4A, 0xDA5, 0x20B55, 0x56A, 0x7155B, 0x25D, 0x92D, 0x5192B,
0xA95, 0xB4A, 0x416AA, 0xAD5, 0x90AB5, 0x4BA, 0xA5B, 0x60A57, 0x52B, 0xA93, 0x40E95];
// 以 2000 年 2 月 5 日為基准(農歷正月初一)
var standard = new Date(2000, 1, 5);
// 將日期對象化
var cur = new Date(y, m, d);
// 間隔的日期天數
var gap = (cur.getTime() - standard.getTime()) / 1000 / 3600 / 24 + 1;
// 確認是不是 i 年
for (var i = 2000; i <= 2020; i++) {
var yearDays = getYearDays(i);
// 就是 i 年
if (gap <= yearDays) {
var leapMonth = getLeapMonth(i)
, totalMonths = leapMonth ? 13 : 12;
// 遍歷月份
for (var j = 0; j < totalMonths; j++) {
var days = data[i - 2000] & (1 << (totalMonths - j - 1)) ? 30 : 29;
// 不是 j 月
if (gap > days) {
gap -= days;
} else {
if (leapMonth) { // 如果當年有閏月,還需判斷
if (j < leapMonth)
return [i, j + 1, gap, 0];
else if (j === leapMonth)
return [i, j, gap, 1];
else
return [i, j, gap, 0];
} else {
return [i, j + 1, gap, 0]; // i 年 j+1 月 gap 日,0 表示非閏月
}
}
}
} else {
gap -= yearDays;
}
}
// 獲取 year 年的閏月月份,如果沒有,返回 0
function getLeapMonth(year) {
year -= 2000;
var month = 0;
for (var i = 16; i < 20; i++)
month += data[year] & (1 << i);
return month >> 16;
}
// 獲取 year 年農歷總天數
function getYearDays(year) {
var leapMonth = getLeapMonth(year)
, totalMonths = leapMonth ? 13 : 12;
var totalDays = 0;
// 2001
// 0100, 0001, 1010, 1001, 0101
for (var i = 0; i < totalMonths; i++) {
var tmp = totalMonths - i - 1;
if (data[year - 2000] & (1 << tmp))
totalDays += 30;
else
totalDays += 29;
}
return totalDays;
}
}
// 調用
var now = new Date();
var ans = getLunarDay(now.getFullYear(), now.getMonth(), now.getDate());
console.log(ans);
var tmp = getLunarDay(2017, 7, 18); // 2017-8-18
console.log(tmp); // [2017, 6, 27, 1]
除了一些計算外,最關鍵的當屬將一年的農歷月份信息用一個整數來保存,除了上述的方法外,當然還有很多信息壓縮的辦法。比如有一種這樣運算,先跟上文說的一樣將 12 個月的信息用 12 個二進制數表示,比如 2000 年表示為 110010010110
,然后在末尾添加 4 位表示閏月月份,沒有閏月用 0 表示,比如 2000 年表示為 110010010110,0000
。對於 2000 年這樣就算結束了,但是 2001 年表示為 110110010101,0100
后,並不知道閏月是多少天,我們規定,如果是 30 天就在前面再添加個 1,比如 2017 年是閏六月大,用二進制表示為 1,010100010111,0110
這樣,對於 2017 年來說,它能用二進制 10101000101110110 表示,也能用十進制 86390 表示,也能用十六進制 0x15176 表示,為了壓縮字節碼節省空間,我們用 36 進制數 1unq 表示(toString() 方法參數范圍2-36)。
這里把 1901-2050 的 150 個 36 進制表示字符串列一下,有需要的可以直接獲取。
var map = 'ezc|esg|wog|gr9|15k0|16xc|1yl0|h40|ukw|gya|esg|wqe|wk0|15jk|2k45|zsw|16e8|yaq|tkg|1t2v|ei8|wj4|zp1|l00|lkw|2ces|8kg|tio|gdu|ei8|k12|1600|1aa8|lud|hxs|8kg|257n|t0g|2i8n|13rk|1600|2ld2|ztc|h40|2bas|7gw|t00|15ma|xg0|ztj|lgg|ztc|1v11|fc0|wr4|1sab|gcw|xig|1a34|l28|yhy|xu8|ew0|xr8|wog|g9s|1bvn|16xc|i1j|h40|tsg|fdh|es0|wk0|161g|15jk|1654|zsw|zvk|284m|tkg|ek0|xh0|wj4|z96|l00|lkw|yme|xuo|tio|et1|ei8|jw0|n1f|1aa8|l7c|gxs|xuo|tsl|t0g|13s0|16xg|1600|174g|n6a|h40|xx3|7gw|t00|141h|xg0|zog|10v8|y8g|gyh|exs|wq8|1unq|gc0|xf4|nys|l28|y8g|i1e|ew0|wyu|wkg|15k0|1aat|1640|hwg|nfn|tsg|ezb|es0|wk0|2jsm|15jk|163k|17ph|zvk|h5c|gxe|ek0|won|wj4|xn4|2dsl|lk0|yao'.split('|');
for (var i = map.length; i--; )
map[i] = parseInt(map[i], 36);
read more: