"""
生辰信息数据模型
"""
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class BirthInfo:
"""生辰信息数据类"""
date: str # YYYY-MM-DD格式
time_index: int # 0-12,对应子时到晚子时
gender: str # "男" 或 "女"
calendar: str = "solar" # "solar" 或 "lunar"
is_leap_month: bool = False # 是否闰月(仅农历有效)
# 真太阳时支持(可选)
longitude: Optional[float] = None # 出生地经度(东经为正,西经为负)
latitude: Optional[float] = None # 出生地纬度(北纬为正,南纬为负)
use_solar_time: bool = False # 是否使用真太阳时修正
birth_hour: Optional[int] = None # 出生时刻小时(0-23),用于真太阳时精确计算
birth_minute: Optional[int] = None # 出生时刻分钟(0-59),用于真太阳时精确计算
def __post_init__(self):
"""验证数据有效性"""
if not 0 <= self.time_index <= 12:
raise ValueError("时辰序号必须在0-12之间")
if self.gender not in ["男", "女"]:
raise ValueError("性别必须是'男'或'女'")
if self.calendar not in ["solar", "lunar"]:
raise ValueError("历法类型必须是'solar'或'lunar'")
# 验证日期格式
try:
datetime.strptime(self.date, "%Y-%m-%d")
except ValueError:
raise ValueError("日期格式必须是YYYY-MM-DD")
# 真太阳时验证
if self.use_solar_time:
if self.longitude is None:
raise ValueError("使用真太阳时必须提供经度(longitude)")
if not -180 <= self.longitude <= 180:
raise ValueError("经度必须在-180到180之间")
# 如果提供了纬度,验证范围
if self.latitude is not None and not -90 <= self.latitude <= 90:
raise ValueError("纬度必须在-90到90之间")
# 如果提供了具体时刻,验证范围
if self.birth_hour is not None and not 0 <= self.birth_hour <= 23:
raise ValueError("小时必须在0-23之间")
if self.birth_minute is not None and not 0 <= self.birth_minute <= 59:
raise ValueError("分钟必须在0-59之间")
# 如果使用真太阳时,建议提供具体时刻
if self.birth_hour is None or self.birth_minute is None:
# 这里只是警告,不抛出异常,因为可以使用time_index的中点时间
pass
def to_dict(self) -> dict:
"""转换为字典"""
data = {
"date": self.date,
"time_index": self.time_index,
"gender": self.gender,
"calendar": self.calendar,
"is_leap_month": self.is_leap_month,
}
# 添加真太阳时相关字段(如果存在)
if self.longitude is not None:
data["longitude"] = self.longitude
if self.latitude is not None:
data["latitude"] = self.latitude
if self.use_solar_time:
data["use_solar_time"] = self.use_solar_time
if self.birth_hour is not None:
data["birth_hour"] = self.birth_hour
if self.birth_minute is not None:
data["birth_minute"] = self.birth_minute
return data
@classmethod
def from_dict(cls, data: dict) -> "BirthInfo":
"""从字典创建"""
return cls(
date=data["date"],
time_index=data["time_index"],
gender=data["gender"],
calendar=data.get("calendar", "solar"),
is_leap_month=data.get("is_leap_month", False),
longitude=data.get("longitude"),
latitude=data.get("latitude"),
use_solar_time=data.get("use_solar_time", False),
birth_hour=data.get("birth_hour"),
birth_minute=data.get("birth_minute"),
)
def get_time_range(self) -> str:
"""获取时辰对应的时间段"""
time_ranges = [
"23:00~01:00", # 0 早子时
"01:00~03:00", # 1 丑时
"03:00~05:00", # 2 寅时
"05:00~07:00", # 3 卯时
"07:00~09:00", # 4 辰时
"09:00~11:00", # 5 巳时
"11:00~13:00", # 6 午时
"13:00~15:00", # 7 未时
"15:00~17:00", # 8 申时
"17:00~19:00", # 9 酉时
"19:00~21:00", # 10 戌时
"21:00~23:00", # 11 亥时
"23:00~01:00", # 12 晚子时
]
return time_ranges[self.time_index]
def get_time_name(self) -> str:
"""获取时辰名称"""
time_names = [
"早子时",
"丑时",
"寅时",
"卯时",
"辰时",
"巳时",
"午时",
"未时",
"申时",
"酉时",
"戌时",
"亥时",
"晚子时",
]
return time_names[self.time_index]
def get_adjusted_time_index(self) -> int:
"""
获取真太阳时修正后的时辰序号
如果use_solar_time=True且提供了经度,则根据真太阳时计算时辰序号;
否则返回原始的time_index。
Returns:
修正后的时辰序号(0-12)
Examples:
>>> # 不使用真太阳时
>>> info = BirthInfo(date="2000-08-16", time_index=6, gender="女")
>>> info.get_adjusted_time_index()
6
>>> # 使用真太阳时(乌鲁木齐,经度87.6°,时差约-129分钟)
>>> info = BirthInfo(
... date="2000-08-16", time_index=6, gender="女",
... longitude=87.6, use_solar_time=True,
... birth_hour=12, birth_minute=0
... )
>>> info.get_adjusted_time_index() # 12:00 - 129分钟 ≈ 9:51,仍是巳时(5)
5
"""
if not self.use_solar_time or self.longitude is None:
return self.time_index
# 延迟导入避免循环依赖
from utils.solar_time import adjust_time_index_for_solar_time
# 获取出生时刻
if self.birth_hour is not None and self.birth_minute is not None:
hour = self.birth_hour
minute = self.birth_minute
else:
# 如果没有提供具体时刻,使用时辰的中点时间
hour, minute = self._get_time_index_midpoint()
# 计算真太阳时修正后的时辰
adjusted_index, _, _ = adjust_time_index_for_solar_time(hour, minute, self.longitude)
return adjusted_index
def _get_time_index_midpoint(self) -> tuple[int, int]:
"""
获取时辰的中点时间(小时,分钟)
Returns:
(小时, 分钟) 元组
Examples:
>>> info = BirthInfo(date="2000-08-16", time_index=6, gender="女")
>>> info._get_time_index_midpoint()
(12, 0) # 午时中点
"""
# 时辰中点映射(小时, 分钟)
midpoints = [
(0, 0), # 0 早子时 (23:00-01:00) -> 00:00
(2, 0), # 1 丑时 (01:00-03:00) -> 02:00
(4, 0), # 2 寅时 (03:00-05:00) -> 04:00
(6, 0), # 3 卯时 (05:00-07:00) -> 06:00
(8, 0), # 4 辰时 (07:00-09:00) -> 08:00
(10, 0), # 5 巳时 (09:00-11:00) -> 10:00
(12, 0), # 6 午时 (11:00-13:00) -> 12:00
(14, 0), # 7 未时 (13:00-15:00) -> 14:00
(16, 0), # 8 申时 (15:00-17:00) -> 16:00
(18, 0), # 9 酉时 (17:00-19:00) -> 18:00
(20, 0), # 10 戌时 (19:00-21:00) -> 20:00
(22, 0), # 11 亥时 (21:00-23:00) -> 22:00
(0, 0), # 12 晚子时 (23:00-01:00) -> 00:00(次日)
]
return midpoints[self.time_index]
def get_solar_time_info(self) -> Optional[str]:
"""
获取真太阳时信息(用于日志和调试)
Returns:
格式化的真太阳时信息字符串,如果不使用真太阳时则返回None
Examples:
>>> info = BirthInfo(
... date="2000-08-16", time_index=6, gender="女",
... longitude=116.4, use_solar_time=True,
... birth_hour=12, birth_minute=0
... )
>>> print(info.get_solar_time_info())
北京时间: 2000-08-16 12:00
出生地经度: 116.4°E
时差: -14分钟
真太阳时: 2000-08-16 11:46
修正前时辰: 午时 (序号: 6)
修正后时辰: 午时 (序号: 6)
"""
if not self.use_solar_time or self.longitude is None:
return None
# 延迟导入
from utils.solar_time import (
beijing_to_solar_time,
calculate_solar_time_offset,
)
# 获取出生时刻
if self.birth_hour is not None and self.birth_minute is not None:
hour = self.birth_hour
minute = self.birth_minute
else:
hour, minute = self._get_time_index_midpoint()
# 创建临时datetime
birth_datetime = datetime.strptime(self.date, "%Y-%m-%d")
birth_datetime = birth_datetime.replace(hour=hour, minute=minute)
# 计算真太阳时
solar_datetime = beijing_to_solar_time(birth_datetime, self.longitude)
offset = calculate_solar_time_offset(self.longitude)
adjusted_index = self.get_adjusted_time_index()
offset_str = f"+{offset}分钟" if offset > 0 else f"{offset}分钟"
return f"""北京时间: {birth_datetime.strftime('%Y-%m-%d %H:%M')}
出生地经度: {self.longitude}°E
时差: {offset_str}
真太阳时: {solar_datetime.strftime('%Y-%m-%d %H:%M')}
修正前时辰: {self.get_time_name()} (序号: {self.time_index})
修正后时辰: {self._get_time_name_by_index(adjusted_index)} (序号: {adjusted_index})"""
def _get_time_name_by_index(self, index: int) -> str:
"""根据时辰序号获取时辰名称"""
time_names = [
"早子时",
"丑时",
"寅时",
"卯时",
"辰时",
"巳时",
"午时",
"未时",
"申时",
"酉时",
"戌时",
"亥时",
"晚子时",
]
return time_names[index]