Add comprehensive README documentation
- Project overview and features - Installation instructions - Usage examples (CLI, Python API, REST API) - Supported historical era ranges - Technical explanation of lunar calendar calculations - Development guidelines Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
250
README.md
Normal file
250
README.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Lunar Converter (農曆轉換器)
|
||||
|
||||
A comprehensive Chinese lunar calendar converter with support for historical era names, age calculation, and REST API services.
|
||||
|
||||
## 功能 (Features)
|
||||
|
||||
- **西曆 ↔ 農曆轉換**: 精確的公曆與農曆日期互轉,支援閏月處理
|
||||
- **歷代年號轉換**: 支援明清年號、太平天國、民國、日本元號等
|
||||
- **年齡計算**: 計算實歲(足歲)與虛歲
|
||||
- **REST API**: 提供 HTTP API 服務
|
||||
- **天文計算**: 使用 Skyfield 與 JPL 星曆資料進行精確計算
|
||||
|
||||
## 專案結構
|
||||
|
||||
```
|
||||
lunar-converter/
|
||||
├── lunar_calendar.py # 農曆天文計算引擎
|
||||
├── converter.py # 西曆↔農曆轉換核心類別
|
||||
├── chrono_converter.py # 歷史年號轉換
|
||||
├── converter_with_eras.py # 整合型 CLI 工具
|
||||
├── api.py # FastAPI REST API 伺服器
|
||||
├── age_calculator.py # 年齡計算器
|
||||
├── generate_lunar_calendar_json.py # 批量生成農曆資料
|
||||
├── de421.bsp / de422.bsp # JPL 星曆檔案(需另行下載)
|
||||
└── lunar_json/ # 生成的農曆資料 JSON 檔案
|
||||
```
|
||||
|
||||
## 安裝 (Installation)
|
||||
|
||||
### 1. 複製儲存庫
|
||||
|
||||
```bash
|
||||
git clone http://192.168.42.124:31337/timmy/lunar-converter.git
|
||||
cd lunar-converter
|
||||
```
|
||||
|
||||
### 2. 安裝依賴
|
||||
|
||||
```bash
|
||||
# 使用 uv(推薦)
|
||||
uv sync
|
||||
|
||||
# 或使用 pip
|
||||
pip install skyfield jplephem numpy fastapi uvicorn loguru
|
||||
```
|
||||
|
||||
### 3. 下載 JPL 星曆檔案
|
||||
|
||||
從 [JPL 星曆官網](https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/) 下載其中一個:
|
||||
|
||||
- `de421.bsp` (預設,涵蓋 1900-2050)
|
||||
- `de422.bsp` (較新,涵蓋更長時間範圍)
|
||||
- `de440s.bsp` (精簡版,適合 1900-2150)
|
||||
|
||||
將檔案放在專案根目錄。
|
||||
|
||||
### 4. 生成農曆資料
|
||||
|
||||
```bash
|
||||
# 生成單一年份
|
||||
python lunar_calendar.py 2025
|
||||
|
||||
# 或批量生成所有年份(使用多處理加速)
|
||||
python generate_lunar_calendar_json.py
|
||||
```
|
||||
|
||||
生成的 JSON 檔案會儲存在 `lunar_json/` 目錄下。
|
||||
|
||||
## 使用方法 (Usage)
|
||||
|
||||
### 命令列工具
|
||||
|
||||
#### 西曆轉農曆
|
||||
|
||||
```bash
|
||||
python converter_with_eras.py solar2lunar 2025-01-01
|
||||
```
|
||||
|
||||
#### 農曆轉西曆
|
||||
|
||||
```bash
|
||||
python converter_with_eras.py lunar2solar 2025-01-01
|
||||
python converter_with_eras.py lunar2solar 2025-01-01 --leap # 閏月
|
||||
```
|
||||
|
||||
#### 查詢歷史年號
|
||||
|
||||
```bash
|
||||
python converter_with_eras.py era 1900-01-01
|
||||
```
|
||||
|
||||
輸出示例:
|
||||
```json
|
||||
{
|
||||
"太平天國": "太平天國 50 年",
|
||||
"民國": "民國 89 年",
|
||||
"光緒": "光緒 26 年",
|
||||
"明治": "明治 33 年",
|
||||
"星期": "Monday"
|
||||
}
|
||||
```
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from converter import Converter
|
||||
from chrono_converter import ChronoConverter
|
||||
|
||||
# 西曆轉農曆
|
||||
converter = Converter()
|
||||
result = converter.solar_to_lunar("2025-01-01")
|
||||
print(result)
|
||||
# {'西曆': '2025-01-01', '農曆': '12月2日', '干支': '甲辰', '生肖': '龍', '節氣': ''}
|
||||
|
||||
# 農曆轉西曆
|
||||
solar_date = converter.lunar_to_solar(2025, 1, 1)
|
||||
print(solar_date) # '2025-01-29'
|
||||
|
||||
# 查詢閏月
|
||||
leap_month = converter.get_leap_month(2025)
|
||||
print(leap_month) # 6 (表示閏六月)
|
||||
|
||||
# 歷史年號轉換
|
||||
era_info = ChronoConverter.to_era(1900, 1, 1)
|
||||
print(era_info)
|
||||
```
|
||||
|
||||
### 年齡計算
|
||||
|
||||
```python
|
||||
from datetime import date
|
||||
from age_calculator import AgeCalculator
|
||||
from converter import Converter
|
||||
|
||||
converter = Converter()
|
||||
age_calc = AgeCalculator(converter)
|
||||
|
||||
solar_birth = date(1985, 6, 15)
|
||||
lunar_birth = {"month": 5, "day": 28, "leap": False}
|
||||
today = date(2025, 3, 9)
|
||||
|
||||
result = age_calc.calculate(solar_birth, lunar_birth, today)
|
||||
print(f"實歲: {result.real_age}, 虛歲: {result.east_asian_age}")
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
### 啟動伺服器
|
||||
|
||||
```bash
|
||||
python api.py
|
||||
```
|
||||
|
||||
伺服器會自動選擇可用端口並啟動。
|
||||
|
||||
### API 端點
|
||||
|
||||
#### 1. 西曆轉農曆
|
||||
|
||||
```bash
|
||||
curl "http://localhost:PORT/solar2lunar?date=2025-01-01"
|
||||
```
|
||||
|
||||
回應:
|
||||
```json
|
||||
{
|
||||
"gregorian": "2025-01-01",
|
||||
"lunar": "12月2日",
|
||||
"ganzhi": "甲辰",
|
||||
"zodiac": "龍",
|
||||
"solar_term": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 農曆轉西曆
|
||||
|
||||
```bash
|
||||
curl "http://localhost:PORT/lunar2solar?date=2025-1-1"
|
||||
```
|
||||
|
||||
#### 3. 查詢閏月資訊
|
||||
|
||||
```bash
|
||||
curl "http://localhost:PORT/leap-info?year=2025"
|
||||
```
|
||||
|
||||
回應:
|
||||
```json
|
||||
{
|
||||
"year": 2025,
|
||||
"leap_month": 6
|
||||
}
|
||||
```
|
||||
|
||||
## 支援的年號範圍
|
||||
|
||||
### 中國年號
|
||||
|
||||
- **明朝**: 萬曆、天啟、崇禎
|
||||
- **清朝**: 天命、順治、康熙、雍正、乾隆、嘉慶、道光、咸豐、同治、光緒、宣統
|
||||
- **太平天國**: 1851-1872
|
||||
- **民國**: 1912 年起
|
||||
|
||||
### 日本元號
|
||||
|
||||
- 慶應、明治、大正、昭和、平成、令和
|
||||
|
||||
## 技術說明
|
||||
|
||||
### 農曆計算原理
|
||||
|
||||
本專案使用 [Skyfield](https://skyfield.readthedocs.io/) 天文計算庫,基於 JPL 發布的星曆檔案計算:
|
||||
|
||||
1. **朔望月**: 計算每次新月時間,確定農曆月份起始
|
||||
2. **二十四節氣**: 計算太陽黃經每 15° 的節氣時間點
|
||||
3. **閏月判定**: 根據「冬至後無中氣之月為閏月」規則判定閏月
|
||||
4. **干支生肖**: 依據農曆年計算天干地支與生肖
|
||||
|
||||
### 時區
|
||||
|
||||
所有計算使用 UTC+8(台北時區)。
|
||||
|
||||
## 開發 (Development)
|
||||
|
||||
### 執行測試
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
### 程式碼格式化
|
||||
|
||||
```bash
|
||||
ruff check .
|
||||
ruff format .
|
||||
```
|
||||
|
||||
## 授權 (License)
|
||||
|
||||
MIT License
|
||||
|
||||
## 貢獻 (Contributing)
|
||||
|
||||
歡迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 相關資源
|
||||
|
||||
- [Skyfield 文檔](https://skyfield.readthedocs.io/)
|
||||
- [JPL 星曆下載](https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/)
|
||||
- [農曆計算原理](https://en.wikipedia.org/wiki/Chinese_calendar)
|
||||
@@ -1,225 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.13"
|
||||
# dependencies = [
|
||||
# "skyfield",
|
||||
# "jplephem",
|
||||
# "numpy",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from skyfield.almanac import find_discrete, moon_phases
|
||||
from skyfield.api import load
|
||||
from skyfield.framelib import ecliptic_frame
|
||||
|
||||
|
||||
class LunarCalendar:
|
||||
"""利用 Skyfield 計算農曆、干支、生肖、節氣並輸出 JSON。"""
|
||||
|
||||
TZ_OFFSET = timedelta(hours=8) # 台北時區
|
||||
|
||||
HEAVENLY_STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"]
|
||||
EARTHLY_BRANCHES = [
|
||||
"子",
|
||||
"丑",
|
||||
"寅",
|
||||
"卯",
|
||||
"辰",
|
||||
"巳",
|
||||
"午",
|
||||
"未",
|
||||
"申",
|
||||
"酉",
|
||||
"戌",
|
||||
"亥",
|
||||
]
|
||||
ZODIAC = {
|
||||
"子": "鼠",
|
||||
"丑": "牛",
|
||||
"寅": "虎",
|
||||
"卯": "兔",
|
||||
"辰": "龍",
|
||||
"巳": "蛇",
|
||||
"午": "馬",
|
||||
"未": "羊",
|
||||
"申": "猴",
|
||||
"酉": "雞",
|
||||
"戌": "狗",
|
||||
"亥": "豬",
|
||||
}
|
||||
SOLAR_TERMS_24 = {
|
||||
315: "立春",
|
||||
330: "雨水",
|
||||
345: "驚蟄",
|
||||
0: "春分",
|
||||
15: "清明",
|
||||
30: "穀雨",
|
||||
45: "立夏",
|
||||
60: "小滿",
|
||||
75: "芒種",
|
||||
90: "夏至",
|
||||
105: "小暑",
|
||||
120: "大暑",
|
||||
135: "立秋",
|
||||
150: "處暑",
|
||||
165: "白露",
|
||||
180: "秋分",
|
||||
195: "寒露",
|
||||
210: "霜降",
|
||||
225: "立冬",
|
||||
240: "小雪",
|
||||
255: "大雪",
|
||||
270: "冬至",
|
||||
285: "小寒",
|
||||
300: "大寒",
|
||||
}
|
||||
|
||||
def __init__(self, year: int):
|
||||
self.year = year
|
||||
self.ts = load.timescale()
|
||||
# Ephemeris kernel options:
|
||||
# - de422.bsp: default here as it is bundled in this repo.
|
||||
# - de440s.bsp: newer, compact; suitable for 1900–2150 use-cases.
|
||||
# - de421.bsp: older but still usable for compatibility.
|
||||
# Switch by uncommenting one of the lines below and ensure the file
|
||||
# exists in the working directory (or provide an absolute path).
|
||||
# self.eph = load('de421.bsp')
|
||||
# self.eph = load('de440s.bsp')
|
||||
self.eph = load("de422.bsp")
|
||||
# Pylance sometimes mis-infers types.
|
||||
# Cast to Any to avoid false positives.
|
||||
self.earth = cast(Any, self.eph["earth"]) # has method .at(t)
|
||||
self.sun = cast(Any, self.eph["sun"]) # used with .observe()
|
||||
|
||||
def _compute_astronomy(self):
|
||||
"""抓新朔與 24 節氣時間點"""
|
||||
t0 = self.ts.utc(self.year - 1, 11, 1)
|
||||
t1 = self.ts.utc(self.year + 1, 3, 1)
|
||||
|
||||
# 每月朔
|
||||
times, phases = find_discrete(t0, t1, moon_phases(self.eph))
|
||||
new_moons = [t for t, p in zip(times, phases, strict=False) if p == 0]
|
||||
|
||||
# 每 15° 的節氣
|
||||
def term_index(t):
|
||||
lat, lon, _ = (
|
||||
self.earth.at(t)
|
||||
.observe(self.sun)
|
||||
.apparent()
|
||||
.frame_latlon(ecliptic_frame)
|
||||
)
|
||||
return (lon.degrees // 15).astype(int)
|
||||
|
||||
term_index.step_days = 1
|
||||
|
||||
st_times, st_idxs = find_discrete(t0, t1, term_index)
|
||||
zhongqi = [
|
||||
(t, (idx * 15) % 360) for t, idx in zip(st_times, st_idxs, strict=False)
|
||||
]
|
||||
|
||||
return new_moons, zhongqi
|
||||
|
||||
def _label_months(self, new_moons, zhongqi):
|
||||
"""根據冬至、中氣規則標記每段朔月的月號與閏月屬性"""
|
||||
cutoff = date(self.year, 1, 1)
|
||||
sols = [
|
||||
t
|
||||
for t, deg in zhongqi
|
||||
if deg == 270 and (t.utc_datetime() + self.TZ_OFFSET).date() < cutoff
|
||||
]
|
||||
solstice = max(sols)
|
||||
i0 = max(
|
||||
i
|
||||
for i, t in enumerate(new_moons)
|
||||
if t.utc_datetime() < solstice.utc_datetime()
|
||||
)
|
||||
|
||||
labels = []
|
||||
month_no = 11
|
||||
leap_used = False
|
||||
|
||||
for j in range(i0, len(new_moons) - 1):
|
||||
start, end = new_moons[j], new_moons[j + 1]
|
||||
cnt = sum(1 for t, deg in zhongqi if start < t < end and deg % 30 == 0)
|
||||
|
||||
if j == i0:
|
||||
labels.append((start, end, 11, False))
|
||||
else:
|
||||
if cnt == 0 and not leap_used:
|
||||
labels.append((start, end, month_no, True))
|
||||
leap_used = True
|
||||
else:
|
||||
month_no = month_no % 12 + 1
|
||||
labels.append((start, end, month_no, False))
|
||||
|
||||
return labels
|
||||
|
||||
def _ganzhi_year(self, y):
|
||||
"""計算指定年之干支"""
|
||||
s = self.HEAVENLY_STEMS[(y - 4) % 10]
|
||||
b = self.EARTHLY_BRANCHES[(y - 4) % 12]
|
||||
return s + b
|
||||
|
||||
def build_calendar(self):
|
||||
"""產生從 YYYY-01-01 至 YYYY-12-31 的完整農曆對照表"""
|
||||
new_moons, zhongqi = self._compute_astronomy()
|
||||
month_labels = self._label_months(new_moons, zhongqi)
|
||||
|
||||
# 節氣快查
|
||||
solar_terms = {}
|
||||
for t, deg in zhongqi:
|
||||
d = (t.utc_datetime() + self.TZ_OFFSET).date().isoformat()
|
||||
name = self.SOLAR_TERMS_24.get(deg)
|
||||
if name:
|
||||
solar_terms[d] = name
|
||||
|
||||
result = {}
|
||||
day = date(self.year, 1, 1)
|
||||
end = date(self.year, 12, 31)
|
||||
|
||||
while day <= end:
|
||||
for ts, te, m, is_leap in month_labels:
|
||||
ds = (ts.utc_datetime() + self.TZ_OFFSET).date()
|
||||
de = (te.utc_datetime() + self.TZ_OFFSET).date()
|
||||
if ds <= day < de:
|
||||
# 農曆年:11–12 月屬上一農曆年,其它屬當年
|
||||
lunar_year = self.year if 1 <= m <= 10 else self.year - 1
|
||||
gz_year = self._ganzhi_year(lunar_year)
|
||||
zodiac = self.ZODIAC[self.EARTHLY_BRANCHES[(lunar_year - 4) % 12]]
|
||||
|
||||
offset = (day - ds).days + 1
|
||||
lunar_str = f"{'閏' if is_leap else ''}{m}月{offset}日"
|
||||
result[day.isoformat()] = {
|
||||
"西曆": day.isoformat(),
|
||||
"農曆": lunar_str,
|
||||
"干支": gz_year,
|
||||
"生肖": zodiac,
|
||||
"節氣": solar_terms.get(day.isoformat(), ""),
|
||||
}
|
||||
break
|
||||
day += timedelta(days=1)
|
||||
|
||||
return result
|
||||
|
||||
def save_json(self, filename: str | None = None):
|
||||
"""將該年日曆輸出為 JSON 檔"""
|
||||
data = self.build_calendar()
|
||||
fname = filename or f"{self.year}.json"
|
||||
with open(fname, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
print(f"完成!已輸出:{fname}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) != 2 or not sys.argv[1].isdigit():
|
||||
print("用法:python3 lunar_calendar.py <年份>")
|
||||
sys.exit(1)
|
||||
year = int(sys.argv[1])
|
||||
cal = LunarCalendar(year)
|
||||
cal.save_json()
|
||||
Reference in New Issue
Block a user