From 471e122509ad396c63fd3a871cef51752f82ec0c Mon Sep 17 00:00:00 2001 From: Timmy Date: Mon, 9 Mar 2026 13:53:13 +0800 Subject: [PATCH] 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 --- README.md | 250 ++++++++++++++++++++++++++++++++++++++++++++ lunar_calendar.py.1 | 225 --------------------------------------- 2 files changed, 250 insertions(+), 225 deletions(-) create mode 100644 README.md delete mode 100644 lunar_calendar.py.1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4991fd --- /dev/null +++ b/README.md @@ -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) diff --git a/lunar_calendar.py.1 b/lunar_calendar.py.1 deleted file mode 100644 index 117c8e2..0000000 --- a/lunar_calendar.py.1 +++ /dev/null @@ -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()