154 lines
3.9 KiB
Python
154 lines
3.9 KiB
Python
"""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.
|
|
|
|
Returns:
|
|
Current datetime
|
|
"""
|
|
return datetime.now(self.timezone)
|
|
|
|
def combine_date_time(self, date: datetime, time_of_day: time) -> datetime:
|
|
"""
|
|
Combine date and time in configured timezone.
|
|
|
|
Args:
|
|
date: Date component
|
|
time_of_day: Time component
|
|
|
|
Returns:
|
|
Combined datetime
|
|
"""
|
|
naive_dt = datetime.combine(date.date(), time_of_day)
|
|
return naive_dt.replace(tzinfo=self.timezone)
|
|
|
|
def calculate_next_run_time(
|
|
self,
|
|
time_of_day: time,
|
|
days_interval: int,
|
|
from_datetime: Optional[datetime] = None,
|
|
) -> datetime:
|
|
"""
|
|
Calculate next run datetime for a reminder.
|
|
|
|
Args:
|
|
time_of_day: Desired time of day
|
|
days_interval: Days between reminders
|
|
from_datetime: Base datetime (defaults to now)
|
|
|
|
Returns:
|
|
Next run datetime
|
|
"""
|
|
if from_datetime is None:
|
|
from_datetime = self.get_now()
|
|
|
|
# Start with today at the specified time
|
|
next_run = self.combine_date_time(from_datetime, time_of_day)
|
|
|
|
# If the time has already passed today, start from tomorrow
|
|
if next_run <= from_datetime:
|
|
next_run += timedelta(days=1)
|
|
|
|
return next_run
|
|
|
|
def calculate_next_occurrence(
|
|
self,
|
|
current_run: datetime,
|
|
days_interval: int,
|
|
) -> datetime:
|
|
"""
|
|
Calculate next occurrence after a completed reminder.
|
|
|
|
Args:
|
|
current_run: Current run datetime
|
|
days_interval: Days between reminders
|
|
|
|
Returns:
|
|
Next occurrence datetime
|
|
"""
|
|
return current_run + timedelta(days=days_interval)
|
|
|
|
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
|
|
|
|
Returns:
|
|
Formatted string
|
|
"""
|
|
now = self.get_now()
|
|
delta = next_run - 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
|