Files
moxiaonian d9da4301cd 公众授权版:normal · 1000 次 · 有效期至 2026-12-31
副本编号 LY-20261007-AC63(水印指纹已嵌入)
含 README、.gitignore;排除 .DS_Store / __pycache__ / .pytest_cache
2026-10-07 17:29:52 +08:00

362 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding:utf-8 -*-
from __future__ import annotations
import re
from typing import Union, List, TYPE_CHECKING
from tyme4py import AbstractCulture
from tyme4py.enums import EventType
if TYPE_CHECKING:
from tyme4py.solar import SolarMonth, SolarDay, SolarTerm
from tyme4py.lunar import LunarMonth, LunarDay
class Event(AbstractCulture):
"""事件"""
def __init__(self, name: str, data: str):
Event.validate(data)
self._name = name
self._data = data
@staticmethod
def validate(data: str) -> None:
if len(data) != 9:
raise ValueError(f'illegal event data: {data}')
@staticmethod
def builder() -> EventBuilder:
"""构造器"""
return EventBuilder()
@classmethod
def from_name(cls, name: str) -> Union[Event, None]:
pattern: re.Pattern[str] = re.compile(f'{EventManager.REGEX}{name}')
matcher: Union[re.Match[str], None] = pattern.search(EventManager.DATA)
return cls(name, matcher.group(1)) if matcher else None
def _get_char_index(self, index: int) -> int:
return EventManager.CHARS.index(self._data[index])
def get_value(self, index: int) -> int:
return self._get_char_index(index) - 31
def get_month(self, year: int) -> List[int]:
y: int = year
m: int = self.get_value(2)
if m > 12:
m = 1
y += 1
return [y, m]
def get_type(self) -> EventType:
"""事件类型"""
n: int = self._get_char_index(1)
if n == 1:
return EventType.SOLAR_WEEK
if n == 2:
return EventType.LUNAR_DAY
if n == 3:
return EventType.TERM_DAY
if n == 4:
return EventType.TERM_HS
if n == 5:
return EventType.TERM_EB
return EventType.SOLAR_DAY
def get_name(self) -> str:
"""名称"""
return self._name
def get_data(self) -> str:
"""数据"""
return self._data
def get_start_year(self) -> int:
"""起始年"""
n: int = 0
size: int = len(EventManager.CHARS)
for i in range(0, 3):
n = n * size + self._get_char_index(6 + i)
return n
def get_solar_day(self, year: int) -> Union[SolarDay, None]:
if year < self.get_start_year():
return None
t: EventType = self.get_type()
d: Union[SolarDay, None] = None
if t == EventType.SOLAR_DAY:
d = self._get_solar_day_by_solar_day(year)
elif t == EventType.SOLAR_WEEK:
d = self._get_solar_day_by_week(year)
elif t == EventType.LUNAR_DAY:
d = self._get_solar_day_by_lunar_day(year)
elif t == EventType.TERM_DAY:
d = self._get_solar_day_by_term(year)
elif t == EventType.TERM_HS:
d = self._get_solar_day_by_term_heaven_stem(year)
elif t == EventType.TERM_EB:
d = self._get_solar_day_by_term_earth_branch(year)
if d is None:
return None
offset: int = self.get_value(5)
return d if 0 == offset else d.next(offset)
def _get_solar_day_by_solar_day(self, year: int) -> Union[SolarDay, None]:
from tyme4py.solar import SolarMonth, SolarDay
month: List[int] = self.get_month(year)
y: int = month[0]
m: int = month[1]
d: int = self.get_value(3)
delay: int = self.get_value(4)
last_day: int = SolarMonth.from_ym(y, m).get_day_count()
if d > last_day:
if 0 == delay:
return None
elif delay < 0:
return SolarDay.from_ymd(y, m, d + delay)
return SolarDay.from_ymd(y, m, last_day).next(delay)
return SolarDay.from_ymd(y, m, d)
def _get_solar_day_by_lunar_day(self, year: int) -> Union[SolarDay, None]:
from tyme4py.lunar import LunarMonth, LunarDay
month: List[int] = self.get_month(year)
y: int = month[0]
m: int = month[1]
d: int = self.get_value(3)
delay: int = self.get_value(4)
last_day: int = LunarMonth.from_ym(y, m).get_day_count()
if d > last_day:
if 0 == delay:
return None
elif delay < 0:
return LunarDay.from_ymd(y, m, d + delay).get_solar_day()
return LunarDay.from_ymd(y, m, last_day).get_solar_day().next(delay)
return LunarDay.from_ymd(y, m, d).get_solar_day()
def _get_solar_day_by_week(self, year: int) -> Union[SolarDay, None]:
from tyme4py.solar import SolarMonth, SolarDay
# 第几个星期
n: int = self.get_value(3)
if n == 0:
return None
m: SolarMonth = SolarMonth.from_ym(year, self.get_value(2))
# 星期几
w: int = self.get_value(4)
if n > 0:
# 当月第1天
d: SolarDay = m.get_first_day()
# 往后找第几个星期几
return d.next(d.get_week().steps_to(w) + 7 * n - 7)
# 当月最后一天
d: SolarDay = SolarDay.from_ymd(year, m.get_month(), m.get_day_count())
# 往前找第几个星期几
return d.next(d.get_week().steps_back_to(w) + 7 * n + 7)
def _get_solar_day_by_term(self, year: int) -> Union[SolarDay, None]:
from tyme4py.solar import SolarTerm, SolarDay
d: SolarDay = SolarTerm.from_index(year, self.get_value(2)).get_solar_day()
offset: int = self.get_value(4)
return d if 0 == offset else d.next(offset)
def _get_solar_day_by_term_heaven_stem(self, year: int) -> Union[SolarDay, None]:
from tyme4py.solar import SolarDay
d: SolarDay = self._get_solar_day_by_term(year)
return d.next(d.get_lunar_day().get_sixty_cycle().get_heaven_stem().steps_to(self.get_value(3)))
def _get_solar_day_by_term_earth_branch(self, year: int) -> Union[SolarDay, None]:
from tyme4py.solar import SolarDay
d: SolarDay = self._get_solar_day_by_term(year)
return d.next(d.get_lunar_day().get_sixty_cycle().get_earth_branch().steps_to(self.get_value(3)))
@classmethod
def from_solar_day(cls, d: SolarDay) -> List[Event]:
"""
指定公历日的事件列表
:param d: 公历日
"""
l: List[Event] = []
for e in cls.all():
if d == e.get_solar_day(d.get_year()):
l.append(e)
return l
@classmethod
def all(cls) -> List[Event]:
l: List[Event] = []
pattern: re.Pattern[str] = re.compile(f'{EventManager.REGEX}(.[^@]+)')
matcher: Union[re.Match[str], None] = pattern.search(EventManager.DATA)
while matcher:
l.append(cls(matcher.group(2), matcher.group(1)))
matcher = pattern.search(EventManager.DATA, matcher.end())
return l
class EventBuilder:
"""事件构造器"""
def __init__(self):
self._name = ''
self._data = ['@', '_', '_', '_', '_', '_', '0', '0', '0']
def name(self, n: str) -> EventBuilder:
self._name = n
return self
@staticmethod
def _get_char(index: int) -> str:
return EventManager.CHARS[index]
def _set_value(self, index: int, n: int) -> EventBuilder:
self._data[index] = self._get_char(31 + n)
return self
@staticmethod
def encode_type(t: EventType) -> str:
n: int = 0
if t == EventType.SOLAR_WEEK:
n = 1
elif t == EventType.LUNAR_DAY:
n = 2
elif t == EventType.TERM_DAY:
n = 3
elif t == EventType.TERM_HS:
n = 4
elif t == EventType.TERM_EB:
n = 5
return EventBuilder._get_char(n)
def _content(self, t: EventType, a: int, b: int, c: int) -> EventBuilder:
self._data[1] = self.encode_type(t)
return self._set_value(2, a)._set_value(3, b)._set_value(4, c)
def solar_day(self, solar_month: int, solar_day: int, delay_days: int) -> EventBuilder:
"""
公历日
:param solar_month: 公历月(1至12)
:param solar_day: 公历日(1至31)
:param delay_days: 顺延天数,例如生日在2月29,非闰年没有2月29,是+1天,还是-1天(最远支持-31至31天)
:return: 事件构造器
"""
return self._content(EventType.SOLAR_DAY, solar_month, solar_day, delay_days)
def lunar_day(self, lunar_month: int, lunar_day: int, delay_days: int) -> EventBuilder:
"""
农历日
:param lunar_month: 农历月(-12至-1,1至12,闰月为负)
:param lunar_day: 农历日(1至30)
:param delay_days: 顺延天数,例如生日在某月的三十,但下一年当月可能只有29天,是+1天,还是-1天(最远支持-31至31天)
:return: 事件构造器
"""
return self._content(EventType.LUNAR_DAY, lunar_month, lunar_day, delay_days)
def solar_week(self, solar_month: int, week_index: int, week: int) -> EventBuilder:
"""
公历第几个星期几
:param solar_month: 公历月(1至12)
:param week_index: 第几个星期(1为第1个星期,-1为倒数第1个星期)
:param week: 星期几(0至6,0代表星期天,1代表星期一)
:return: 事件构造器
"""
return self._content(EventType.SOLAR_WEEK, solar_month, week_index, week)
def term_day(self, term_index: int, delay_days: int) -> EventBuilder:
"""
节气
:param term_index: 节气索引(0至23)
:param delay_days: 顺延天数(最远支持-31至31天)
:return: 事件构造器
"""
return self._content(EventType.TERM_DAY, term_index, 0, delay_days)
def term_heaven_stem(self, term_index: int, heaven_stem_index: int, delay_days: int) -> EventBuilder:
"""
节气天干
:param term_index: 节气索引(0至23)
:param heaven_stem_index: 天干索引(0至9)
:param delay_days: 顺延天数(最远支持-31至31天)
:return: 事件构造器
"""
return self._content(EventType.TERM_HS, term_index, heaven_stem_index, delay_days)
def term_earth_branch(self, term_index: int, earth_branch_index: int, delay_days: int) -> EventBuilder:
"""
节气地支
:param term_index: 节气索引(0至23)
:param earth_branch_index: 地支索引(0至11)
:param delay_days: 顺延天数(最远支持-31至31天)
:return: 事件构造器
"""
return self._content(EventType.TERM_EB, term_index, earth_branch_index, delay_days)
def start_year(self, year: int) -> EventBuilder:
"""
起始年
:param year: 年
:return: 事件构造器
"""
size: int = len(EventManager.CHARS)
n: int = year
for i in range(3):
self._data[8 - i] = self._get_char(n % size)
n //= size
return self
def offset(self, days: int) -> EventBuilder:
"""
偏移天数
:param days: 天数(最远支持-31至31天)
:return: 事件构造器
"""
return self._set_value(5, days)
def build(self) -> Event:
"""
生成事件
:return: 事件
"""
return Event(self._name, ''.join(self._data))
class EventManager:
"""事件管理器"""
CHARS: str = '0123456789ABCDEFGHIJKLMNOPQRSTU_VWXYZabcdefghijklmnopqrstuvwxyz'
"""有效字符"""
DATA: str = ''
"""全量事件数据"""
REGEX: str = '(@[0-9A-Za-z_]{8})'
"""数据匹配的正则表达式"""
@classmethod
def remove(cls, name: str) -> None:
"""
删除事件
:param name: 名称
"""
cls.DATA = re.sub(rf'{cls.REGEX}{name}', '', cls.DATA)
@classmethod
def _save_or_update(cls, name: str, data: str) -> None:
"""
删除事件
:param name: 名称
"""
pattern: re.Pattern[str] = re.compile(f'{cls.REGEX}{name}')
matcher: Union[re.Match[str], None] = pattern.search(cls.DATA)
if matcher:
cls.DATA = re.sub(rf'{cls.REGEX}{name}', data, EventManager.DATA)
else:
cls.DATA += data
@classmethod
def update(cls, name: str, e: Event) -> None:
cls._save_or_update(name, e.get_data() + name if e.get_name() is None or len(e.get_name()) < 1 else e.get_name())
@classmethod
def update_data(cls, name: str, data: str) -> None:
Event.validate(data)
cls._save_or_update(name, data)