"""Time and timezone utilities.""" from datetime import datetime, time, timedelta from zoneinfo import ZoneInfo from typing import Optional from bot.logging_config import get_logger logger = get_logger(__name__) class TimeService: """Service for time-related operations.""" def __init__(self, timezone: str = "Europe/Moscow"): """ Initialize TimeService. Args: timezone: Timezone name (e.g., "Europe/Moscow") """ self.timezone = ZoneInfo(timezone) logger.info(f"TimeService initialized with timezone: {timezone}") def get_now(self) -> datetime: """ Get current datetime in configured timezone (as naive). Returns: Current datetime in local timezone (Moscow), naive """ # Get aware datetime in configured timezone, then strip timezone aware_now = datetime.now(self.timezone) return aware_now.replace(tzinfo=None) def combine_date_time(self, date: datetime, time_of_day: time) -> datetime: """ Combine date and time as naive datetime. Args: date: Date component time_of_day: Time component Returns: Combined naive datetime """ return datetime.combine(date.date(), time_of_day) def calculate_first_run_time( self, time_of_day: time, from_datetime: Optional[datetime] = None, ) -> datetime: """ Calculate the nearest future occurrence of time_of_day (today or tomorrow). Used for new reminders and resume. Args: time_of_day: Desired time of day from_datetime: Base datetime (defaults to now) Returns: Next run datetime (today if time hasn't passed, otherwise tomorrow) """ if from_datetime is None: from_datetime = self.get_now() next_run = self.combine_date_time(from_datetime, time_of_day) if next_run <= from_datetime: next_run += timedelta(days=1) return next_run def calculate_next_run_with_interval( self, time_of_day: time, days_interval: int, from_datetime: Optional[datetime] = None, ) -> datetime: """ Calculate next run datetime using interval offset from now. Used when interval is changed. Args: time_of_day: Desired time of day days_interval: Days between reminders from_datetime: Base datetime (defaults to now) Returns: Datetime at time_of_day, days_interval days from from_datetime """ if from_datetime is None: from_datetime = self.get_now() target_date = from_datetime + timedelta(days=days_interval) return self.combine_date_time(target_date, time_of_day) def calculate_next_occurrence( self, current_run: datetime, days_interval: int, ) -> datetime: """ Calculate next occurrence after a completed/sent reminder. Guarantees the result is in the future. Args: current_run: Current run datetime days_interval: Days between reminders Returns: Next occurrence datetime (always in the future) """ now = self.get_now() next_run = current_run + timedelta(days=days_interval) while next_run <= now: next_run += timedelta(days=days_interval) return next_run def add_hours(self, dt: datetime, hours: int) -> datetime: """ Add hours to a datetime. Args: dt: Base datetime hours: Hours to add Returns: New datetime """ return dt + timedelta(hours=hours) def format_next_run(self, next_run: datetime) -> str: """ Format next run datetime for display. Args: next_run: Next run datetime (naive UTC) Returns: Formatted string """ now = self.get_now() # If it's today if next_run.date() == now.date(): return f"сегодня в {next_run.strftime('%H:%M')}" # If it's tomorrow tomorrow = (now + timedelta(days=1)).date() if next_run.date() == tomorrow: return f"завтра в {next_run.strftime('%H:%M')}" # Otherwise, show full date return next_run.strftime("%d.%m.%Y в %H:%M") # Global instance _time_service: Optional[TimeService] = None def get_time_service(timezone: Optional[str] = None) -> TimeService: """ Get global TimeService instance. Args: timezone: Timezone name (only used for first initialization) Returns: TimeService instance """ global _time_service if _time_service is None: if timezone is None: from bot.config import get_config timezone = get_config().timezone _time_service = TimeService(timezone) return _time_service