268 lines
9.0 KiB
Python
268 lines
9.0 KiB
Python
"""Stopwatch module."""
|
||
|
||
import json
|
||
import re
|
||
from datetime import datetime
|
||
|
||
from ..core.config import BASE_DIR
|
||
|
||
STOPWATCH_FILE = BASE_DIR / "data" / "stopwatches.json"
|
||
|
||
|
||
# Optional ordinal formatting for list numbering.
|
||
try:
|
||
from num2words import num2words
|
||
except Exception:
|
||
num2words = None
|
||
|
||
|
||
def _format_ordinal_index(index: int) -> str:
|
||
if num2words is None:
|
||
return f"{index}-й"
|
||
try:
|
||
return num2words(index, lang="ru", to="ordinal", case="nominative", gender="m")
|
||
except Exception:
|
||
return f"{index}-й"
|
||
|
||
|
||
def _format_duration(seconds: float) -> str:
|
||
total = int(round(max(0, seconds)))
|
||
hours = total // 3600
|
||
minutes = (total % 3600) // 60
|
||
sec = total % 60
|
||
|
||
parts = []
|
||
if hours:
|
||
parts.append(f"{hours} ч")
|
||
if minutes:
|
||
parts.append(f"{minutes} мин")
|
||
parts.append(f"{sec} сек")
|
||
return " ".join(parts)
|
||
|
||
|
||
class StopwatchManager:
|
||
def __init__(self):
|
||
self.stopwatches = []
|
||
self.load_stopwatches()
|
||
|
||
def load_stopwatches(self):
|
||
if not STOPWATCH_FILE.exists():
|
||
return
|
||
try:
|
||
with open(STOPWATCH_FILE, "r", encoding="utf-8") as f:
|
||
raw = json.load(f)
|
||
except Exception as e:
|
||
print(f"❌ Ошибка загрузки секундомеров: {e}")
|
||
return
|
||
|
||
items = []
|
||
for item in raw:
|
||
try:
|
||
stopwatch_id = int(item["id"])
|
||
except Exception:
|
||
continue
|
||
items.append(
|
||
{
|
||
"id": stopwatch_id,
|
||
"name": str(item.get("name", "")).strip(),
|
||
"elapsed": float(item.get("elapsed", 0)),
|
||
"running": bool(item.get("running", False)),
|
||
"started_at": item.get("started_at"),
|
||
}
|
||
)
|
||
self.stopwatches = sorted(items, key=lambda x: x["id"])
|
||
|
||
def save_stopwatches(self):
|
||
payload = [
|
||
{
|
||
"id": sw["id"],
|
||
"name": sw.get("name", ""),
|
||
"elapsed": sw.get("elapsed", 0),
|
||
"running": sw.get("running", False),
|
||
"started_at": sw.get("started_at"),
|
||
}
|
||
for sw in self.stopwatches
|
||
]
|
||
try:
|
||
with open(STOPWATCH_FILE, "w", encoding="utf-8") as f:
|
||
json.dump(payload, f, indent=4)
|
||
except Exception as e:
|
||
print(f"❌ Ошибка сохранения секундомеров: {e}")
|
||
|
||
def _next_id(self) -> int:
|
||
if not self.stopwatches:
|
||
return 1
|
||
return max(sw["id"] for sw in self.stopwatches) + 1
|
||
|
||
def _now_iso(self) -> str:
|
||
return datetime.now().isoformat()
|
||
|
||
def _elapsed_now(self, stopwatch: dict) -> float:
|
||
elapsed = float(stopwatch.get("elapsed", 0))
|
||
if not stopwatch.get("running"):
|
||
return elapsed
|
||
|
||
started_at = stopwatch.get("started_at")
|
||
if not started_at:
|
||
return elapsed
|
||
|
||
try:
|
||
started_dt = datetime.fromisoformat(started_at)
|
||
except Exception:
|
||
return elapsed
|
||
|
||
delta = (datetime.now() - started_dt).total_seconds()
|
||
return elapsed + max(0, delta)
|
||
|
||
def _running(self):
|
||
return [sw for sw in self.stopwatches if sw.get("running")]
|
||
|
||
def _paused(self):
|
||
return [sw for sw in self.stopwatches if not sw.get("running")]
|
||
|
||
def has_running_stopwatches(self) -> bool:
|
||
return bool(self._running())
|
||
|
||
def describe_active_stopwatches(self) -> str:
|
||
running = self._running()
|
||
if not running:
|
||
return "Активных секундомеров нет."
|
||
|
||
running.sort(key=lambda sw: sw["id"])
|
||
items = []
|
||
for idx, sw in enumerate(running, start=1):
|
||
ordinal = _format_ordinal_index(idx)
|
||
duration = _format_duration(self._elapsed_now(sw))
|
||
name = sw.get("name", "")
|
||
if name:
|
||
items.append(f"{ordinal}) {name} — {duration}")
|
||
else:
|
||
items.append(f"{ordinal}) {duration}")
|
||
return "Активные секундомеры: " + "; ".join(items) + "."
|
||
|
||
def start_stopwatch(self, name: str = "") -> str:
|
||
stopwatch = {
|
||
"id": self._next_id(),
|
||
"name": name.strip(),
|
||
"elapsed": 0.0,
|
||
"running": True,
|
||
"started_at": self._now_iso(),
|
||
}
|
||
self.stopwatches.append(stopwatch)
|
||
self.save_stopwatches()
|
||
if stopwatch["name"]:
|
||
return f"Запустил секундомер «{stopwatch['name']}»."
|
||
return "Запустил секундомер."
|
||
|
||
def pause_stopwatches(self) -> str:
|
||
running = self._running()
|
||
if not running:
|
||
return "Сейчас нет активных секундомеров."
|
||
|
||
elapsed_items = []
|
||
for sw in running:
|
||
elapsed_now = self._elapsed_now(sw)
|
||
elapsed_items.append(
|
||
{
|
||
"id": sw["id"],
|
||
"name": sw.get("name", ""),
|
||
"elapsed": elapsed_now,
|
||
}
|
||
)
|
||
sw["elapsed"] = elapsed_now
|
||
sw["running"] = False
|
||
sw["started_at"] = None
|
||
|
||
self.save_stopwatches()
|
||
count = len(running)
|
||
if count == 1:
|
||
elapsed_text = _format_duration(elapsed_items[0]["elapsed"])
|
||
return f"Остановил секундомер. Он работал {elapsed_text}."
|
||
|
||
details = []
|
||
for idx, item in enumerate(sorted(elapsed_items, key=lambda x: x["id"]), start=1):
|
||
ordinal = _format_ordinal_index(idx)
|
||
elapsed_text = _format_duration(item["elapsed"])
|
||
name = item.get("name", "")
|
||
if name:
|
||
details.append(f"{ordinal} «{name}» — {elapsed_text}")
|
||
else:
|
||
details.append(f"{ordinal} — {elapsed_text}")
|
||
return f"Остановил секундомеры: {count} шт. Время: " + "; ".join(details) + "."
|
||
|
||
def resume_stopwatches(self) -> str:
|
||
paused = self._paused()
|
||
if not paused:
|
||
return "Пауза не активна: секундомеры уже запущены или отсутствуют."
|
||
|
||
for sw in paused:
|
||
sw["running"] = True
|
||
sw["started_at"] = self._now_iso()
|
||
|
||
self.save_stopwatches()
|
||
count = len(paused)
|
||
if count == 1:
|
||
return "Продолжил секундомер."
|
||
return f"Продолжил секундомеры: {count} шт."
|
||
|
||
def reset_stopwatches(self) -> str:
|
||
if not self.stopwatches:
|
||
return "Секундомеров для сброса нет."
|
||
|
||
count = len(self.stopwatches)
|
||
self.stopwatches = []
|
||
self.save_stopwatches()
|
||
if count == 1:
|
||
return "Секундомер сброшен."
|
||
return f"Сбросил секундомеры: {count} шт."
|
||
|
||
def parse_command(self, text: str) -> str | None:
|
||
text = text.lower().strip()
|
||
|
||
has_stopwatch_word = any(
|
||
word in text
|
||
for word in [
|
||
"секундомер",
|
||
"секундомеры",
|
||
"секундомером",
|
||
"секундомера",
|
||
"секундомеру",
|
||
]
|
||
)
|
||
if not has_stopwatch_word:
|
||
return None
|
||
|
||
if re.search(r"(какие|какой|список|активн|покажи|сколько|есть ли)", text):
|
||
return self.describe_active_stopwatches()
|
||
|
||
if any(word in text for word in ["сброс", "удали", "отмени", "очист"]):
|
||
return self.reset_stopwatches()
|
||
|
||
if any(word in text for word in ["продолж", "возобнов"]):
|
||
return self.resume_stopwatches()
|
||
|
||
if any(word in text for word in ["стоп", "останов", "пауза"]):
|
||
return self.pause_stopwatches()
|
||
|
||
if "постав" in text or "установ" in text:
|
||
return self.start_stopwatch()
|
||
|
||
if any(word in text for word in ["запусти", "включи", "старт", "начни"]):
|
||
return self.start_stopwatch()
|
||
|
||
# Если пользователь просто сказал "секундомер", трактуем как запуск.
|
||
if text in {"секундомер", "запусти секундомер", "включи секундомер"}:
|
||
return self.start_stopwatch()
|
||
|
||
return "Я понял команду про секундомер, но не распознал действие. Скажите: запусти, стоп, продолжи, сбрось или покажи активные секундомеры."
|
||
|
||
|
||
_stopwatch_manager = None
|
||
|
||
|
||
def get_stopwatch_manager():
|
||
global _stopwatch_manager
|
||
if _stopwatch_manager is None:
|
||
_stopwatch_manager = StopwatchManager()
|
||
return _stopwatch_manager
|