"""
Calendar conversion utilities for Chinese lunar calendar system.
"""
from datetime import date, datetime
from typing import Any
try:
from lunardate import LunarDate
LUNARDATE_AVAILABLE = True
except ImportError:
LUNARDATE_AVAILABLE = False
try:
from zhdate import ZhDate
ZHDATE_AVAILABLE = True
except ImportError:
ZHDATE_AVAILABLE = False
try:
import chinese_calendar # noqa: F401
CHINESE_CALENDAR_AVAILABLE = True
except ImportError:
CHINESE_CALENDAR_AVAILABLE = False
class CalendarConverter:
"""Converter between different calendar systems."""
def __init__(self) -> None:
"""Initialize the calendar converter."""
self.zodiac_animals = [
"Rat",
"Ox",
"Tiger",
"Rabbit",
"Dragon",
"Snake",
"Horse",
"Goat",
"Monkey",
"Rooster",
"Dog",
"Pig",
]
self.zodiac_elements = ["Wood", "Fire", "Earth", "Metal", "Water"]
# Traditional Chinese zodiac start years (Rat years)
self.rat_years = [
1900,
1912,
1924,
1936,
1948,
1960,
1972,
1984,
1996,
2008,
2020,
2032,
]
def _calculate_chinese_zodiac_year(self, year: int) -> dict[str, Any]:
"""Calculate Chinese zodiac animal and element for a year."""
# Find the closest rat year
base_year = 1900
year_diff = year - base_year
zodiac_index = year_diff % 12
# Calculate element (each animal has 2 years of same element)
element_cycle = year_diff % 10
element_index = element_cycle // 2
# Determine yin/yang
is_yang = (year_diff % 2) == 0
return {
"animal": self.zodiac_animals[zodiac_index],
"element": self.zodiac_elements[element_index],
"yin_yang": "Yang" if is_yang else "Yin",
"full_name": f"{self.zodiac_elements[element_index]} {self.zodiac_animals[zodiac_index]}",
"cycle_position": year_diff % 60,
}
def _fallback_chinese_conversion(self, solar_date: date) -> dict[str, Any]:
"""Fallback Chinese lunar conversion when libraries are unavailable."""
# Simplified lunar calendar calculation
# This is an approximation - real lunar calendar conversion is more complex
# Average lunar month is 29.53059 days
lunar_month_days = 29.53059
# Reference new year dates (approximate)
chinese_new_years = {
2020: date(2020, 1, 25),
2021: date(2021, 2, 12),
2022: date(2022, 2, 1),
2023: date(2023, 1, 22),
2024: date(2024, 2, 10),
2025: date(2025, 1, 29),
2026: date(2026, 2, 17),
}
year = solar_date.year
chinese_new_year = chinese_new_years.get(year)
if not chinese_new_year:
# Estimate based on pattern (Chinese New Year is typically between Jan 21 - Feb 20)
estimated_ny = date(year, 1, 25) if year % 4 == 0 else date(year, 2, 1)
chinese_new_year = estimated_ny
# Determine lunar year
if solar_date < chinese_new_year:
lunar_year = year - 1
# Days since previous Chinese New Year
prev_ny = chinese_new_years.get(year - 1, date(year - 1, 2, 1))
days_since_ny = (solar_date - prev_ny).days
else:
lunar_year = year
days_since_ny = (solar_date - chinese_new_year).days
# Calculate lunar month and day (approximation)
lunar_month = min(12, max(1, int(days_since_ny / lunar_month_days) + 1))
lunar_day = min(30, max(1, int(days_since_ny % lunar_month_days) + 1))
result: dict[str, Any] = {
"lunar_year": lunar_year,
"lunar_month": lunar_month,
"lunar_day": lunar_day,
"chinese_new_year": chinese_new_year.strftime("%Y-%m-%d"),
"days_since_new_year": days_since_ny,
"zodiac_info": self._calculate_chinese_zodiac_year(lunar_year),
"calculation_method": "approximation",
}
return result
async def solar_to_lunar(
self, solar_date_str: str, culture: str = "chinese"
) -> dict[str, Any]:
"""Convert solar date to Chinese lunar date."""
try:
solar_date = datetime.strptime(solar_date_str, "%Y-%m-%d").date()
return await self._solar_to_chinese_lunar(solar_date)
except Exception as e:
return {"error": f"Failed to convert solar to lunar date: {str(e)}"}
async def _solar_to_chinese_lunar(self, solar_date: date) -> dict[str, Any]:
"""Convert solar date to Chinese lunar date."""
try:
result: dict[str, Any] = {
"solar_date": solar_date.strftime("%Y-%m-%d"),
"culture": "chinese",
}
# Try using specialized libraries first
if ZHDATE_AVAILABLE:
try:
zh_date = ZhDate.from_datetime(
datetime.combine(solar_date, datetime.min.time())
)
zodiac_info = self._calculate_chinese_zodiac_year(
zh_date.lunar_year
)
result.update(
{
"lunar_year": zh_date.lunar_year,
"lunar_month": zh_date.lunar_month,
"lunar_day": zh_date.lunar_day,
"is_leap_month": zh_date.leap_month,
"zodiac_info": zodiac_info,
"lunar_date_string": f"{zh_date.lunar_year}-{zh_date.lunar_month}-{zh_date.lunar_day}",
"calculation_method": "zhdate",
}
)
return result
except Exception:
pass
if LUNARDATE_AVAILABLE:
try:
lunar_date = LunarDate.fromSolarDate(
solar_date.year, solar_date.month, solar_date.day
)
zodiac_info = self._calculate_chinese_zodiac_year(lunar_date.year)
result.update(
{
"lunar_year": lunar_date.year,
"lunar_month": lunar_date.month,
"lunar_day": lunar_date.day,
"is_leap_month": getattr(lunar_date, "isLeapMonth", False),
"zodiac_info": zodiac_info,
"lunar_date_string": f"{lunar_date.year}-{lunar_date.month}-{lunar_date.day}",
"calculation_method": "lunardate",
}
)
return result
except Exception:
pass
# Fallback to approximation
fallback_result = self._fallback_chinese_conversion(solar_date)
result.update(fallback_result)
return result
except Exception as e:
return {"error": f"Failed to convert to Chinese lunar date: {str(e)}"}
async def lunar_to_solar(
self, lunar_date_str: str, culture: str = "chinese"
) -> dict[str, Any]:
"""Convert Chinese lunar date to solar date."""
try:
return await self._chinese_lunar_to_solar(lunar_date_str)
except Exception as e:
return {"error": f"Failed to convert lunar to solar date: {str(e)}"}
async def _chinese_lunar_to_solar(self, lunar_date_str: str) -> dict[str, Any]:
"""Convert Chinese lunar date to solar date."""
try:
# Parse lunar date string (format: YYYY-MM-DD)
parts = lunar_date_str.split("-")
if len(parts) != 3:
return {"error": "Invalid lunar date format. Use YYYY-MM-DD"}
lunar_year, lunar_month, lunar_day = map(int, parts)
result = {
"lunar_date": lunar_date_str,
"culture": "chinese",
"lunar_year": lunar_year,
"lunar_month": lunar_month,
"lunar_day": lunar_day,
}
# Try using specialized libraries
if ZHDATE_AVAILABLE:
try:
zh_date = ZhDate(lunar_year, lunar_month, lunar_day)
solar_date = zh_date.to_datetime().date()
result.update(
{
"solar_date": solar_date.strftime("%Y-%m-%d"),
"solar_year": solar_date.year,
"solar_month": solar_date.month,
"solar_day": solar_date.day,
"calculation_method": "zhdate",
}
)
return result
except Exception:
pass
if LUNARDATE_AVAILABLE:
try:
lunar_date = LunarDate(lunar_year, lunar_month, lunar_day)
solar_date = lunar_date.toSolarDate()
result.update(
{
"solar_date": solar_date.strftime("%Y-%m-%d"),
"solar_year": solar_date.year,
"solar_month": solar_date.month,
"solar_day": solar_date.day,
"calculation_method": "lunardate",
}
)
return result
except Exception:
pass
# Fallback approximation
# This is a very rough estimate
estimated_solar_month = min(12, max(1, lunar_month + 1))
estimated_solar_day = min(28, lunar_day)
result.update(
{
"solar_date": f"{lunar_year}-{estimated_solar_month:02d}-{estimated_solar_day:02d}",
"solar_year": lunar_year,
"solar_month": estimated_solar_month,
"solar_day": estimated_solar_day,
"calculation_method": "approximation",
"warning": "This is a rough approximation. Actual conversion requires lunar calendar libraries.",
}
)
return result
except Exception as e:
return {"error": f"Failed to convert Chinese lunar to solar date: {str(e)}"}
async def get_zodiac_info(
self, date_str: str, culture: str = "chinese"
) -> dict[str, Any]:
"""Get Chinese zodiac information for a date."""
try:
target_date = datetime.strptime(date_str, "%Y-%m-%d").date()
return await self._get_chinese_zodiac_info(target_date)
except Exception as e:
return {"error": f"Failed to get zodiac info: {str(e)}"}
async def _get_chinese_zodiac_info(self, target_date: date) -> dict[str, Any]:
"""Get Chinese zodiac information."""
try:
# Get lunar conversion for more accurate zodiac calculation
lunar_info = await self.solar_to_lunar(
target_date.strftime("%Y-%m-%d"), "chinese"
)
year_zodiac = self._calculate_chinese_zodiac_year(target_date.year)
# Calculate daily zodiac animal (12-day cycle)
# Reference: January 1, 2000 was a Rat day
reference_date = date(2000, 1, 1) # Rat day
days_diff = (target_date - reference_date).days
daily_zodiac_index = days_diff % 12
# Calculate hourly zodiac for current hour
current_hour = datetime.now().hour
hourly_zodiac_index = ((current_hour + 1) // 2) % 12
# Chinese zodiac characteristics
zodiac_traits = {
"Rat": {
"personality": "clever, adaptable, charming",
"lucky_colors": ["blue", "gold", "green"],
"lucky_numbers": [2, 3],
},
"Ox": {
"personality": "reliable, strong, determined",
"lucky_colors": ["white", "yellow", "green"],
"lucky_numbers": [1, 9],
},
"Tiger": {
"personality": "brave, competitive, confident",
"lucky_colors": ["orange", "gray", "white"],
"lucky_numbers": [1, 3, 4],
},
"Rabbit": {
"personality": "gentle, quiet, elegant",
"lucky_colors": ["pink", "purple", "blue"],
"lucky_numbers": [3, 4, 9],
},
"Dragon": {
"personality": "energetic, intelligent, gifted",
"lucky_colors": ["gold", "silver", "gray"],
"lucky_numbers": [1, 6, 7],
},
"Snake": {
"personality": "wise, elegant, intuitive",
"lucky_colors": ["black", "red", "yellow"],
"lucky_numbers": [2, 8, 9],
},
"Horse": {
"personality": "animated, active, energetic",
"lucky_colors": ["yellow", "green", "purple"],
"lucky_numbers": [2, 3, 7],
},
"Goat": {
"personality": "calm, gentle, sympathetic",
"lucky_colors": ["green", "red", "purple"],
"lucky_numbers": [3, 9, 4],
},
"Monkey": {
"personality": "sharp, smart, curious",
"lucky_colors": ["white", "gold", "blue"],
"lucky_numbers": [1, 8, 7],
},
"Rooster": {
"personality": "observant, hardworking, courageous",
"lucky_colors": ["gold", "brown", "yellow"],
"lucky_numbers": [5, 7, 8],
},
"Dog": {
"personality": "lovely, honest, responsible",
"lucky_colors": ["green", "red", "purple"],
"lucky_numbers": [3, 4, 9],
},
"Pig": {
"personality": "honest, generous, reliable",
"lucky_colors": ["yellow", "gray", "brown"],
"lucky_numbers": [2, 5, 8],
},
}
year_animal = year_zodiac["animal"]
daily_animal = self.zodiac_animals[daily_zodiac_index]
hourly_animal = self.zodiac_animals[hourly_zodiac_index]
return {
"date": target_date.strftime("%Y-%m-%d"),
"culture": "chinese",
"year_zodiac": year_zodiac,
"daily_zodiac": {
"animal": daily_animal,
"traits": zodiac_traits.get(daily_animal, {}),
"influence": f"Today is a {daily_animal} day, bringing {zodiac_traits.get(daily_animal, {}).get('personality', 'special')} energy",
},
"hourly_zodiac": {
"animal": hourly_animal,
"hour_range": f"{(hourly_zodiac_index * 2 - 1) % 24:02d}:00-{(hourly_zodiac_index * 2 + 1) % 24:02d}:00",
"influence": f"Current hours favor {hourly_animal} characteristics",
},
"lunar_info": lunar_info,
"compatibility": {
"best_matches": self._get_zodiac_compatibility(year_animal)["best"],
"challenging_matches": self._get_zodiac_compatibility(year_animal)[
"challenging"
],
},
}
except Exception as e:
return {"error": f"Failed to get Chinese zodiac info: {str(e)}"}
def _get_zodiac_compatibility(self, animal: str) -> dict[str, list[str]]:
"""Get Chinese zodiac compatibility."""
compatibility_map = {
"Rat": {
"best": ["Dragon", "Monkey", "Ox"],
"challenging": ["Horse", "Goat"],
},
"Ox": {
"best": ["Snake", "Rooster", "Rat"],
"challenging": ["Goat", "Horse"],
},
"Tiger": {
"best": ["Horse", "Dog", "Pig"],
"challenging": ["Monkey", "Snake"],
},
"Rabbit": {
"best": ["Goat", "Pig", "Dog"],
"challenging": ["Rooster", "Dragon"],
},
"Dragon": {
"best": ["Rat", "Monkey", "Rooster"],
"challenging": ["Dog", "Rabbit"],
},
"Snake": {
"best": ["Ox", "Rooster", "Monkey"],
"challenging": ["Pig", "Tiger"],
},
"Horse": {"best": ["Tiger", "Dog", "Goat"], "challenging": ["Rat", "Ox"]},
"Goat": {"best": ["Rabbit", "Pig", "Horse"], "challenging": ["Ox", "Rat"]},
"Monkey": {
"best": ["Rat", "Dragon", "Snake"],
"challenging": ["Tiger", "Pig"],
},
"Rooster": {
"best": ["Ox", "Snake", "Dragon"],
"challenging": ["Rabbit", "Dog"],
},
"Dog": {
"best": ["Tiger", "Horse", "Rabbit"],
"challenging": ["Dragon", "Rooster"],
},
"Pig": {
"best": ["Rabbit", "Goat", "Tiger"],
"challenging": ["Snake", "Monkey"],
},
}
return compatibility_map.get(animal, {"best": [], "challenging": []})