From bf3399d70ec6d508cf24a068d46008effded70ce Mon Sep 17 00:00:00 2001 From: "aidan.He" Date: Fri, 3 Jul 2026 23:38:12 +0800 Subject: [PATCH 1/7] remove python2, add type hint --- .github/workflows/python-package.yml | 5 +- AGENTS.md | 41 +++++++++++++++ lunardate.egg-info/PKG-INFO | 25 +++++---- lunardate.py | 76 +++++++++++++++------------- setup.py | 11 ++-- 5 files changed, 108 insertions(+), 50 deletions(-) create mode 100644 AGENTS.md diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2fd9a2a..659908f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest + python -m pip install flake8 mypy pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | @@ -35,6 +35,9 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Type check with mypy + run: | + mypy lunardate.py - name: Test with pytest run: | python lunardate.py -v diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6c6625e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# python-lunardate + +## 项目结构 + +单文件库,唯一源码是 `lunardate.py`,无 `src/` 目录,无 `__init__.py`。 + +- setup.py 使用 `py_modules = ['lunardate']`(不是 `packages=`) +- 无 requirements.txt,零外部依赖(纯标准库) +- 兼容 Python 3.7+ + +## 测试 + +所有测试是 `lunardate.py` 中的 doctest,无第三方测试框架: + +```bash +python lunardate.py -v +``` + +CI 中运行方式同上。 + +## Lint & 类型检查 + +```bash +flake8 . --select=E9,F63,F7,F82 --show-source --statistics +flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics +mypy lunardate.py +``` + +## 限制 + +农历仅支持 1900–2099 年。 + +## 公共 API + +仅 `LunarDate` 类。关键方法: + +- `LunarDate(year, month, day, isLeapMonth=False)` — 构造农历日期 +- `LunarDate.fromSolarDate(year, month, day)` — 公历转农历 +- `LunarDate.toSolarDate()` — 农历转公历 +- `LunarDate.today()` — 当前农历日期 +- `LunarDate.leapMonthForYear(year)` — 获取某年闰月(无闰月返回 None) diff --git a/lunardate.egg-info/PKG-INFO b/lunardate.egg-info/PKG-INFO index 840233b..09c28a6 100644 --- a/lunardate.egg-info/PKG-INFO +++ b/lunardate.egg-info/PKG-INFO @@ -1,4 +1,4 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: lunardate Version: 0.2.2 Summary: A Chinese Calendar Library in Pure Python @@ -7,18 +7,26 @@ Author: LI Daobing Author-email: lidaobing@gmail.com License: GPLv3 Classifier: Development Status :: 4 - Beta -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.7 License-File: LICENSE.txt +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: requires-python +Dynamic: summary A Chinese Calendar Library in Pure Python @@ -108,8 +116,7 @@ Usage >>> LunarDate.leapMonthForYear(2023) 2 - >>> LunarDate.leapMonthForYear(2022) - None + >>> LunarDate.leapMonthForYear(2022) # will return None Limits ------ diff --git a/lunardate.py b/lunardate.py index cd23782..37ac0b6 100644 --- a/lunardate.py +++ b/lunardate.py @@ -110,27 +110,35 @@ Another library written in C, including a python binding. ''' +from __future__ import annotations + import datetime +from typing import ClassVar, Iterator, Optional, overload __version__ = "0.2.2" __all__ = ['LunarDate'] -class LunarDate(object): - _startDate = datetime.date(1900, 1, 31) +class LunarDate: + _startDate: ClassVar[datetime.date] = datetime.date(1900, 1, 31) + + year: int + month: int + day: int + isLeapMonth: bool - def __init__(self, year, month, day, isLeapMonth=False): + def __init__(self, year: int, month: int, day: int, isLeapMonth: bool = False) -> None: self.year = year self.month = month self.day = day self.isLeapMonth = bool(isLeapMonth) - def __str__(self): + def __str__(self) -> str: return 'LunarDate(%d, %d, %d, %d)' % (self.year, self.month, self.day, self.isLeapMonth) - __repr__ = __str__ + __repr__: ClassVar = __str__ @staticmethod - def leapMonthForYear(year): + def leapMonthForYear(year: int) -> Optional[int]: ''' return None if no leap month, otherwise return the leap month of the year. return 1 for the first month, and return 12 for the last month. @@ -156,7 +164,7 @@ def leapMonthForYear(year): raise ValueError("yearInfo %r mod 16 should in [0, 12]" % yearInfo) @staticmethod - def fromSolarDate(year, month, day): + def fromSolarDate(year: int, month: int, day: int) -> LunarDate: ''' >>> LunarDate.fromSolarDate(1900, 1, 31) LunarDate(1900, 1, 1, 0) @@ -173,7 +181,7 @@ def fromSolarDate(year, month, day): offset = (solarDate - LunarDate._startDate).days return LunarDate._fromOffset(offset) - def toSolarDate(self): + def toSolarDate(self) -> datetime.date: ''' >>> LunarDate(1900, 1, 1).toSolarDate() datetime.date(1900, 1, 31) @@ -203,7 +211,7 @@ def toSolarDate(self): ValueError: year out of range [1900, 2100) >>> ''' - def _calcDays(yearInfo, month, day, isLeapMonth): + def _calcDays(yearInfo: int, month: int, day: int, isLeapMonth: bool) -> int: isLeapMonth = int(isLeapMonth) res = 0 ok = False @@ -230,7 +238,13 @@ def _calcDays(yearInfo, month, day, isLeapMonth): offset += _calcDays(yearInfos[yearIdx], self.month, self.day, self.isLeapMonth) return self._startDate + datetime.timedelta(days=offset) - def __sub__(self, other): + @overload + def __sub__(self, other: LunarDate) -> datetime.timedelta: ... + @overload + def __sub__(self, other: datetime.date) -> datetime.timedelta: ... + @overload + def __sub__(self, other: datetime.timedelta) -> LunarDate: ... + def __sub__(self, other: LunarDate | datetime.date | datetime.timedelta) -> datetime.timedelta | LunarDate: if isinstance(other, LunarDate): return self.toSolarDate() - other.toSolarDate() elif isinstance(other, datetime.date): @@ -240,20 +254,21 @@ def __sub__(self, other): return LunarDate.fromSolarDate(res.year, res.month, res.day) raise TypeError - def __rsub__(self, other): + def __rsub__(self, other: datetime.date) -> datetime.timedelta | None: if isinstance(other, datetime.date): return other - self.toSolarDate() + return None - def __add__(self, other): + def __add__(self, other: datetime.timedelta) -> LunarDate: if isinstance(other, datetime.timedelta): res = self.toSolarDate() + other return LunarDate.fromSolarDate(res.year, res.month, res.day) raise TypeError - def __radd__(self, other): + def __radd__(self, other: datetime.timedelta) -> LunarDate: return self + other - def __eq__(self, other): + def __eq__(self, other: object) -> bool: ''' >>> LunarDate.today() == 5 False @@ -263,7 +278,7 @@ def __eq__(self, other): return self - other == datetime.timedelta(0) - def __lt__(self, other): + def __lt__(self, other: LunarDate) -> bool: ''' >>> LunarDate.today() < LunarDate.today() False @@ -277,12 +292,12 @@ def __lt__(self, other): except TypeError: raise TypeError("can't compare LunarDate to %s" % (type(other).__name__,)) - def __le__(self, other): + def __le__(self, other: LunarDate) -> bool: # needed because the default implementation tries equality first, # and that does not throw a type error return self < other or self == other - def __gt__(self, other): + def __gt__(self, other: LunarDate) -> bool: ''' >>> LunarDate.today() > LunarDate.today() False @@ -293,7 +308,7 @@ def __gt__(self, other): ''' return not self <= other - def __ge__(self, other): + def __ge__(self, other: LunarDate) -> bool: ''' >>> LunarDate.today() >= LunarDate.today() True @@ -305,12 +320,12 @@ def __ge__(self, other): return not self < other @classmethod - def today(cls): + def today(cls) -> LunarDate: res = datetime.date.today() return cls.fromSolarDate(res.year, res.month, res.day) @staticmethod - def _enumMonth(yearInfo): + def _enumMonth(yearInfo: int) -> Iterator[tuple[int, int, int]]: months = [(i, 0) for i in range(1, 13)] leapMonth = yearInfo % 16 if leapMonth == 0: @@ -328,8 +343,8 @@ def _enumMonth(yearInfo): yield month, days, isLeapMonth @classmethod - def _fromOffset(cls, offset): - def _calcMonthDay(yearInfo, offset): + def _fromOffset(cls, offset: int) -> LunarDate: + def _calcMonthDay(yearInfo: int, offset: int) -> tuple[int, int, bool]: for month, days, isLeapMonth in cls._enumMonth(yearInfo): if offset < days: break @@ -348,7 +363,7 @@ def _calcMonthDay(yearInfo, offset): month, day, isLeapMonth = _calcMonthDay(yearInfo, offset) return LunarDate(year, month, day, isLeapMonth) -yearInfos = [ +yearInfos: list[int] = [ # /* encoding: # b bbbbbbbbbbbb bbbb # bit# 1 111111000000 0000 @@ -404,7 +419,7 @@ def _calcMonthDay(yearInfo, offset): 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, # /* 2099 */ ] -def yearInfo2yearDay(yearInfo): +def yearInfo2yearDay(yearInfo: int) -> int: '''calculate the days in a lunar year from the lunar year's info >>> yearInfo2yearDay(0) # no leap month, and every month has 29 days. @@ -436,17 +451,10 @@ def yearInfo2yearDay(yearInfo): yearInfo //= 2 return res -yearDays = [yearInfo2yearDay(x) for x in yearInfos] - -def day2LunarDate(offset): - offset = int(offset) - res = LunarDate() +yearDays: list[int] = [yearInfo2yearDay(x) for x in yearInfos] - for idx, yearDay in enumerate(yearDays): - if offset < yearDay: - break - offset -= yearDay - res.year = 1900 + idx +def day2LunarDate(offset: int) -> LunarDate: + return LunarDate._fromOffset(offset) if __name__ == '__main__': import doctest diff --git a/setup.py b/setup.py index 02c97b8..d043b5a 100755 --- a/setup.py +++ b/setup.py @@ -13,16 +13,15 @@ author_email = 'lidaobing@gmail.com', url = 'https://github.com/lidaobing/python-lunardate', license = 'GPLv3', + python_requires='>=3.7', classifiers = [ 'Development Status :: 4 - Beta', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries :: Python Modules' From 5695a8d50689bd9778750e8529f18a874de4aba2 Mon Sep 17 00:00:00 2001 From: "aidan.He" Date: Fri, 3 Jul 2026 23:47:15 +0800 Subject: [PATCH 2/7] rename naming styles --- README.md | 13 ++-- lunardate.py | 211 +++++++++++++++++++++++---------------------------- 2 files changed, 104 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index d54c0a3..94b15a2 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ pip install lunardate ``` >>> from lunardate import LunarDate - >>> LunarDate.fromSolarDate(1976, 10, 1) + >>> LunarDate.from_solar_date(1976, 10, 1) LunarDate(1976, 8, 8, 1) - >>> LunarDate(1976, 8, 8, 1).toSolarDate() + >>> LunarDate(1976, 8, 8, 1).to_solar_date() datetime.date(1976, 10, 1) >>> LunarDate(1976, 8, 8, 1).year 1976 @@ -27,7 +27,7 @@ pip install lunardate 8 >>> LunarDate(1976, 8, 8, 1).day 8 - >>> LunarDate(1976, 8, 8, 1).isLeapMonth + >>> LunarDate(1976, 8, 8, 1).is_leap_month True >>> today = LunarDate.today() @@ -68,15 +68,16 @@ pip install lunardate >>> LunarDate.today() == LunarDate.today() True - >>> LunarDate.leapMonthForYear(2023) + >>> LunarDate.leap_month_for_year(2023) 2 - >>> LunarDate.leapMonthForYear(2022) + >>> LunarDate.leap_month_for_year(2022) None ``` ## News -* 0.2.2: add LunarDate.leapMonthForYear; fix bug in year 1899 +* 0.2.3: rename all APIs to PEP 8 snake_case; add type hints; drop Python 2 support +* 0.2.2: add LunarDate.leap_month_for_year; fix bug in year 1899 * 0.2.1: fix bug in year 1956 * 0.2.0: extend year to 2099, thanks to @FuGangqiang * 0.1.5: fix bug in `==` diff --git a/lunardate.py b/lunardate.py index 37ac0b6..76771c5 100644 --- a/lunardate.py +++ b/lunardate.py @@ -14,9 +14,9 @@ Usage ----- - >>> LunarDate.fromSolarDate(1976, 10, 1) + >>> LunarDate.from_solar_date(1976, 10, 1) LunarDate(1976, 8, 8, 1) - >>> LunarDate(1976, 8, 8, 1).toSolarDate() + >>> LunarDate(1976, 8, 8, 1).to_solar_date() datetime.date(1976, 10, 1) >>> LunarDate(1976, 8, 8, 1).year 1976 @@ -24,7 +24,7 @@ 8 >>> LunarDate(1976, 8, 8, 1).day 8 - >>> LunarDate(1976, 8, 8, 1).isLeapMonth + >>> LunarDate(1976, 8, 8, 1).is_leap_month True >>> today = LunarDate.today() @@ -64,37 +64,37 @@ True >>> LunarDate.today() == LunarDate.today() True - >>> before_leap_month = LunarDate.fromSolarDate(2088, 5, 17) + >>> before_leap_month = LunarDate.from_solar_date(2088, 5, 17) >>> before_leap_month.year 2088 >>> before_leap_month.month 4 >>> before_leap_month.day 27 - >>> before_leap_month.isLeapMonth + >>> before_leap_month.is_leap_month False - >>> leap_month = LunarDate.fromSolarDate(2088, 6, 17) + >>> leap_month = LunarDate.from_solar_date(2088, 6, 17) >>> leap_month.year 2088 >>> leap_month.month 4 >>> leap_month.day 28 - >>> leap_month.isLeapMonth + >>> leap_month.is_leap_month True - >>> after_leap_month = LunarDate.fromSolarDate(2088, 7, 17) + >>> after_leap_month = LunarDate.from_solar_date(2088, 7, 17) >>> after_leap_month.year 2088 >>> after_leap_month.month 5 >>> after_leap_month.day 29 - >>> after_leap_month.isLeapMonth + >>> after_leap_month.is_leap_month False - >>> LunarDate.leapMonthForYear(2023) + >>> LunarDate.leap_month_for_year(2023) 2 - >>> LunarDate.leapMonthForYear(2022) # will return None + >>> LunarDate.leap_month_for_year(2022) # will return None Limits ------ @@ -115,128 +115,127 @@ import datetime from typing import ClassVar, Iterator, Optional, overload -__version__ = "0.2.2" +__version__ = "0.2.3" __all__ = ['LunarDate'] class LunarDate: - _startDate: ClassVar[datetime.date] = datetime.date(1900, 1, 31) + _start_date: ClassVar[datetime.date] = datetime.date(1900, 1, 31) year: int month: int day: int - isLeapMonth: bool + is_leap_month: bool - def __init__(self, year: int, month: int, day: int, isLeapMonth: bool = False) -> None: + def __init__(self, year: int, month: int, day: int, is_leap_month: bool = False) -> None: self.year = year self.month = month self.day = day - self.isLeapMonth = bool(isLeapMonth) + self.is_leap_month = bool(is_leap_month) def __str__(self) -> str: - return 'LunarDate(%d, %d, %d, %d)' % (self.year, self.month, self.day, self.isLeapMonth) + return 'LunarDate(%d, %d, %d, %d)' % (self.year, self.month, self.day, self.is_leap_month) __repr__: ClassVar = __str__ @staticmethod - def leapMonthForYear(year: int) -> Optional[int]: + def leap_month_for_year(year: int) -> Optional[int]: ''' return None if no leap month, otherwise return the leap month of the year. return 1 for the first month, and return 12 for the last month. - >>> LunarDate.leapMonthForYear(1976) + >>> LunarDate.leap_month_for_year(1976) 8 - >>> LunarDate.leapMonthForYear(2023) + >>> LunarDate.leap_month_for_year(2023) 2 - >>> LunarDate.leapMonthForYear(2022) + >>> LunarDate.leap_month_for_year(2022) ''' start_year = 1900 - end_year = start_year + len(yearInfos) + end_year = start_year + len(YEAR_INFOS) if year < start_year or year >= end_year: raise ValueError('year out of range [{}, {})'.format(start_year, end_year)) - yearIdx = year - start_year - yearInfo = yearInfos[yearIdx] - leapMonth = yearInfo % 16 - if leapMonth == 0: + year_idx = year - start_year + year_info = YEAR_INFOS[year_idx] + leap_month = year_info % 16 + if leap_month == 0: return None - elif leapMonth <= 12: - return leapMonth + elif leap_month <= 12: + return leap_month else: - raise ValueError("yearInfo %r mod 16 should in [0, 12]" % yearInfo) + raise ValueError("yearInfo %r mod 16 should in [0, 12]" % year_info) @staticmethod - def fromSolarDate(year: int, month: int, day: int) -> LunarDate: + def from_solar_date(year: int, month: int, day: int) -> LunarDate: ''' - >>> LunarDate.fromSolarDate(1900, 1, 31) + >>> LunarDate.from_solar_date(1900, 1, 31) LunarDate(1900, 1, 1, 0) - >>> LunarDate.fromSolarDate(2008, 10, 2) + >>> LunarDate.from_solar_date(2008, 10, 2) LunarDate(2008, 9, 4, 0) - >>> LunarDate.fromSolarDate(1976, 10, 1) + >>> LunarDate.from_solar_date(1976, 10, 1) LunarDate(1976, 8, 8, 1) - >>> LunarDate.fromSolarDate(2033, 10, 23) + >>> LunarDate.from_solar_date(2033, 10, 23) LunarDate(2033, 10, 1, 0) - >>> LunarDate.fromSolarDate(1956, 12, 2) + >>> LunarDate.from_solar_date(1956, 12, 2) LunarDate(1956, 11, 1, 0) ''' - solarDate = datetime.date(year, month, day) - offset = (solarDate - LunarDate._startDate).days - return LunarDate._fromOffset(offset) + solar_date = datetime.date(year, month, day) + offset = (solar_date - LunarDate._start_date).days + return LunarDate._from_offset(offset) - def toSolarDate(self) -> datetime.date: + def to_solar_date(self) -> datetime.date: ''' - >>> LunarDate(1900, 1, 1).toSolarDate() + >>> LunarDate(1900, 1, 1).to_solar_date() datetime.date(1900, 1, 31) - >>> LunarDate(2008, 9, 4).toSolarDate() + >>> LunarDate(2008, 9, 4).to_solar_date() datetime.date(2008, 10, 2) - >>> LunarDate(1976, 8, 8, 1).toSolarDate() + >>> LunarDate(1976, 8, 8, 1).to_solar_date() datetime.date(1976, 10, 1) - >>> LunarDate(1976, 7, 8, 1).toSolarDate() + >>> LunarDate(1976, 7, 8, 1).to_solar_date() Traceback (most recent call last): ... ValueError: month out of range - >>> LunarDate(1899, 1, 1).toSolarDate() + >>> LunarDate(1899, 1, 1).to_solar_date() Traceback (most recent call last): ... ValueError: year out of range [1900, 2100) - >>> LunarDate(2004, 1, 30).toSolarDate() + >>> LunarDate(2004, 1, 30).to_solar_date() Traceback (most recent call last): ... ValueError: day out of range - >>> LunarDate(2004, 13, 1).toSolarDate() + >>> LunarDate(2004, 13, 1).to_solar_date() Traceback (most recent call last): ... ValueError: month out of range - >>> LunarDate(2100, 1, 1).toSolarDate() + >>> LunarDate(2100, 1, 1).to_solar_date() Traceback (most recent call last): ... ValueError: year out of range [1900, 2100) >>> ''' - def _calcDays(yearInfo: int, month: int, day: int, isLeapMonth: bool) -> int: - isLeapMonth = int(isLeapMonth) + def _calc_days(year_info: int, month: int, day: int, is_leap_month: bool) -> int: + is_leap_month = int(is_leap_month) res = 0 - ok = False - for _month, _days, _isLeapMonth in self._enumMonth(yearInfo): - if (_month, _isLeapMonth) == (month, isLeapMonth): - if 1 <= day <= _days: + for m, d, is_leap in self._enum_month(year_info): + if (m, is_leap) == (month, is_leap_month): + if 1 <= day <= d: res += day - 1 return res else: raise ValueError("day out of range") - res += _days + res += d raise ValueError("month out of range") offset = 0 start_year = 1900 - end_year = start_year + len(yearInfos) + end_year = start_year + len(YEAR_INFOS) if self.year < start_year or self.year >= end_year: raise ValueError('year out of range [{}, {})'.format(start_year, end_year)) - yearIdx = self.year - start_year - for i in range(yearIdx): - offset += yearDays[i] + year_idx = self.year - start_year + for i in range(year_idx): + offset += YEAR_DAYS[i] - offset += _calcDays(yearInfos[yearIdx], self.month, self.day, self.isLeapMonth) - return self._startDate + datetime.timedelta(days=offset) + offset += _calc_days(YEAR_INFOS[year_idx], self.month, self.day, self.is_leap_month) + return self._start_date + datetime.timedelta(days=offset) @overload def __sub__(self, other: LunarDate) -> datetime.timedelta: ... @@ -246,23 +245,23 @@ def __sub__(self, other: datetime.date) -> datetime.timedelta: ... def __sub__(self, other: datetime.timedelta) -> LunarDate: ... def __sub__(self, other: LunarDate | datetime.date | datetime.timedelta) -> datetime.timedelta | LunarDate: if isinstance(other, LunarDate): - return self.toSolarDate() - other.toSolarDate() + return self.to_solar_date() - other.to_solar_date() elif isinstance(other, datetime.date): - return self.toSolarDate() - other + return self.to_solar_date() - other elif isinstance(other, datetime.timedelta): - res = self.toSolarDate() - other - return LunarDate.fromSolarDate(res.year, res.month, res.day) + res = self.to_solar_date() - other + return LunarDate.from_solar_date(res.year, res.month, res.day) raise TypeError def __rsub__(self, other: datetime.date) -> datetime.timedelta | None: if isinstance(other, datetime.date): - return other - self.toSolarDate() + return other - self.to_solar_date() return None def __add__(self, other: datetime.timedelta) -> LunarDate: if isinstance(other, datetime.timedelta): - res = self.toSolarDate() + other - return LunarDate.fromSolarDate(res.year, res.month, res.day) + res = self.to_solar_date() + other + return LunarDate.from_solar_date(res.year, res.month, res.day) raise TypeError def __radd__(self, other: datetime.timedelta) -> LunarDate: @@ -322,48 +321,48 @@ def __ge__(self, other: LunarDate) -> bool: @classmethod def today(cls) -> LunarDate: res = datetime.date.today() - return cls.fromSolarDate(res.year, res.month, res.day) + return cls.from_solar_date(res.year, res.month, res.day) @staticmethod - def _enumMonth(yearInfo: int) -> Iterator[tuple[int, int, int]]: + def _enum_month(year_info: int) -> Iterator[tuple[int, int, int]]: months = [(i, 0) for i in range(1, 13)] - leapMonth = yearInfo % 16 - if leapMonth == 0: + leap_month = year_info % 16 + if leap_month == 0: pass - elif leapMonth <= 12: - months.insert(leapMonth, (leapMonth, 1)) + elif leap_month <= 12: + months.insert(leap_month, (leap_month, 1)) else: - raise ValueError("yearInfo %r mod 16 should in [0, 12]" % yearInfo) + raise ValueError("yearInfo %r mod 16 should in [0, 12]" % year_info) - for month, isLeapMonth in months: - if isLeapMonth: - days = (yearInfo >> 16) % 2 + 29 + for month, is_leap_month in months: + if is_leap_month: + days = (year_info >> 16) % 2 + 29 else: - days = (yearInfo >> (16 - month)) % 2 + 29 - yield month, days, isLeapMonth + days = (year_info >> (16 - month)) % 2 + 29 + yield month, days, is_leap_month @classmethod - def _fromOffset(cls, offset: int) -> LunarDate: - def _calcMonthDay(yearInfo: int, offset: int) -> tuple[int, int, bool]: - for month, days, isLeapMonth in cls._enumMonth(yearInfo): + def _from_offset(cls, offset: int) -> LunarDate: + def _calc_month_day(year_info: int, offset: int) -> tuple[int, int, bool]: + for month, days, is_leap_month in cls._enum_month(year_info): if offset < days: break offset -= days - return (month, offset + 1, isLeapMonth) + return (month, offset + 1, is_leap_month) offset = int(offset) - for idx, yearDay in enumerate(yearDays): - if offset < yearDay: + for idx, year_day in enumerate(YEAR_DAYS): + if offset < year_day: break - offset -= yearDay + offset -= year_day year = 1900 + idx - yearInfo = yearInfos[idx] - month, day, isLeapMonth = _calcMonthDay(yearInfo, offset) - return LunarDate(year, month, day, isLeapMonth) + year_info = YEAR_INFOS[idx] + month, day, is_leap_month = _calc_month_day(year_info, offset) + return LunarDate(year, month, day, is_leap_month) -yearInfos: list[int] = [ +YEAR_INFOS: list[int] = [ # /* encoding: # b bbbbbbbbbbbb bbbb # bit# 1 111111000000 0000 @@ -419,42 +418,26 @@ def _calcMonthDay(yearInfo: int, offset: int) -> tuple[int, int, bool]: 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, # /* 2099 */ ] -def yearInfo2yearDay(yearInfo: int) -> int: - '''calculate the days in a lunar year from the lunar year's info - - >>> yearInfo2yearDay(0) # no leap month, and every month has 29 days. - 348 - >>> yearInfo2yearDay(1) # 1 leap month, and every month has 29 days. - 377 - >>> yearInfo2yearDay((2**12-1)*16) # no leap month, and every month has 30 days. - 360 - >>> yearInfo2yearDay((2**13-1)*16+1) # 1 leap month, and every month has 30 days. - 390 - >>> # 1 leap month, and every normal month has 30 days, and leap month has 29 days. - >>> yearInfo2yearDay((2**12-1)*16+1) - 389 - ''' - yearInfo = int(yearInfo) - +def year_info_to_year_day(year_info: int) -> int: res = 29 * 12 leap = False - if yearInfo % 16 != 0: + if year_info % 16 != 0: leap = True res += 29 - yearInfo //= 16 + year_info //= 16 for i in range(12 + leap): - if yearInfo % 2 == 1: + if year_info % 2 == 1: res += 1 - yearInfo //= 2 + year_info //= 2 return res -yearDays: list[int] = [yearInfo2yearDay(x) for x in yearInfos] +YEAR_DAYS: list[int] = [year_info_to_year_day(x) for x in YEAR_INFOS] -def day2LunarDate(offset: int) -> LunarDate: - return LunarDate._fromOffset(offset) +def day_to_lunar_date(offset: int) -> LunarDate: + return LunarDate._from_offset(offset) if __name__ == '__main__': import doctest From d69355b1f1b132f8faaffe52f03d321a3676a6c6 Mon Sep 17 00:00:00 2001 From: "aidan.He" Date: Fri, 3 Jul 2026 23:51:19 +0800 Subject: [PATCH 3/7] sync AGENTS.md --- AGENTS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6c6625e..399ca6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,8 +34,8 @@ mypy lunardate.py 仅 `LunarDate` 类。关键方法: -- `LunarDate(year, month, day, isLeapMonth=False)` — 构造农历日期 -- `LunarDate.fromSolarDate(year, month, day)` — 公历转农历 -- `LunarDate.toSolarDate()` — 农历转公历 +- `LunarDate(year, month, day, is_leap_month=False)` — 构造农历日期 +- `LunarDate.from_solar_date(year, month, day)` — 公历转农历 +- `LunarDate.to_solar_date()` — 农历转公历 - `LunarDate.today()` — 当前农历日期 -- `LunarDate.leapMonthForYear(year)` — 获取某年闰月(无闰月返回 None) +- `LunarDate.leap_month_for_year(year)` — 获取某年闰月(无闰月返回 None) From 7ad9ebb930c1f6061aaf37e54a1871f107995284 Mon Sep 17 00:00:00 2001 From: "aidan.He" Date: Sun, 5 Jul 2026 22:06:49 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20review=E4=B8=AD=E6=8F=90=E5=87=BA?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 41 ----------------------------------------- README.md | 10 ++++------ lunardate.py | 17 +++++++++++++---- 3 files changed, 17 insertions(+), 51 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 399ca6f..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,41 +0,0 @@ -# python-lunardate - -## 项目结构 - -单文件库,唯一源码是 `lunardate.py`,无 `src/` 目录,无 `__init__.py`。 - -- setup.py 使用 `py_modules = ['lunardate']`(不是 `packages=`) -- 无 requirements.txt,零外部依赖(纯标准库) -- 兼容 Python 3.7+ - -## 测试 - -所有测试是 `lunardate.py` 中的 doctest,无第三方测试框架: - -```bash -python lunardate.py -v -``` - -CI 中运行方式同上。 - -## Lint & 类型检查 - -```bash -flake8 . --select=E9,F63,F7,F82 --show-source --statistics -flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics -mypy lunardate.py -``` - -## 限制 - -农历仅支持 1900–2099 年。 - -## 公共 API - -仅 `LunarDate` 类。关键方法: - -- `LunarDate(year, month, day, is_leap_month=False)` — 构造农历日期 -- `LunarDate.from_solar_date(year, month, day)` — 公历转农历 -- `LunarDate.to_solar_date()` — 农历转公历 -- `LunarDate.today()` — 当前农历日期 -- `LunarDate.leap_month_for_year(year)` — 获取某年闰月(无闰月返回 None) diff --git a/README.md b/README.md index 94b15a2..68ff0a8 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ [![PyPI - Version](https://img.shields.io/pypi/v/lunardate)](https://pypi.org/project/lunardate/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/lunardate)](https://pypistats.org/packages/lunardate) - -Chinese Calendar: http://en.wikipedia.org/wiki/Chinese_calendar +Chinese Calendar: ## Install @@ -76,8 +75,7 @@ pip install lunardate ## News -* 0.2.3: rename all APIs to PEP 8 snake_case; add type hints; drop Python 2 support -* 0.2.2: add LunarDate.leap_month_for_year; fix bug in year 1899 +* 0.2.2: add LunarDate.leapMonthForYear; fix bug in year 1899 * 0.2.1: fix bug in year 1956 * 0.2.0: extend year to 2099, thanks to @FuGangqiang * 0.1.5: fix bug in `==` @@ -90,7 +88,7 @@ this library can only deal with year from 1900 to 2099 (in chinese calendar). ## See also -* lunar: http://packages.qa.debian.org/l/lunar.html, +* lunar: , A converter written in C, this program is derived from it. -* python-lunar: http://code.google.com/p/liblunar/ +* python-lunar: Another library written in C, including a python binding. diff --git a/lunardate.py b/lunardate.py index 76771c5..602c20a 100644 --- a/lunardate.py +++ b/lunardate.py @@ -113,6 +113,7 @@ from __future__ import annotations import datetime +import warnings from typing import ClassVar, Iterator, Optional, overload __version__ = "0.2.3" @@ -137,6 +138,15 @@ def __str__(self) -> str: __repr__: ClassVar = __str__ + @staticmethod + def leapMonthForYear(year: int) -> Optional[int]: + warnings.warn( + "leapMonthForYear 已废弃,请使用 leap_month_for_year 代替", + DeprecationWarning, + stacklevel=2 # 让警告指向调用者的代码行,而不是这里 + ) + return LunarDate.leap_month_for_year(year) + @staticmethod def leap_month_for_year(year: int) -> Optional[int]: ''' @@ -211,8 +221,7 @@ def to_solar_date(self) -> datetime.date: ValueError: year out of range [1900, 2100) >>> ''' - def _calc_days(year_info: int, month: int, day: int, is_leap_month: bool) -> int: - is_leap_month = int(is_leap_month) + def _calc_days(year_info: int, month: int, day: int, is_leap_month: int) -> int: res = 0 for m, d, is_leap in self._enum_month(year_info): if (m, is_leap) == (month, is_leap_month): @@ -343,7 +352,7 @@ def _enum_month(year_info: int) -> Iterator[tuple[int, int, int]]: @classmethod def _from_offset(cls, offset: int) -> LunarDate: - def _calc_month_day(year_info: int, offset: int) -> tuple[int, int, bool]: + def _calc_month_day(year_info: int, offset: int) -> tuple[int, int, int]: for month, days, is_leap_month in cls._enum_month(year_info): if offset < days: break @@ -360,7 +369,7 @@ def _calc_month_day(year_info: int, offset: int) -> tuple[int, int, bool]: year_info = YEAR_INFOS[idx] month, day, is_leap_month = _calc_month_day(year_info, offset) - return LunarDate(year, month, day, is_leap_month) + return LunarDate(year, month, day, bool(is_leap_month)) YEAR_INFOS: list[int] = [ # /* encoding: From 096bf0bf049fa6197673c11ddad10f32607965c4 Mon Sep 17 00:00:00 2001 From: "aidan.He" Date: Sun, 5 Jul 2026 23:36:05 +0800 Subject: [PATCH 5/7] fix: keep it backword compatible --- lunardate.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/lunardate.py b/lunardate.py index 602c20a..c4693fc 100644 --- a/lunardate.py +++ b/lunardate.py @@ -116,7 +116,7 @@ import warnings from typing import ClassVar, Iterator, Optional, overload -__version__ = "0.2.3" +__version__ = "0.2.2" __all__ = ['LunarDate'] class LunarDate: @@ -132,6 +132,7 @@ def __init__(self, year: int, month: int, day: int, is_leap_month: bool = False) self.month = month self.day = day self.is_leap_month = bool(is_leap_month) + self.isLeapMonth = self.is_leap_month # deprecated def __str__(self) -> str: return 'LunarDate(%d, %d, %d, %d)' % (self.year, self.month, self.day, self.is_leap_month) @@ -141,9 +142,9 @@ def __str__(self) -> str: @staticmethod def leapMonthForYear(year: int) -> Optional[int]: warnings.warn( - "leapMonthForYear 已废弃,请使用 leap_month_for_year 代替", + "leapMonthForYear is deprecated, use leap_month_for_year instead", DeprecationWarning, - stacklevel=2 # 让警告指向调用者的代码行,而不是这里 + stacklevel=2 ) return LunarDate.leap_month_for_year(year) @@ -172,6 +173,15 @@ def leap_month_for_year(year: int) -> Optional[int]: return leap_month else: raise ValueError("yearInfo %r mod 16 should in [0, 12]" % year_info) + + @staticmethod + def fromSolarDate(year: int, month: int, day: int) -> LunarDate: + warnings.warn( + "fromSolarDate is deprecated, use from_solar_date instead", + DeprecationWarning, + stacklevel=2 + ) + return LunarDate.from_solar_date(year, month, day) @staticmethod def from_solar_date(year: int, month: int, day: int) -> LunarDate: @@ -190,6 +200,15 @@ def from_solar_date(year: int, month: int, day: int) -> LunarDate: solar_date = datetime.date(year, month, day) offset = (solar_date - LunarDate._start_date).days return LunarDate._from_offset(offset) + + def toSolarDate(self) -> datetime.date: + warnings.warn( + "toSolarDate is deprecated, use to_solar_date instead", + DeprecationWarning, + stacklevel=2 + ) + return self.to_solar_date() + def to_solar_date(self) -> datetime.date: ''' @@ -428,6 +447,20 @@ def _calc_month_day(year_info: int, offset: int) -> tuple[int, int, int]: ] def year_info_to_year_day(year_info: int) -> int: + '''calculate the days in a lunar year from the lunar year's info + + >>> year_info_to_year_day(0) # no leap month, and every month has 29 days. + 348 + >>> year_info_to_year_day(1) # 1 leap month, and every month has 29 days. + 377 + >>> year_info_to_year_day((2**12-1)*16) # no leap month, and every month has 30 days. + 360 + >>> year_info_to_year_day((2**13-1)*16+1) # 1 leap month, and every month has 30 days. + 390 + >>> # 1 leap month, and every normal month has 30 days, and leap month has 29 days. + >>> year_info_to_year_day((2**12-1)*16+1) + 389 + ''' res = 29 * 12 leap = False From c816d3d0a533bdaf0ca55ce3d5e26de11cce4376 Mon Sep 17 00:00:00 2001 From: "aidan.He" Date: Sun, 5 Jul 2026 23:37:32 +0800 Subject: [PATCH 6/7] fix: keep it backword compatible --- lunardate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lunardate.py b/lunardate.py index c4693fc..32047bc 100644 --- a/lunardate.py +++ b/lunardate.py @@ -126,6 +126,7 @@ class LunarDate: month: int day: int is_leap_month: bool + isLeapMonth: bool # deprecated def __init__(self, year: int, month: int, day: int, is_leap_month: bool = False) -> None: self.year = year From 9950f363e3e450c82a3bdca36c27ee3ce711fbf1 Mon Sep 17 00:00:00 2001 From: "aidan.He" Date: Mon, 6 Jul 2026 11:29:29 +0800 Subject: [PATCH 7/7] refactor: isLeapMonth to property, | to Union --- lunardate.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lunardate.py b/lunardate.py index 32047bc..c79681a 100644 --- a/lunardate.py +++ b/lunardate.py @@ -114,7 +114,7 @@ import datetime import warnings -from typing import ClassVar, Iterator, Optional, overload +from typing import ClassVar, Iterator, Optional, Union, overload __version__ = "0.2.2" __all__ = ['LunarDate'] @@ -126,19 +126,22 @@ class LunarDate: month: int day: int is_leap_month: bool - isLeapMonth: bool # deprecated def __init__(self, year: int, month: int, day: int, is_leap_month: bool = False) -> None: self.year = year self.month = month self.day = day self.is_leap_month = bool(is_leap_month) - self.isLeapMonth = self.is_leap_month # deprecated def __str__(self) -> str: return 'LunarDate(%d, %d, %d, %d)' % (self.year, self.month, self.day, self.is_leap_month) - __repr__: ClassVar = __str__ + __repr__ = __str__ + + @property + def isLeapMonth(self) -> bool: + warnings.warn("use is_leap_month", DeprecationWarning) + return self.is_leap_month @staticmethod def leapMonthForYear(year: int) -> Optional[int]: @@ -272,7 +275,7 @@ def __sub__(self, other: LunarDate) -> datetime.timedelta: ... def __sub__(self, other: datetime.date) -> datetime.timedelta: ... @overload def __sub__(self, other: datetime.timedelta) -> LunarDate: ... - def __sub__(self, other: LunarDate | datetime.date | datetime.timedelta) -> datetime.timedelta | LunarDate: + def __sub__(self, other: Union[LunarDate, datetime.date, datetime.timedelta]) -> Union[datetime.timedelta, LunarDate]: if isinstance(other, LunarDate): return self.to_solar_date() - other.to_solar_date() elif isinstance(other, datetime.date): @@ -282,7 +285,7 @@ def __sub__(self, other: LunarDate | datetime.date | datetime.timedelta) -> date return LunarDate.from_solar_date(res.year, res.month, res.day) raise TypeError - def __rsub__(self, other: datetime.date) -> datetime.timedelta | None: + def __rsub__(self, other: datetime.date) -> Union[datetime.timedelta, None]: if isinstance(other, datetime.date): return other - self.to_solar_date() return None