中國傳統曆法結合太陽年(公曆)與月相週期(農曆),演算法相對複雜。本文詳細解析公曆星期計算、農曆日期轉換、二十四節氣判定等核心演算法,並提供可直接使用的 Java 完整實作碼(適用西元 1901-2100 年)。
基準:西元 1 年 1 月 1 日為星期一。利用 400 年週期特性簡化計算。
/**
* 計算公曆日期對應星期幾
* @return 1=週日, 2=週一, ..., 7=週六
*/
public static int dayOfWeek(int year, int month, int day) {
int w = 1; // 1/1/1 是週一,起始值設為週日
// 利用 400 年週期減少運算量
year = (year - 1) % 400 + 1;
int leapYears = (year - 1) / 4; // 每 4 年一閏
leapYears -= (year - 1) / 100; // 但整百年不閏
leapYears += (year - 1) / 400; // 除非整 400 年
int commonYears = year - 1 - leapYears;
w += commonYears; // 平年推移 1 天
w += 2 * leapYears; // 閏年推移 2 天
w += dayOfYear(year, month, day); // 加上今年經過天數
return (w - 1) % 7 + 1; // 映射到 1-7
}
農曆採用「定氣朔」法:以冬至為基準定義月份大小、閏月安排。演算法核心包含三大資料表:
sectionalTermMap (節氣) 與 principleTermMap (中氣),每月 28-29 年週期資料
// 兩個錨點提升計算效率
private static final int BASE_YEAR = 1901;
private static final int BASE_CHINESE_YEAR = 4597; // 1901/1/1 = 農曆 4598 年 11 月 11 日
// 2000 年為第二錨點
private static final int BASE_YEAR_2000 = 2000;
private static final int BASE_CHINESE_YEAR_2000 = 4696; // 2000/1/1 = 農曆 4697 年 11 月 25 日
public int computeChineseFields() {
if (gregorianYear < 1901 || gregorianYear > 2100) return -1;
// 選擇較近錨點減少迴圈次數
int startYear = (gregorianYear >= 2000) ? 2000 : 1901;
int chineseYear = (gregorianYear >= 2000) ? 4696 : 4597;
int chineseMonth = 11;
int chineseDate = (gregorianYear >= 2000) ? 25 : 11;
// 計算兩日期相差天數
int daysDiff = calculateDayDifference(startYear, gregorianYear, gregorianMonth, gregorianDate);
chineseDate += daysDiff;
// 向前推算農曆年月
while (chineseDate > daysInChineseMonth(chineseYear, chineseMonth)) {
chineseDate -= daysInChineseMonth(chineseYear, chineseMonth);
int nextMonth = nextChineseMonth(chineseYear, chineseMonth);
if (Math.abs(nextMonth) < Math.abs(chineseMonth)) chineseYear++;
chineseMonth = nextMonth;
}
this.chineseYear = chineseYear;
this.chineseMonth = chineseMonth;
this.chineseDate = chineseDate;
return 0;
}
public static int sectionalTerm(int year, int month) {
int idx = 0;
int relYear = year - 1900; // 1901 -> 1
// 找到對應年份區間
while (relYear >= sectionalTermYear[month-1][idx]) idx++;
// 基礎日期 + 4 年週期修正
int term = sectionalTermMap[month-1][4*idx + relYear%4];
// 硬編碼特例修正(天文觀測微調)
if (relYear == 121 && month == 4) term = 5; // 2021 清明
if (relYear == 132 && month == 4) term = 5; // 2032 清明
if (relYear == 194 && month == 6) term = 6; // 2094 芒種
return term;
}
核心屬性:
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("公曆: %d-%02d-%02d ", gy, gm, gd));
sb.append(String.format("農曆: %d年%s%d月%d日 ",
chineseYear + 1,
chineseMonth < 0 ? "閏" : "",
Math.abs(chineseMonth), chineseDate));
sb.append(String.format("干支: %s%s年 ",
STEMS[chineseYear % 10], BRANCHES[chineseYear % 12]));
if (sectionalTerm == gm) sb.append("節氣: ").append(SECTIONAL_TERMS[gm-1]);
if (principleTerm == gm) sb.append("節氣: ").append(PRINCIPLE_TERMS[gm-1]);
return sb.toString();
}
程式可產出 1901-2100 年完整對照表,格式範例(2000 年 1 月):
一月
日 一 二 三 四 五 六
1/25 2/26 3/27 4/28 5/29 立春 臘月
8/ 2 9/ 3 10/ 4 11/ 5 12/ 6 13/ 7 14/ 8
15/ 9 16/10 17/11 18/12 19/13 20/14 雨水 22/16
23/17 24/18 25/19 26/20 27/21 28/22 29/23
30/24 31/25
說明:
1/25 = 公曆 1 日 / 農曆 25 日立春、雨水 = 節氣名稱臘月 = 農曆 12 月首日
// 命令列使用
java ChineseCalendarGB day 2024 10 15 // 查詢單日
java ChineseCalendarGB year 2024 // 輸出全年曆
java ChineseCalendarGB month 2024 10 // 輸出單月曆
java.time 套件(LocalDate, Chronology)實作標準化介面