reminder-bot/bot/services/time_service.py

182 lines
4.9 KiB
Python
Raw Normal View History

2025-12-19 11:19:54 +01:00
"""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:
"""
2025-12-19 12:20:26 +01:00
Get current datetime in configured timezone (as naive).
2025-12-19 11:19:54 +01:00
Returns:
2025-12-19 12:20:26 +01:00
Current datetime in local timezone (Moscow), naive
2025-12-19 11:19:54 +01:00
"""
2025-12-19 12:20:26 +01:00
# Get aware datetime in configured timezone, then strip timezone
aware_now = datetime.now(self.timezone)
return aware_now.replace(tzinfo=None)
2025-12-19 11:19:54 +01:00
def combine_date_time(self, date: datetime, time_of_day: time) -> datetime:
"""
2025-12-19 11:46:21 +01:00
Combine date and time as naive datetime.
2025-12-19 11:19:54 +01:00
Args:
date: Date component
time_of_day: Time component
Returns:
2025-12-19 11:46:21 +01:00
Combined naive datetime
2025-12-19 11:19:54 +01:00
"""
2025-12-19 11:46:21 +01:00
return datetime.combine(date.date(), time_of_day)
2025-12-19 11:19:54 +01:00
2026-02-17 10:52:46 +01:00
def calculate_first_run_time(
2025-12-19 11:19:54 +01:00
self,
time_of_day: time,
from_datetime: Optional[datetime] = None,
) -> datetime:
"""
2026-02-17 10:52:46 +01:00
Calculate the nearest future occurrence of time_of_day (today or tomorrow).
Used for new reminders and resume.
2025-12-19 11:19:54 +01:00
Args:
time_of_day: Desired time of day
from_datetime: Base datetime (defaults to now)
Returns:
2026-02-17 10:52:46 +01:00
Next run datetime (today if time hasn't passed, otherwise tomorrow)
2025-12-19 11:19:54 +01:00
"""
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
2026-02-17 10:52:46 +01:00
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)
2025-12-19 11:19:54 +01:00
def calculate_next_occurrence(
self,
current_run: datetime,
days_interval: int,
) -> datetime:
"""
2026-02-17 10:52:46 +01:00
Calculate next occurrence after a completed/sent reminder.
Guarantees the result is in the future.
2025-12-19 11:19:54 +01:00
Args:
current_run: Current run datetime
days_interval: Days between reminders
Returns:
2026-02-17 10:52:46 +01:00
Next occurrence datetime (always in the future)
2025-12-19 11:19:54 +01:00
"""
2026-02-17 10:52:46 +01:00
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
2025-12-19 11:19:54 +01:00
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:
2025-12-19 11:46:21 +01:00
next_run: Next run datetime (naive UTC)
2025-12-19 11:19:54 +01:00
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