Initial commit: Lunar calendar converter project

- Add lunar calendar astronomical calculation engine (lunar_calendar.py)
- Add converter utilities for solar/lunar date conversion
- Add historical era name conversions (chrono_converter.py, converter_with_eras.py)
- Add FastAPI REST API for date conversion services
- Add age calculator using lunar calendar
- Add batch generation script for lunar calendar JSON data
- Add project documentation (CLAUDE.md, pyproject.toml)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 13:51:45 +08:00
commit ae21a0ae66
19 changed files with 1977 additions and 0 deletions

110
api.py Normal file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# 提供農曆-西曆轉換的 API 服務FastAPI 版本)
# 依賴: fastapi, uvicorn, converter.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "fastapi",
# "uvicorn",
# ]
# ///
import socket
from contextlib import closing
from typing import Any, Dict, List
from fastapi import FastAPI, Query # type: ignore[reportMissingImports]
from fastapi.responses import JSONResponse # type: ignore[reportMissingImports]
import uvicorn # type: ignore[reportMissingImports]
from converter import Converter
def find_free_port() -> int:
"""使用臨時 socket 綁定到 0 取得可用埠號"""
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind(("", 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return s.getsockname()[1]
app = FastAPI(title="Lunar Calendar Converter API")
conv = Converter()
@app.get("/solar2lunar")
def solar2lunar_endpoint(date: str = Query(None, description="YYYY-MM-DD")):
"""西曆轉農曆"""
if not date:
return JSONResponse({"error": "缺少參數 date格式 YYYY-MM-DD"}, status_code=400)
try:
info_raw = conv.solar_to_lunar(date)
info = {
"gregorian": info_raw.get("西曆"),
"lunar": info_raw.get("農曆"),
"ganzhi": info_raw.get("干支"),
"zodiac": info_raw.get("生肖"),
"solar_term": info_raw.get("節氣"),
}
return info
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=400)
@app.get("/lunar2solar")
def lunar2solar_endpoint(date: str = Query(None, description="YYYY-MM-DD")):
"""農曆轉西曆(自動處理閏月,回傳所有可能性)"""
if not date:
return JSONResponse({"error": "缺少參數 date格式 YYYY-MM-DD"}, status_code=400)
try:
y_str, m_str, d_str = date.split("-", 2)
y, m, d = int(y_str), int(m_str), int(d_str)
except Exception:
return JSONResponse({"error": "農曆日期格式應為 YYYY-MM-DD"}, status_code=400)
possible_solar_dates: List[str] = conv.lunar_to_solar_all(y, m, d)
if not possible_solar_dates:
error_msg = f"找不到任何對應農曆 {y}{m}{d}日 的西曆日期"
return JSONResponse({"error": error_msg}, status_code=404)
results_list: List[Dict[str, Any]] = []
for solar_date in possible_solar_dates:
try:
info_raw = conv.solar_to_lunar(solar_date)
results_list.append({
"gregorian": info_raw.get("西曆"),
"lunar": info_raw.get("農曆"),
"ganzhi": info_raw.get("干支"),
"zodiac": info_raw.get("生肖"),
"solar_term": info_raw.get("節氣"),
})
except Exception:
continue
return results_list
@app.get("/leap-info")
def leap_info_endpoint(year: str = Query(None, description="西曆年份,數字")):
"""查詢指定西曆年份是否有農曆閏月,以及是閏幾月。"""
if not year:
return JSONResponse({"error": "缺少參數 year"}, status_code=400)
try:
y = int(year)
leap_month = conv.get_leap_month(y)
return {"year": y, "leap_month": leap_month}
except ValueError:
return JSONResponse({"error": "參數 year 必須是有效的整數"}, status_code=400)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
if __name__ == "__main__":
try:
free_port = find_free_port()
print(f"找到一個可用的 port: {free_port}")
uvicorn.run(app, host="0.0.0.0", port=free_port, reload=False)
except Exception as e:
print(f"啟動服務失敗: {e}")