kowala's home

kowala's home
這裡是我的學習筆記,陸續增加中。
http://kowala21.blogspot.com

2012-09-20

農曆程式概念探討(5)

kowala's home
http://kowala21.blogspot.com

前一篇 討論了編表規則,本篇就說明如何把日期資料取出來,原作對農曆主演算法是採用查表,計算過程是先設定一基準日做為第一天,然後計算某日距離基準日之累積日數,再對干支定位。這樣農曆基本日期就完成了,再來是潤飾部分,節氣、生肖、節慶、初十廿卅等等,最後定週休顏色等,這個已經是到了 HTML 語法,屬於介面顯示部分了。

先來看看從表格取出資料的函數

1.取出某年的總日數 lYearDays(y)

function lYearDays(y) {
   var i, sum = 348;
   for(i=0x8000; i>0x8; i>>=1)
      sum += (lunarInfo[y-1900] & i)? 1: 0;  
   return(sum+leapDays(y));
}


先把一年日數以小月29日放進去,再比對大月有幾個,加回去,最後是檢查有無潤月,潤大還是閏小,再加進去。

sum = 348 = 12*29


編表規則中,我們知道最右邊四位元是潤月,我們只需要用到 5-16 位元,代表月份,用紅色來表示,
下面的迴圈中,需要說明的是位元移位的概念,我們用二進位來看 i 就很清楚了。

for(i=0x8000; i>0x8; i>>=1)

i = 0x8000 = 1000 0000 0000 0000    i >>= 1 右旋一次
i = 0x4000 = 0100 0000 0000 0000    i >>= 1 右旋一次
i = 0x2000 = 0010 0000 0000 0000    i >>= 1 右旋一次
i = 0x1000 = 0001 0000 0000 0000    i >>= 1 右旋一次
i = 0x0800 = 0000 1000 0000 0000    i >>= 1 右旋一次
i = 0x0400 = 0000 0100 0000 0000    i >>= 1 右旋一次
i = 0x0200 = 0000 0010 0000 0000    i >>= 1 右旋一次
i = 0x0100 = 0000 0001 0000 0000    i >>= 1 右旋一次
i = 0x0080 = 0000 0000 1000 0000    i >>= 1 右旋一次
i = 0x0040 = 0000 0000 0100 0000    i >>= 1 右旋一次
i = 0x0020 = 0000 0000 0010 0000    i >>= 1 右旋一次
i = 0x0010 = 0000 0000 0001 0000    i >>= 1 右旋一次
i = 0x8; 停止

這樣就可以依序取出月份位元,在利用 AND i 把不要的位元消去,AND 運算符號為 &


lunarInfo[y-1900] & i

如果運算後值為1,代表是大月 30 天,就把 sum +1 否則 + 0

以1901年為例 lunarInfo[1] = 0x04ae0,代表月份的位元為

0x4ae = 0100 1010 1110
月份   = 1234 5678 9abc

若要取出 5 月,則 i = 0x0800 = 0000 1000 0000

    0100 1010 1110
& 0000 1000 0000
=          1

sum +1

最後是檢查該年有無潤月,若有則加潤月天數,使用函式 leapDays(y) 來求算。


2.檢查該年有無潤月

function leapMonth(y) {
   return(lunarInfo[y-1900] & 0xf)
}

這是直接取最右邊4個位元來比對,0表示無潤月,其他數值代表潤幾月。

lunarInfo[y-1900] = 0x04ae0


0x04ae0 = 0000 0100 1010 1110 0000
&       0xf = 0000 0000 0000 0000 1111
=                                                          0

3.傳回潤月天數

function leapDays(y) {
   if(leapMonth(y))  return((lunarInfo[y-1900] & 0x10000)? 30: 29)
   else return(0)
}

假設1  lunarInfo[y-1900] = 0x14b63
有潤月 leapMonth(y) =3

    0x14b63 = 0001 0100 1011 0110 0011
& 0x10000 = 0001 0000 0000 0000 0000
=                          1 結果會傳回 30

假設2  lunarInfo[y-1900] = 0x0b557
有潤月 leapMonth(y) =7

    0x0b557 = 0000 1011 0101 0101 0111
& 0x10000 = 0001 0000 0000 0000 0000
=                          0 結果會傳回 29

條件式 (bool)?true:false;
這等於

if (bool)
  true;
else
  false;

農曆主演算法

上述說明了如何查表取出相應的數值,接著說明如何求出某一日轉農曆日期的物件 Lunar(objDate)。

function Lunar(objDate){...}

objDate 是待查的日期物件,資料型態為 Date() 物件,我們可以賦予它一個值。

例如:2012-10-09

objDate = new Date(2012,9,9); //月份是從 0-11, 日是 1-31

這樣我們就指定了某一天給 objDate 物件了。

而在轉農曆日期的物件 Lunar() 中,使用一些屬性,隨後會透過計算而把值填進去下列屬性。

this.year    //陰曆年(陽曆年)[1]
this.month    //陰曆月(陽曆月)[1]
this.day    //陰曆日(陽曆日)[1]

this.yearCyl    //陰曆年
this.monCyl    //陰曆月
this.dayCyl    //陰曆日

this.isLeap    //潤年

該物件沒有方法,只是負責把指定了某一天轉成相應數值,填入上列屬性中。


轉換過程說明

Lunar() 物件被建立

1. 取基準日 1900-1-31 (此為庚子年農曆正月初一)

2. 將指定日減去基準日再轉豪秒為天數,存放於變數 offset,此為某日距離基準日的日數。

一日 = 24*60*60*1000 = 86400000 ms

offset = (objDate - baseDate) / 86400000;

3. 將此日數 offset 做干支定位

由於基準日是甲辰日,在干支表是排在第 41,所以 offset 必須 +40,這樣就正確定位了。

4. 將月數做干支定位

我們先將 offset 對基準月做一校正,基準月是丁丑月,在干支表是排在第 14,所以先把它 +14 完成初步定位。

干支表

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

月數計算比較複雜些,因為陰曆大小月份並不固定,必須查表逐一計算,從基準日到指定日經過了幾個月,再取出對應的干支,潤月則重複該月干支一次。

一開始先由某年總日數開始倒扣 offset,直到 offset 為負值,代表最後一年,不足一年,扣過頭須加回。
接著計算該年某月總日數,開始倒扣 offset,直到 offset 為負值,代表最後一月,不足一月,扣過頭須加回。
在計算某月時,還須考慮潤月,潤月須扣除 offset日數,但不可以累計月數,因為潤月重複該月干支一次。
最後剩下的offset就是日數了,如此完成了陰曆轉換。

這段說明請參考程式碼觀看,便知道我在說甚麼了。

//算出農曆, 傳入日期物件
function Lunar(objDate){  
   var i, leap=0, temp=0;  
   var baseDate = new Date(1900,0,31);//基準日 = 農曆第一天  
   var offset = (objDate - baseDate)/86400000; //一天 = 24*60*60*1000=86400000ms
  
   this.dayCyl = offset + 40;//日干支定位
   this.monCyl = 14;//月干支定位

   //--------------------------------------
   for(i=1900; i<2060 && offset>0; i++) {
      temp = lYearDays(i);//取i年的總天數
      offset -= temp;
      this.monCyl += 12;
   }
   //最後一年
   if(offset<0) {
      offset += temp;
      i--;  //西元 i 年
      this.monCyl -= 12;
   }
   this.year = i
   this.yearCyl = i-1864;//甲子年(公元1864年,1924年,1984年)

   //--------------------------------------
   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 == false) this.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;
}

待續...

[1] .依陳先生指出錯誤改正為農曆年月日

2 則留言:

  1. 而在轉農曆日期的物件 Lunar() 中,使用一些屬性,隨後會透過計算而把值填進去下列屬性。
    this.year //陽曆年
    this.month //陽曆月
    this.day //陽曆日
    this.yearCyl //陰曆年
    this.monCyl //陰曆月
    this.dayCyl //陰曆日

    year, month, day應該是農曆年月日而非陽曆。

    在下一篇裡也有使用到:
    lY = lDObj.year; //農曆年
    lM = lDObj.month; //農曆月
    lD = lDObj.day; //農曆日

    回覆刪除
    回覆
    1. 感謝您的提示,有一段時間沒過來,剛剛才看到留言,謝謝。

      刪除

請提供您的寶貴意見 ;-)