#!/usr/bin/env python3 # converter_with_eras.py # # 結合「西曆↔農曆轉換」與「歷代年號 ↔ 西曆」功能 # 支援:太平天國、民國、中國明清諸年號(農曆基準)、日本各元號(西曆基準) import os import sys import json import argparse from datetime import datetime, date from typing import Dict # 中國明清年號對應的西曆範圍及基準年 (起始日, 結束日, 基年) CHINESE_ERAS_SOLAR = { # 明朝 "萬曆": (date(1573, 1, 1), date(1620, 12, 31), 1572), "天啟": (date(1621, 1, 1), date(1627, 12, 31), 1620), "崇禎": (date(1628, 1, 1), date(1644, 4, 25), 1627), # 崇禎最後一天 1644-04-25 # 清朝 "順治": (date(1644, 2, 8), date(1662, 2, 17), 1643), # 順治 1年 1644-02-08 至 1662-02-17 "康熙": (date(1662, 2, 18), date(1723, 2, 8), 1661), # 康熙 1年 1662-02-18 至 1723-02-08 "雍正": (date(1723, 2, 9), date(1736, 1, 28), 1722), # 雍正 1年 1723-02-09 至 1736-01-28 "乾隆": (date(1736, 1, 29), date(1796, 2, 17), 1735), # 乾隆 1年 1736-01-29 至 1796-02-17 "嘉慶": (date(1796, 2, 18), date(1821, 2, 6), 1795), # 嘉慶 1年 1796-02-18 至 1821-02-06 "道光": (date(1821, 2, 7), date(1851, 1, 27), 1820), # 道光 1年 1821-02-07 至 1851-01-27 "咸豐": (date(1851, 1, 28), date(1862, 1, 16), 1850), # 咸豐 1年 1851-01-28 至 1862-01-16 "同治": (date(1862, 1, 17), date(1875, 2, 5), 1861), # 同治 1年 1862-01-17 至 1875-02-05 "光緒": (date(1875, 2, 6), date(1909, 1, 22), 1874), # 光緒 1年 1875-02-06 至 1909-01-22 "宣統": (date(1909, 1, 23), date(1912, 2, 18), 1908), # 宣統 1年 1909-01-23 至 1912-02-18 } class Converter: """ 提供西曆與農曆互轉功能: - solar_to_lunar(date_str: str) -> dict - lunar_to_solar(year: int, month: int, day: int, leap: bool=False) -> str """ LUNAR_JSON_DIR = 'lunar_json' @classmethod def load_solar_to_lunar(cls, year: int) -> Dict: path = os.path.join(cls.LUNAR_JSON_DIR, f"{year}.json") if not os.path.exists(path): raise FileNotFoundError(f"找不到 {path}") with open(path, encoding='utf-8') as f: return json.load(f) def solar_to_lunar(self, solar_date_str: str) -> Dict: # 允許輸入 "YYYY-M-D",並自動標準化為 ISO 格式 "YYYY-MM-DD" parts = solar_date_str.split('-') if len(parts) != 3: raise ValueError(f"日期格式錯誤,請使用 YYYY-MM-DD 或類似格式:{solar_date_str}") y, m, d = map(int, parts) dt = date(y, m, d) key = dt.isoformat() try: data = self.load_solar_to_lunar(dt.year) except FileNotFoundError: raise FileNotFoundError(f"無法取得 {dt.year} 年的農曆資料,請先生成對應的 lunar_json/{dt.year}.json 檔案") if key not in data: raise KeyError(f"沒有找到對應 {key} 的農曆資料") return data[key] def lunar_to_solar(self, lunar_year: int, lunar_month: int, lunar_day: int, leap: bool=False) -> str: solar_year = lunar_year + 1 if lunar_month in (11, 12) else lunar_year data = self.load_solar_to_lunar(solar_year) # 精確匹配 for solar, info in data.items(): lstr = info.get('農曆','') is_leap = lstr.startswith('閏') core = lstr[1:] if is_leap else lstr m_str, d_str = core.replace('月','-').replace('日','').split('-') if int(m_str)==lunar_month and int(d_str)==lunar_day and is_leap==leap: return solar # 回退匹配非閏月 for solar, info in data.items(): lstr = info.get('農曆','') is_leap = lstr.startswith('閏') core = lstr[1:] if is_leap else lstr m_str, d_str = core.replace('月','-').replace('日','').split('-') if int(m_str)==lunar_month and int(d_str)==lunar_day and not is_leap: return solar raise KeyError(f"找不到對應農曆 {lunar_year}{'閏' if leap else ''}{lunar_month}月{lunar_day}日 的西曆") class ChronoConverter: """ 統一各種紀年 ↔ 西曆 的轉換 """ # 日本元號對應的西曆範圍及基準年 (起始日, 結束日, 基年) JAPANESE_ERAS = { "慶應": (date(1865,3,18), date(1868,10,22), 1864), "明治": (date(1868,10,23), date(1912,7,30), 1867), "大正": (date(1912,7,30), date(1926,12,24), 1911), "昭和": (date(1926,12,25), date(1989,1,7), 1925), "平成": (date(1989,1,8), date(2019,4,30), 1988), "令和": (date(2019,5,1), None, 2018), } @staticmethod def _lunar_year(y: int, m: int, d: int) -> int: conv = Converter() dt = date(y, m, d) for ly in (y, y-1): try: start = conv.lunar_to_solar(ly, 1, 1) nxt = conv.lunar_to_solar(ly+1, 1, 1) except Exception: continue d0, d1 = date.fromisoformat(start), date.fromisoformat(nxt) if d0 <= dt < d1: return ly return y @classmethod def to_era(cls, y: int, m: int, d: int) -> Dict: dt = date(y, m, d) ly = cls._lunar_year(y, m, d) res: Dict[str, str] = {} if date(1851,2,1) <= dt <= date(1872,7,5): res["太平天國"] = f"太平天國 {y-1850} 年" if y >= 1912: res["民國"] = f"民國 {y-1911} 年" for era, (st, ed, base) in CHINESE_ERAS_SOLAR.items(): if st <= dt <= ed: res[era] = f"{era} {dt.year - base} 年" for era, (st, ed, base) in cls.JAPANESE_ERAS.items(): if dt >= st and (ed is None or dt <= ed): res[era] = f"{era} {dt.year-base} 年" res["星期"] = dt.strftime("%A") return res def convert_date(y: int, m: int, d: int) -> Dict: return ChronoConverter.to_era(y, m, d) def main(): parser = argparse.ArgumentParser(description="西曆↔農曆+歷代年號+日本元號 轉換工具") sub = parser.add_subparsers(dest='cmd', required=True) p1 = sub.add_parser('solar2lunar', help='西曆轉農曆') p1.add_argument('date', help='YYYY-MM-DD 或 YYYY-M-D') p2 = sub.add_parser('lunar2solar', help='農曆轉西曆') p2.add_argument('date', help='YYYY-MM-DD') p2.add_argument('--leap', action='store_true', help='閏月') p3 = sub.add_parser('era', help='西曆轉歷代年號+日本元號') p3.add_argument('date', help='YYYY-MM-DD') args = parser.parse_args() if args.cmd == 'solar2lunar': try: result = Converter().solar_to_lunar(args.date) print(json.dumps(result, ensure_ascii=False, indent=2)) except (FileNotFoundError, KeyError, ValueError) as e: print(e) sys.exit(1) elif args.cmd == 'lunar2solar': try: y, m, d = map(int, args.date.split('-',2)) solar = Converter().lunar_to_solar(y, m, d, args.leap) print(json.dumps(Converter().solar_to_lunar(solar), ensure_ascii=False, indent=2)) except Exception as e: print(e) sys.exit(1) elif args.cmd == 'era': y, m, d = map(int, args.date.split('-',2)) print(json.dumps(convert_date(y, m, d), ensure_ascii=False, indent=2)) if __name__ == '__main__': main()