天堂的另一角

天堂魷魚的原创技术博客。所謂兼容並包,無奇不有。

 

中國農曆計算的javascript實現方法

中國農曆很有意思,因為它並不是如英文名所表達的那種純月亮曆,而是結合了月曆和太陽曆(公曆)的一種曆法,實際上是一種日月混合曆,因此計算方法非常複雜。而更麻煩的是,幾乎所有的主流編程語言和函數庫中都沒有提供農曆的實現。因此,程序中需要計算農曆時,就很容易遇到各種各樣的問題。近期為了寫一個個人web日記系統,不得已學習了一番,並將研究結果發佈在此,以供討論和參考。

當前比較廣泛應用的是一段javascript實現(參考這裡),功能比較強大,支持閏月、節氣、公曆節日、農曆節日等。這段代碼的原始來源已不可考,不過大致可以判斷其最初應該是自台灣流出。估計當前網上的萬年曆系統應該有90%能夠找到其蹤影,其應用的廣泛程度可見一斑。但是,今年曾經有過立春日期的爭論,其罪魁禍首也正是這段代碼。根據這段代碼,2009年的立春是2月3日,而實際的天文觀測數據表明應該是2月4日。許多萬年曆由於使用了這套系統,導致出現了大量的不一致,並引起了人們的關注。由於實際時間只差不到1小時(2009年立春開始於2月4日凌晨0點40分左右),官方把這一問題解釋為「時差」。事實上,這是因為該代碼的節氣算法誤差過大造成的。

所有的曆法都需要天文觀測數據加以校正,但農曆對天文數據的依賴遠比公曆(即格里高里曆)大。因此,任何農曆計算程序都需要若干數據表才有可能完成計算,代碼非常繁雜。

首先是年基礎數據表,形式通常如下:

var lunarInfo=new Array(
0x04bd8,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,0x06e95,0x05ad0,
0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950,
0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,
0x025d0,0x092d0,0x0d2b2,0x0a950,0x0b557,
0x06ca0,0x0b550,0x15355,0x04da0,0x0a5d0,
0x14573,0x052d0,0x0a9a8,0x0e950,0x06aa0,
0x0aea6,0x0ab50,0x04b60,0x0aae4,0x0a570,
0x05260,0x0f263,0x0d950,0x05b57,0x056a0,
0x096d0,0x04dd5,0x04ad0,0x0a4d0,0x0d4d4,
0x0d250,0x0d558,0x0b540,0x0b5a0,0x195a6,
0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,
0x06a50,0x06d40,0x0af46,0x0ab60,0x09570,
0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,
0x06b58,0x055c0,0x0ab60,0x096d5,0x092e0,
0x0c960,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,0x04bd7,0x04ad0,
0x0a4d0,0x1d0b6,0x0d250,0x0d520,0x0dd45,
0x0b5a0,0x056d0,0x055b2,0x049b0,0x0a577,
0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0);


這實際上是一份農曆月份信息表,表格里的每個元素都是一個長度為20的二進制數組(用一個5位16進制數字表示)。詳細信息可以參考這裡的說明(當然,那篇文章存在不少問題)。幾乎所有的農曆計算程序都會用到這個表,不同的只是形式和範圍。這裡附上的表格包含了從公元1900年到2049年的信息。超出這個範圍的年份自然是無法計算的。

日期計算的代碼比較通用,如下所示:

//====================================== 傳回農曆 y年的總天數
function lYearDays(y) {
  
var i, sum = 348
  
for(i=0x8000; i>0x8; i>>=1) sum += (lunarInfo[y-1900& i)? 10
  
return(sum+leapDays(y))
}

//====================================== 傳回農曆 y年閏月的天數
function leapDays(y) {
  
if(leapMonth(y)) return((lunarInfo[y-1900& 0x10000)? 3029)
  
else return(0)
}

//====================================== 傳回農曆 y年閏哪個月 1-12 , 沒閏傳回 0
function leapMonth(y) {
  
return(lunarInfo[y-1900& 0xf)
}

//====================================== 傳回農曆 y年m月的總天數
function monthDays(y,m) {
  
return( (lunarInfo[y-1900& (0x10000>>m))? 3029 )
}

//====================================== 算出農曆, 傳入日期物件, 傳回農曆日期物件
//
 該物件屬性有 .year .month .day .isLeap .yearCyl .dayCyl .monCyl
function Lunar(objDate) {

  
var i, leap=0, temp=0
  
var baseDate = new Date(1900,0,31)
  
var offset = (objDate - baseDate)/86400000
 
  
this.dayCyl = offset + 40
  
this.monCyl = 14
 
  
for(i=1900; i<2050 && offset>0; i++) {
    temp 
= lYearDays(i)
    offset 
-= temp
    
this.monCyl += 12
  }
 
  
if(offset<0) {
    offset 
+= temp;
    i
--;
    
this.monCyl -= 12
  }
 
  
this.year = i
  
this.yearCyl = i-1864
 
  leap 
= leapMonth(i) //閏哪個月
  this.isLeap = false
 
  
for(i=1; i<13 && offset>0; i++) {
  
//閏月
    if(leap>0 && i==(leap+1&& this.isLeap==false)
    { 
--i; this.isLeap = true; temp = leapDays(this.year); }
    
else
    { temp 
= monthDays(this.year, i); }
   
    
//解除閏月
    if(this.isLeap==true && i==(leap+1)) this.isLeap = false
   
    offset 
-= temp
    
if(this.isLeap == falsethis.monCyl ++
  }
 
  
if(offset==0 && leap>0 && i==leap+1)
  
if(this.isLeap)
  { 
this.isLeap = false; }
  
else
  { 
this.isLeap = true--i; --this.monCyl;}
 
  
if(offset<0){ offset += temp; --i; --this.monCyl; }
 
  
this.month = i
  
this.day = offset + 1
}


關於這部份的計算比較好懂,也被大多人認同,我就不多解釋了。按中國的習慣,月份需要用一個字表示(如「臘月」),日期則統一按兩個漢字表示(如「初五」),調用時還需要稍微調整。調用代碼如下所示

var numString="十一二三四五六七八九十";
var lMString="正二三四五六七八九十冬臘";

function getLunarDateStr(date){
    
var tY = date.getFullYear();
    
var tM = date.getMonth();
    
var tD = date.getDate();
    
var l = new Lunar(date);
    
var lM = l.month.toFixed(0);
    
var pre = (l.isLeap) ? '閏' : '';
    
var mStr = pre + lMString[lM-1+ '月';
    
var lD = l.day.toFixed(0- 1;
    pre 
= (lD <= 10? '初' : ((lD <= 19? '十' : ((lD <= 29? '廿' : '三'));
    
var dStr = pre + numString[lD % 10];
    
return mStr + dStr;
}


接著就是重頭戲──節氣了!!在原來的代碼中,已經是可以計算節氣了。由於節氣計算非常複雜,代碼只提供了一個函數,形式如function sTerm(year, n),用來計算某年的第n個節氣(從0小寒算起)為幾號,這也基本被認可為節氣計算的基本形式。由於每個月份有兩個節氣,計算時需要調用兩次(n和n+1),調用代碼如下:

var solarTerm = new Array("小寒","大寒","立春","雨水","驚蟄","春分","清明",
"谷雨","立夏","小滿","芒種","夏至","小暑","大暑","立秋","處暑","白露","秋分",
"寒露","霜降","立冬","小雪","大雪","冬至");

var termStr = (tD == sTerm(tY, tM*2)) ? solarTerm[tM*2] : ((tD == sTerm(tY, tM*2+1)) ? solarTerm[tM*2+1] : '');


然而,造成2009年立春計算問題的關鍵也就在這裡。原代碼的sTerm()實現存在一定誤差,雖然大多數時候沒有問題,但因為2009年立春正好出現在凌晨0時,這個誤差就導致了日期被錯算成2月3日。(其實也就若干小時,完全是可容許範圍的說。。。)更精確的算法需要引入另外的數據表格,這一工作主要來自台灣林洵賢先生的「農曆月曆&世界時間 DHTML 程式」。相關代碼(引自IdeoCal)如下所示:

var solarTermBase = new Array(4,19,3,18,4,19,4,19,4,20,4,20,6,22,6,22,6,22,7,22,6,21,6,21);
var solarTermIdx = '0123415341536789:;<9:=<>:=1>?012@015@015@015AB78CDE8CD=1FD01GH01GH01IH01IJ0KLMN;LMBEOPDQRST0RUH0RVH0RWH0RWM0XYMNZ[MB\\]PT^_ST`_WH`_WH`_WM`_WM`aYMbc[Mde]Sfe]gfh_gih_Wih_WjhaWjka[jkl[jmn]ope]qph_qrh_sth_W';
var solarTermOS = '211122112122112121222211221122122222212222222221222122222232222222222222222233223232223232222222322222112122112121222211222122222222222222222222322222112122112121222111211122122222212221222221221122122222222222222222222223222232222232222222222222112122112121122111211122122122212221222221221122122222222222222221211122112122212221222211222122222232222232222222222222112122112121111111222222112121112121111111222222111121112121111111211122112122112121122111222212111121111121111111111122112122112121122111211122112122212221222221222211111121111121111111222111111121111111111111111122112121112121111111222111111111111111111111111122111121112121111111221122122222212221222221222111011111111111111111111122111121111121111111211122112122112121122211221111011111101111111111111112111121111121111111211122112122112221222211221111011111101111111110111111111121111111111111111122112121112121122111111011111121111111111111111011111111112111111111111011111111111111111111221111011111101110111110111011011111111111111111221111011011101110111110111011011111101111111111211111001011101110111110110011011111101111111111211111001011001010111110110011011111101111111110211111001011001010111100110011011011101110111110211111001011001010011100110011001011101110111110211111001010001010011000100011001011001010111110111111001010001010011000111111111111111111111111100011001011001010111100111111001010001010000000111111000010000010000000100011001011001010011100110011001011001110111110100011001010001010011000110011001011001010111110111100000010000000000000000011001010001010011000111100000000000000000000000011001010001010000000111000000000000000000000000011001010000010000000';

//===== 某年的第n個節氣為幾日(從0小寒起算)
function sTerm(y,n) {
   
return(solarTermBase[n] +  Math.floor( solarTermOS.charAt( ( Math.floor(solarTermIdx.charCodeAt(y-1900)) - 48* 24 + n  ) ) );
}


這種方法大幅提昇了節氣計算的準確程度,也解決了2009年立春的問題。不過實話實說,這段代碼我是完全沒看懂的。數學白痴,殘念。。。

最後,感謝那些研究出這套算法的人們,沒有他們的工作,我們在電腦上使用農曆(哪怕是不準確的)將非常麻煩。另外,再次感慨一下,希望我國的天文台能盡一下本分,拿出一個標準的農曆計算方案來。也希望我們的農曆能夠早日進入主流語言函數庫。

posted on 2009-12-30 11:46 Addone 阅读(7818) 评论(9)  编辑 收藏 引用 所属分类: 软件开发

评论

# re: 中國農曆計算的javascript實現方法 2009-12-30 23:32 Addone

@99读书人俱乐部
嘛,不怪你,咱祖宗的曆法太強大了。。。  回复  更多评论   

# re: 中國農曆計算的javascript實現方法 2010-01-20 14:11 海鲜池

神人,呵呵,楼主就是太强大了!  回复  更多评论   

# re: 中國農曆計算的javascript實現方法 2010-02-04 23:24 不锈钢水箱

学习了!楼主厉害!  回复  更多评论   

# re: 中國農曆計算的javascript實現方法 2010-09-28 22:11 一起读

写的不错 支持  回复  更多评论   

# re: 中國農曆計算的javascript實現方法 2012-02-13 06:24 Derek

怎樣用啊??? @_@"
找不到讓我輸入Date object 的function耶~  回复  更多评论   

# re: 中國農曆計算的javascript實現方法 2012-02-13 12:06 Addone

@Derek
嗯,貌似没有说清楚啊⋯⋯还是用红字标上吧。
直接调用getLunarDateStr(date)就可以了。返回的日期格式直接在里面就可以修改。
节气的话需要另外调用sTerm(year, n),方法是
var termStr = (tD == sTerm(tY, tM*2)) ? solarTerm[tM*2] : ((tD == sTerm(tY, tM*2+1)) ? solarTerm[tM*2+1] : '');
tD, tM什么的看getLunarDateStr()就明白了。

因为是侧重讲方法(虽然也没有讲清楚),所以调用方面并没有特别强调呃。。本来想做成控件形式发布的,不过貌似已经有不少了所以也没有太大的动力去做⋯⋯  回复  更多评论   

# re: 中國農曆計算的javascript實現方法 2013-03-22 18:35 雙胞胎

請問是否能夠說明一下, 用此代碼, 如何實現農曆換算為國曆的函式?  回复  更多评论   

# re: 中國農曆計算的javascript實現方法 2013-03-29 13:52 Addone

@雙胞胎
你好。该代码只能把国历转为对应的农历,无法直接进行逆运算。农历的相关计算还是比较复杂的。  回复  更多评论   

# re: 中國農曆計算的javascript實現方法 2014-06-17 15:50 donghanji

var lD = l.day.toFixed(0) - 1;
为何要讲lD减去一天呢?得到的农历日期都要少一天,应该是:
var lD = l.day.toFixed(0);
吧!
刚开始,还以为是为了对应var numString="十一二三四五六七八九十";字符串中,最前面增加了一个“十”,单仔细一琢磨不是的。
简单测试了下代码!发现这个问题。  回复  更多评论   

只有注册用户登录后才能发表评论。

导航

统计

公告


Addone,又名:天堂鱿鱼。
这里是我的技术博客。其他文章
作为“杂感”分类存档。
我的新思想主要发往新站:
幻想园
幻想园

欢迎光临

Linux注册用户

feedsky
抓虾
google reader
bloglines

联系方式




My status

常用链接

留言簿(11)

随笔分类(99)

随笔档案(69)

相册

友情链接

推荐站点

搜索

积分与排名

最新评论

阅读排行榜