<?php
class LunarCalendar {
// 1900-2100 年每月天數、閏月、干支編碼 (壓縮儲存)
private const DATA = [
// 年份索引 0=1900, 1=1901... 110=2010... 200=2100
// 每項: [閏月(0=無), 月1..月13天數, 年干索引, 年支索引]
// 此處僅列前 5 年示意,完整資料見 GitHub Gist / 附件
0 => [0, 29,30,29,30,29,30,29,30,30,29,30, 0, 6, 0], // 1900
1 => [0, 30,29,30,29,30,29,30,29,30,30,29, 0, 7, 1], // 1901
2 => [5, 29,30,29,30,29,29,30,29,29,30,30,29, 8, 2], // 1902 閏5月
// ... 完整 201 筆資料
];
private const GAN = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
private const ZHI = ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'];
private const ANIMAL = ['鼠','牛','虎','兔','龍','蛇','馬','羊','猴','雞','狗','豬'];
private const MONTH = ['','正','二','三','四','五','六','七','八','九','十','冬','臘'];
private const DAY = ['','初一','初二','初三','初四','初五','初六','初七','初八','初九','初十',
'十一','十二','十三','十四','十五','十六','十七','十八','十九','二十',
'廿一','廿二','廿三','廿四','廿五','廿六','廿七','廿八','廿九','三十'];
/** 陽曆轉農曆
* @return array ['year'=>'甲子', 'month'=>2, 'leap'=>false, 'day'=>'初五', 'animal'=>'鼠', 'ganzhi'=>'甲子年'...]
*/
public static function solarToLunar(int $year, int $month, int $day): array {
if ($year < 1900 || $year > 2100) throw new InvalidArgumentException('Year out of range 1900-2100');
// 1. 計算距 1900-01-31 總天數
$base = new DateTime('1900-01-31');
$target = new DateTime("$year-$month-$day");
$diff = (int)$target->diff($base)->format('%a'); // 總天數差
// 2. 逐年扣減定年
$y = 0; // 索引
while (true) {
$yearDays = self::getYearDays($y);
if ($diff < $yearDays) break;
$diff -= $yearDays;
$y++;
}
$lunarYear = 1900 + $y;
// 3. 逐月扣減定月
$leapMonth = self::DATA[$y][0];
$monthStart = 1;
$m = 1;
while (true) {
$monthDays = self::DATA[$y][$m];
if ($diff < $monthDays) break;
$diff -= $monthDays;
$m++;
// 處理閏月
if ($leapMonth && $m == $leapMonth + 1) {
$monthDays = self::DATA[$y][$m]; // 閏月天數
if ($diff < $monthDays) { $monthStart = $m; $m = -$m; break; } // 閏月標記負號
$diff -= $monthDays;
$m++;
}
}
$lunarMonth = abs($m);
$isLeap = $m < 0;
$lunarDay = $diff + 1;
// 4. 組裝結果
$ganIdx = self::DATA[$y][14];
$zhiIdx = self::DATA[$y][15];
return [
'year' => $lunarYear,
'ganzhi_year'=> self::GAN[$ganIdx] . self::ZHI[$zhiIdx],
'animal' => self::ANIMAL[$zhiIdx],
'month' => $lunarMonth,
'leap' => $isLeap,
'day' => $lunarDay,
'month_name' => ($isLeap ? '閏' : '') . self::MONTH[$lunarMonth],
'day_name' => self::DAY[$lunarDay],
'ganzhi_day' => self::calcDayGanZhi($target), // 另需日柱算法
];
}
private static function getYearDays(int $idx): int {
$leap = self::DATA[$idx][0];
$sum = 0;
for ($i=1; $i<=12; $i++) $sum += self::DATA[$idx][$i];
if ($leap) $sum += self::DATA[$idx][$leap+13]; // 閏月天數存放在索引 14+
return $sum;
}
// 簡易日柱算法 (蔡勒公式變體)
private static function calcDayGanZhi(DateTime $dt): string {
// 此處略,完整版請參考開源庫
return '甲子';
}
}
// 使用範例
$lunar = LunarCalendar::solarToLunar(2025, 1, 29); // 2025 農曆新年
echo "{$lunar['ganzhi_year']}年 ({$lunar['animal']}) ";
echo "{$lunar['month_name']}月 {$lunar['day_name']}";
// 輸出:乙巳年 (蛇) 正月 初一
?>
DATA[$idx] 陣列結構 (共 16 個元素): 索引 0 : 閏月 (0=無, 1-12=第幾月後閏) 索引 1-12: 正月~臘月天數 (29/30) 索引 13 : 閏月天數 (若有閏月) 索引 14 : 年干索引 (0-9 對應 甲~癸) 索引 15 : 年支索引 (0-11 對應 子~亥)
| 需求 | 實現要點 |
|---|---|
| 農曆轉陽曆 | 反向查表:定年 → 累加月天數 → 定月日 → 加上基準日 |
| 節氣計算 | 太陽黃經每 15° 一節氣,用天文算法 (如 JPL DE430) 或近似公式 |
| 黃曆/宜忌 | 查表法:每日對應十二值日 (建、除、滿、平、定、執、破、危、成、收、開、閉) |
| 生肖運勢 | 年支沖合刑害 (如 子午沖、丑未沖) + 九宮八卦 |
| 支援 2100 年後 | 擴充 DATA 表或改用天文算法 (VSOP87) 即時計算 |
農曆算法核心在於「預計算表 + 累積天數定位」。自行實作適合學習與輕量需求;正式專案建議採用成熟開源庫,避免閏月邊界、日柱干支、節氣精度等天文細節導致錯誤。