I. THUẬT TOÁN CHUYỂN ĐỔI GIỮA NGÀY DƯƠNG VÀ ÂM
Lịch âm là lịch tính theo mặt trăng, lịch dương là lịch tính theo mặt trời. Theo khoa học hiện đại việc tính lịch mặt trăng theo lịch mặt trời đã được phân tích và tính toán dựa trên từng múi giờ khu vực.
Quy luật của âm lịch Việt Nam
- Ngày đầu tiên của tháng âm lịch là ngày chứa điểm Sóc
- Một năm bình thường có 12 tháng âm lịch, một năm nhuận có 13 tháng âm lịch
- Đông chí luôn rơi vào tháng 11 âm lịch
Trong một năm nhuận, nếu có 1 tháng không có Trung khí thì tháng đó là tháng nhuận. Nếu nhiều tháng trong năm nhuận đều không có Trung khí thì chỉ tháng đầu tiên sau Đông chí là tháng nhuận- Việc tính toán dựa trên kinh tuyến 105° đông.
* Sóc là thời điểm hội diện, đó là khi trái đất, mặt trăng và mặt trời nằm trên một đường thẳng và mặt trăng nằm giữa trái đất và mặt trời. (Như thế góc giữa mặt trăng và mặt trời bằng 0 độ). Gọi là "hội diện" vì mặt trăng và mặt trời ở cùng một hướng đối với trái đất. Chu kỳ của điểm Sóc là khoảng 29,530588853 ngày. Ngày chứa điểm Sóc được gọi là ngày Sóc, và đó là ngày bắt đầu tháng âm lịch.
* Trung khí là các điểm chia đường hoàng đạo thành 12 phần bằng nhau. Trong đó, bốn Trung khí giữa bốn mùa là đặc biệt nhất: Xuân phân (khoảng 20/3), Hạ chí (khoảng 22/6), Thu phân (khoảng 23/9) và Đông chí (khoảng 22/12).
Xác định tháng âm lịch
Theo các nguyên tắc trên, để tính ngày tháng âm lịch cho một năm bất kỳ trước hết chúng ta cần xác định những ngày nào trong năm chứa các thời điểm Sóc (New moon) . Một khi bạn đã tính được ngày Sóc, bạn đã biết được ngày bắt đầu và kết thúc của một tháng âm lịch: ngày mùng một của tháng âm lịch là ngày chứa điểm sóc. Sau khi đã biết ngày bắt đầu/kết thúc các tháng âm lịch, ta tính xem các Trung khí (Major solar term) rơi vào tháng nào để từ đó xác định tên các tháng và tìm tháng nhuận.
Xác định năm nhuận, tháng nhuận
Đông chí luôn rơi vào tháng 11 của năm âm lịch.
Bởi vậy chúng ta cần tính 2 điểm sóc: Sóc A ngay trước ngày Đông chí thứ nhất và Sóc B ngay trước ngày Đông chí thứ hai.
- Nếu khoảng cách giữa A và B là dưới 365 ngày thì năm âm lịch có 12 tháng, và những tháng đó có tên là: tháng 11, tháng 12, tháng 1, tháng 2, …, tháng 10.
- Ngược lại, nếu khoảng cách giữa hai sóc A và B là trên 365 ngày thì năm âm lịch này là năm nhuận, và chúng ta cần tìm xem đâu là tháng nhuận.
Để làm việc này ta xem xét tất cả các tháng giữa A và B, tháng đầu tiên không chứa Trung khí sau ngày Đông chí thứ nhất là tháng nhuận. Tháng đó sẽ được mang tên của tháng trước nó và kèm thêm chữ "nhuận".
Trong tính toán thiên văn người ta lấy ngày 1/1/4713 trước công nguyên của lịch Julius (tức ngày 24/11/4714 trước CN theo lịch Gregorius) làm điểm gốc. Số ngày tính từ điểm gốc này gọi là số ngày Julius (Julian day number) của một thời điểm.
Thuật toán dưới đây cũng dựa trên các nguyên tắc này.
Ghi chú hàm INT là hàm chia lấy phần nguyên
Thuật toán
Thuật toán tính toán dựa trên lịch bắt đầu tính từ ngày 1/1/1900
1. Đổi ngày dd/mm/yyyy ra số ngày Julius jd
Tên hàm:jdFromDate
Biến vào:dd, mm, yy
Trả về:Julius jd
a = INT((14 - mm) / 12)
y = yy+4800-a
m = mm+12*a-3
Lịch Gregory:
jd = dd + INT((153*m+2)/5) + 365*y + INT(y/4) - INT(y/100) + INT(y/400) - 32045
Nếu jd < 2299161 là Lịch Julius thì có công thức:
jd = dd + INT((153*m+2)/5) + INT(365*y) + INT(y/4) - 32083
Trả về jd
2. Đổi số ngày Julius jd ra ngày dd/mm/yyyy
Tên hàm:jdToDate
Biến vào:Julius jd
Trả về:dd, mm, yy
Nếu (jd >2299160) là Lịch Gregory thì có công thức:
a = jd + 32044;
b = INT((4*a+3)/146097);
c = a - INT((b*146097)/4);
ngược lại là Lịch Julius thì có công thức:
b = 0;
c = jd + 32082;
Công thức cho cả 2 loại lịch:
d = INT((4*c+3)/1461);
e = c - INT((1461*d)/4);
m = INT((5*e+2)/153);
dd = e - INT((153*m+2)/5) + 1;
mm = m + 3 - INT(12*(m/10));
yy = b*100 + d - 4800 + INT(m/10);
return dd, mm, yy
3. Tính ngày Sóc
Như trên đã nói, để tính được âm lịch trước hết ta cần xác định các tháng âm lịch bắt đầu vào ngày nào.
Thuật toán sau tính ngày Sóc thứ k (là một số Julius) kể từ điểm Sóc ngày 1/1/1900. Kết quả trả về là số ngày Julius của ngày Sóc cần tìm.
Tên hàm:getNewMoonDay
Biến vào:Julius k, timeZone
Trả về:Julius jd của ngày Sóc cần tìm
var T, T2, T3, dr, Jd1, M, Mpr, F, C1, deltat, JdNew, N;
T = k/1236.85; // Time in Julian centuries from 1900 January 0.5
T2 = T * T;
T3 = T2 * T;
dr = PI/180;
Jd1 = 2415020.75933 + 29.53058868*k + 0.0001178*T2 - 0.000000155*T3;
Jd1 = Jd1 + 0.00033*Math.sin((166.56 + 132.87*T - 0.009173*T2)*dr); // Mean new moon
M = 359.2242 + 29.10535608*k - 0.0000333*T2 - 0.00000347*T3; // Sun's mean anomaly
Mpr = 306.0253 + 385.81691806*k + 0.0107306*T2 + 0.00001236*T3; // Moon's mean anomaly
F = 21.2964 + 390.67050646*k - 0.0016528*T2 - 0.00000239*T3; // Moon's argument of latitude
C1=(0.1734 - 0.000393*T)*Math.sin(M*dr) + 0.0021*Math.sin(2*dr*M);
C1 = C1 - 0.4068*Math.sin(Mpr*dr) + 0.0161*Math.sin(dr*2*Mpr);
C1 = C1 - 0.0004*Math.sin(dr*3*Mpr);
C1 = C1 + 0.0104*Math.sin(dr*2*F) - 0.0051*Math.sin(dr*(M+Mpr));
C1 = C1 - 0.0074*Math.sin(dr*(M-Mpr)) + 0.0004*Math.sin(dr*(2*F+M));
C1 = C1 - 0.0004*Math.sin(dr*(2*F-M)) - 0.0006*Math.sin(dr*(2*F+Mpr));
C1 = C1 + 0.0010*Math.sin(dr*(2*F-Mpr)) + 0.0005*Math.sin(dr*(2*Mpr+M));
if (T < -11) {
deltat= 0.001 + 0.000839*T + 0.0002261*T2 - 0.00000845*T3 - 0.000000081*T*T3;
} else {
deltat= -0.000278 + 0.000265*T + 0.000262*T2;
};
JdNew = Jd1 + C1 - deltat;
N = INT(JdNew + 0.5 + timeZone / 24); //JdNew;
Trả về Julius jd là N
Với hàm này ta có thể tính được tháng âm lịch chứa ngày N bắt đầu vào ngày nào: giữa ngày 1/1/1900 (số ngày Julius: 2415021) và ngày N có khoảng k=INT((N-2415021)/29.530588853) tháng âm lịch, như thế dùng hàm getNewMoonDay sẽ biết ngày đầu tháng âm lịch chứa ngày N, từ đó ta biết ngày N là mùng mấy âm lịch.
4. Tính tọa độ mặt trời
Để biết Trung khí nào nằm trong tháng âm lịch nào, ta chỉ cần tính xem mặt trời nằm ở khoảng nào trên đường hoàng đạo vào thời điểm bắt đầu một tháng âm lịch. Ta chia đường hoàng đạo làm 12 phần và đánh số các cung này từ 0 đến 11: từ Xuân phân đến Cốc vũ là 0; từ Cốc vũ đến Tiểu mãn là 1; từ Tiểu mãn đến Hạ chí là 2; v.v.. Cho jdn là số ngày Julius của bất kỳ một ngày, phương pháp sau này sẽ trả lại số cung nói trên.
Tên hàm:getSunLongitude
Biến vào:Julius jdn, timeZone
Trả về:Số cung hoàng đạo
var T, T2, dr, M, L0, DL, L,SC;
T = (jdn - 2451545.5 - timeZone/24) / 36525; // Time in Julian centuries from 2000-01-01 12:00:00 GMT
T2 = T*T;
dr = PI/180; // degree to radian
M = 357.52910 + 35999.05030*T - 0.0001559*T2 - 0.00000048*T*T2; // mean anomaly, degree
L0 = 280.46645 + 36000.76983*T + 0.0003032*T2; // mean longitude, degree
DL = (1.914600 - 0.004817*T - 0.000014*T2)*Math.sin(dr*M);
DL = DL + (0.019993 - 0.000101*T)*Math.sin(dr*2*M) + 0.000290*Math.sin(dr*3*M);
L = L0 + DL; // true longitude, degree
L = L*dr;
L = L - PI*2*(INT(L/(PI*2))); // Normalize to (0, 2*PI)
SC=INT(L / PI * 6)
Trả về số cung hoàng đạo là SC
Với hàm này ta biết được một tháng âm lịch chứa Trung khí nào.
Giả sử một tháng âm lịch bắt đầu vào ngày N1 và tháng sau đó bắt đầu vào ngày N2 và hàm getSunLongitude cho kết quả là 8 với N1 và 9 với N2. Như vậy tháng âm lịch bắt đầu ngày N1 là tháng chứa Đông chí: trong khoảng từ N1 đến N2 có một ngày mặt trời di chuyển từ cung 8 (sau Tiểu tuyết) sang cung 9 (sau Đông chí).
Nếu hàm getSunLongitude trả lại cùng một kết quả cho cả ngày bắt đầu một tháng âm lịch và ngày bắt đầu tháng sau đó thì tháng đó không có Trung khí và như vậy có thể là tháng nhuận.
5. Tìm ngày bắt đầu tháng 11 âm lịch
Đông chí thường nằm vào khoảng 19/12-22/12, như vậy trước hết ta tìm ngày Sóc trước ngày 31/12. Nếu tháng bắt đầu vào ngày đó không chứa Đông chí thì ta phải lùi lại 1 tháng nữa.
Tên hàm:getLunarMonth11
Biến vào:yy, timeZone
Trả về:Julius ngày sóc
var k, off, nm, sunLong;
off = ConvertDateToJulianID(31, 12, yy) - 2415021;
k = INT(off / 29.530588853);
nm = getNewMoonDay(k, timeZone);
sunLong = getSunLongitude(nm, timeZone); // sun longitude at local midnight
if (sunLong >= 9) {
nm = getNewMoonDay(k-1, timeZone);
}
Trả về Julius ngày sóc là nm
6. Xác định tháng nhuận
Nếu giữa hai tháng 11 âm lịch (tức tháng có chứa Đông chí) có 13 tháng âm lịch thì năm âm lịch đó có tháng nhuận. Để xác định tháng nhuận, ta sử dụng hàm getSunLongitude như đã nói ở trên. Cho a11 là ngày bắt đầu tháng 11 âm lịch mà một trong 13 tháng sau đó là tháng nhuận. Hàm sau cho biết tháng nhuận nằm ở vị trí nào sau tháng 11 này.
Tên hàm:getLeapMonthOffset
Biến vào:a11, timeZone
Trả về:tháng nhuận
var k, last, arc, i;
k = INT((a11 - 2415021.076998695) / 29.530588853 + 0.5);
last = 0;
i = 1; // We start with the month following lunar month 11
arc = getSunLongitude(getNewMoonDay(k+i, timeZone), timeZone);
do {
last = arc;
i++;
arc = getSunLongitude(getNewMoonDay(k+i, timeZone), timeZone);
} while (arc != last && i < 14);
Trả về tháng nhuận là i-1;
Giả sử hàm getLeapMonthOffset trả lại giá trị 4, như thế kể từ tháng 11 sau 4 tháng là tháng nhuận nghĩa là tháng sau tháng 2 thường (Tháng thứ 4 sau tháng 11 đáng ra là tháng 3, nhưng vì đó là tháng nhuận nên sẽ lấy tên của tháng trước đó tức tháng 2, và tháng thứ 5 sau tháng 11 mới là tháng 3).
7. Đổi ngày dương dd/mm/yyyy ra ngày âm
Với các phương pháp hỗ trợ trên ta có thể đổi ngày dương dd/mm/yy ra ngày âm dễ dàng.
Trước hết ta xem ngày bắt đầu tháng âm lịch (monthStart) chứa ngày này là ngày nào (sử dụng dùng hàm getNewMoonDay).
Sau đó, ta tìm các ngày a11 và b11 là ngày bắt đầu các tháng 11 âm lịch trước và sau ngày đang xem xét. Nếu hai ngày này cách nhau dưới 365 ngày thì ta chỉ còn cần xem monthStart và a11 cách nhau bao nhiêu tháng là có thể tính được dd/mm/yy nằm trong tháng mấy âm lịch. Ngược lại, nếu a11 và b11 cách nhau khoảng 13 tháng âm lịch thì ta phải tìm xem tháng nào là tháng nhuận và từ đó suy ra ngày đang tìm nằm trong tháng nào.
Tên hàm:convertSolarToLunar
Biến vào:dd, mm, yy, timeZone
Trả về:lunarDay, lunarMonth, lunarYear
var k, dayNumber, monthStart, a11, b11, lunarDay, lunarMonth, lunarYear, lunarLeap;
dayNumber = jdFromDate(dd, mm, yy);
k = INT((dayNumber - 2415021.076998695) / 29.530588853);
monthStart = getNewMoonDay(k+1, timeZone);
if (monthStart > dayNumber) {
monthStart = getNewMoonDay(k, timeZone);
}
a11 = getLunarMonth11(yy, timeZone);
b11 = a11;
if (a11 >= monthStart) {
lunarYear = yy;
a11 = getLunarMonth11(yy-1, timeZone);
} else {
lunarYear = yy+1;
b11 = getLunarMonth11(yy+1, timeZone);
}
lunarDay = dayNumber-monthStart+1;
diff = INT((monthStart - a11)/29);
lunarLeap = 0;
lunarMonth = diff+11;
if (b11 - a11 > 365) {
leapMonthDiff = getLeapMonthOffset(a11, timeZone);
if (diff >= leapMonthDiff) {
lunarMonth = diff + 10;
if (diff == leapMonthDiff) {
lunarLeap = 1;
}
}
}
if (lunarMonth > 12) {
lunarMonth = lunarMonth - 12;
}
if (lunarMonth >= 11 && diff < 4) {
lunarYear -= 1;
}
8. Đổi âm lịch ra dương lịch
Tên hàm:convertLunarToSolar
Biến vào:lunarDay, lunarMonth, lunarYear, lunarLeap, timeZone
Trả về:dd, mm, yy
var k, a11, b11, off, leapOff, leapMonth, monthStart;
if (lunarMonth < 11) {
a11 = getLunarMonth11(lunarYear-1, timeZone);
b11 = getLunarMonth11(lunarYear, timeZone);
} else {
a11 = getLunarMonth11(lunarYear, timeZone);
b11 = getLunarMonth11(lunarYear+1, timeZone);
}
off = lunarMonth - 11;
if (off < 0) {
off += 12;
}
if (b11 - a11 > 365) {
leapOff = getLeapMonthOffset(a11, timeZone);
leapMonth = leapOff - 2;
if (leapMonth < 0) {
leapMonth += 12;
}
if (lunarLeap != 0 && lunarMonth != leapMonth) {
return new Array(0, 0, 0);
} else if (lunarLeap != 0 || off >= leapOff) {
off += 1;
}
}
k = INT(0.5 + (a11 - 2415021.076998695) / 29.530588853);
monthStart = getNewMoonDay(k+off, timeZone);
return jdToDate(monthStart+lunarDay-1);
II. THUẬT TOÁN TÍNH CAN CHI CHO THỜI GIAN
1. Năm âm lịch sang Can Chi
- Thiên can có chu kỳ lặp là 10; như vậy đem số năm chia lấy phần dư gọi là IDCan thì ta luôn có giá trị: 0 - Canh; 1- Tân; 2- Nhâm; 3- Quý; 4 - Giáp; 5 - Ất; 6 - Bính; 7 - Đinh; 8 - Mậu; 9 - Kỷ.
- Địa chi có chu kỳ 12 năm, như vậy đem số năm chia lấy phần dư gọi là IDChi thì ta luôn có giá trị: 4 - Tý; 5- Sửu; 6 - Dần; 7 - Mão; 8 - Thìn; 9 - Tỵ; 10 - Ngọ; 11 - Mùi; 0 - Thân; 1 - Dậu; 2 - Tuất; 3 - Hợi.
Như vậy, ta có hàm tính Can chi năm như sau:
Tên hàm:CanChiNam
Biến vào:lunarYear
Trả về:Can Chi năm
var IDCan=lunarYear%10;
var IDChi=lunarYear%10;
var Can=['Canh', 'Tân', 'Nhâm', 'Quý', 'Giáp', 'Ất', 'Bính', 'Đinh', 'Mậu', 'Kỷ'];
var Chi=['Thân', 'Dậu', 'Tuất', 'Hợi', 'Tý', 'Sửu', 'Dần', 'Mão', 'Thìn', 'Tỵ', 'Ngọ', 'Mùi'];
Trả về Can[IDCan]+' ' + Chi[IDChi];
2. Tháng âm lịch sang Can Chi
- Địa chi tháng âm lịch có tính chất cố định: Tháng 1 (tháng giêng)- Dần; Tháng 2 - Mão; Tháng 3 - Thìn; Tháng 4 - Tỵ; Tháng 5 - Ngọ; Tháng 6 - Mùi; Tháng 7 - Thân; Tháng 8 - Dậu; Tháng 9 - Tuất; Tháng 10 - Hợi; Tháng 11 - Tý; Tháng 12 (tháng chạp) - Sửu.
- Thiên can được lặp theo chu kỳ 10 và 1 năm có 12 tháng; Theo tính toán ta có công thức IDCan = (Năm * 12 + Tháng +3) chi lấy phần dư cho 10 có giá trị từ 0 đến 9 tương ứng với Giáp, Ất,... Quý (Trong công thức có cộng thêm 3 là tịnh tiến để tính IDCan đầu tiên là 0)
Chú ý: Tháng Nhuận có Can Chi giống tháng thường trước đó.
Như vậy, ta có hàm tính Can chi năm như sau:
Tên hàm:CanChiThang
Biến vào:lunarMonth,lunarYear
Trả về:Can Chi tháng
var IDCan=(lunarYear*12+lunarMonth+3)%10;
var IDChi=lunarMonth-1;
var Can=['Giáp', 'Ất', 'Bính', 'Đinh', 'Mậu','Kỷ', 'Canh', 'Tân', 'Nhâm', 'Quý'];
var Chi=['Dần', 'Mão', 'Thìn', 'Tỵ', 'Ngọ', 'Mùi', 'Thân', 'Dậu', 'Tuất', 'Hợi', 'Tý', 'Sửu'];
Trả về Can[IDCan]+' ' + Chi[IDChi];
3. Ngày âm lịch sang Can Chi
Ta biết, chu kỳ thiên can địa chi là 60, một năm có 365-366 ngày; như vậy nếu gọi N là số ngày Julius thì ta dễ dàng tính được Can chi của ngày là (N+9) chi dư cho 10 có giá trị được là 0 đến 9 tương ứng với Giáp đến Quý; (N+1) chia lấy phần dư cho 12 có giá trị từ 0 đến 11 ứng với Tý đến Hợi. (Con số 9 hoặc 1 cộng với N là tịnh tiến để sao cho số dư chia đầu tiên ứng với Giáp hoặc Tý)
Tên hàm:CanChiNgay
Biến vào:Julius jd
Trả về:Can Chi ngày
var IDCan=(jd+9)%10;
var IDChi=(jd+1)%12;
var Can=['Giáp', 'Ất', 'Bính', 'Đinh', 'Mậu', 'Kỷ', 'Canh', 'Tân', 'Nhâm', 'Quý'];
var Chi=['Tý', 'Sửu', 'Dần', 'Mão', 'Thìn', 'Tỵ', 'Ngọ', 'Mùi', 'Thân', 'Dậu', 'Tuất', 'Hợi'];
Trả về Can[IDCan]+' ' + Chi[IDChi];
4. Chuyển đổi Giờ sang Can Chi
- Địa chi giờ trong ngày có tính chất cố định:
23h ngày hôm trước đến 1h ngày hôm sau - giờ Tý;
Từ 1h đến 3h - giờ Sửu;
Từ 3h đến 5h - giờ Dần;
Từ 5h đến 7h - giờ Mão;
Từ 7h đến 9h - giờ Thìn;
Từ 9h đến 11h - giờ Tỵ;
Từ 11h đến 13h - giờ Ngọ.
Từ 13h đến 15h - giờ Mùi.
Từ 15h đến 17h - giờ Thân.
Từ 17h đến 19h - giờ Dậu.
Từ 19h đến 21h - giờ Tuất.
Từ 21h đến 23h - giờ Hợi. - Thiên can của giờ được tính theo nguyên tắc đã được tổng hợp sẵn như sau:
- Ngày Giáp/Kỷ: bắt đầu là giờ Giáp Tý, giờ Ất Sửu,...
- Ngày Ất/Canh: bắt đầu là giờ Bính Tý, giờ Đinh Sửu,...
- Ngày Bính/Tân: bắt đầu là giờ Mậu Tý, giờ Kỷ Sửu,...
- Ngày Đinh/Nhâm: bắt đầu là giờ Canh Tý, giờ Tân Sửu,...
- Ngày Mậu/Quý: bắt đầu là giờ Nhâm Tý, giờ Quý Sửu,...
Dựa trên thống kê trên và cách tính Can chi ngày đã trình bày ở trên, ta có thể tính Thiên Can của giờ tý trong ngày, các giờ khác cứ tịnh tiến dần theo, hết lại quay vòng lại từ đầu giáp.
Như vậy, ta có hàm tính Can chi giờ trong ngày như sau:
Tên hàm:CanChiGio
Biến vào:Julius jd, gio
Trả về:Can Chi giờ
var IDCanNgay=(lunarYear*12+lunarMonth+3)%10;
var IDCanGioTy;
var IDCan
var IDChi;
var Can=['Giáp', 'Ất', 'Bính', 'Đinh', 'Mậu', 'Kỷ', 'Canh', 'Tân', 'Nhâm', 'Quý'];
var Chi=['Tý', 'Sửu', 'Dần', 'Mão', 'Thìn', 'Tỵ', 'Ngọ', 'Mùi', 'Thân', 'Dậu', 'Tuất', 'Hợi'];
if(gio>=23||gio<1)
IDChi=0;
else
IDChi=INT((Gio-1)/2+1);
if(IDCanNgay==0||IDCanNgay==5) //Ngày Giáp/Kỷ
IDCan=0; // Ngày đó bắt đầu là Giáp Tý
else if(IDCanNgay==1||IDCanNgay==6) //Ngày Ất/Canh
IDCan=2; // Ngày đó bắt đầu là Bính Tý
else if(IDCanNgay==2||IDCanNgay==7) //Ngày Bính/Tân
IDCan=4;// Ngày đó bắt đầu là Mậu Tý
else if(IDCanNgay==3||IDCanNgay==8) //Ngày Đinh/Nhâm
IDCan=6; // Ngày đó bắt đầu là Canh Tý
else if(IDCanNgay==4||IDCanNgay==9) //Ngày Mậu/Quý
IDCan=8; // Ngày đó bắt đầu là Nhâm Tý
Trả về Can[IDCan]+' ' + Chi[IDChi];