115 lines
4.8 KiB
Python
115 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
def _add(d: date, days: int) -> date:
|
|
return d + timedelta(days=days)
|
|
|
|
|
|
def _step(d: date, weekdays: list[int], direction: int) -> date:
|
|
selected = set(weekdays)
|
|
cursor = _add(d, direction)
|
|
for _ in range(28):
|
|
if cursor.weekday() in selected:
|
|
return cursor
|
|
cursor = _add(cursor, direction)
|
|
raise HTTPException(400, "Не удалось найти тренировочный день")
|
|
|
|
|
|
def _on_or_after(d: date, weekdays: list[int]) -> date:
|
|
if d.weekday() in set(weekdays):
|
|
return d
|
|
return _step(_add(d, -1), weekdays, 1)
|
|
|
|
|
|
def _order(week: int, day_index: int) -> int:
|
|
return week * 10 + day_index
|
|
|
|
|
|
def _next_training(after: date, weekdays: list[int], taken: set[date]) -> date:
|
|
value = _step(after, weekdays, 1)
|
|
while value in taken:
|
|
value = _step(value, weekdays, 1)
|
|
return value
|
|
|
|
|
|
def _place(desired: date, previous: date | None, weekdays: list[int], taken: set[date]) -> date:
|
|
value = desired
|
|
if previous is not None and value <= previous:
|
|
value = _next_training(previous, weekdays, taken)
|
|
while value in taken:
|
|
value = _next_training(value, weekdays, taken)
|
|
return value
|
|
|
|
|
|
def plan_move(workouts: list, moved_id: int, new_date: date, weekdays: list[int]) -> dict[int, date]:
|
|
moved = next((w for w in workouts if w.id == moved_id), None)
|
|
if moved is None:
|
|
raise HTTPException(404, "Тренировка не найдена")
|
|
if moved.status == "completed":
|
|
raise HTTPException(400, "Выполненную тренировку переносить нельзя")
|
|
if new_date < date.today():
|
|
raise HTTPException(400, "В закрытый период переносить нельзя")
|
|
|
|
sorted_items = sorted(workouts, key=lambda w: (_order(w.week_number, w.day_index), w.id))
|
|
completed = [w for w in sorted_items if w.status == "completed"]
|
|
if any(w.scheduled_date == new_date for w in completed):
|
|
raise HTTPException(400, "На эту дату уже есть выполненная тренировка")
|
|
|
|
moved_order = _order(moved.week_number, moved.day_index)
|
|
earlier = [w for w in completed if _order(w.week_number, w.day_index) < moved_order]
|
|
later = [w for w in completed if _order(w.week_number, w.day_index) > moved_order]
|
|
if earlier and new_date <= earlier[-1].scheduled_date:
|
|
raise HTTPException(400, "Дата должна быть позже уже выполненной тренировки")
|
|
if later and new_date >= later[0].scheduled_date:
|
|
raise HTTPException(400, "Дата должна быть раньше уже выполненной следующей тренировки")
|
|
|
|
taken = {w.scheduled_date for w in completed}
|
|
assigned: dict[int, date] = {}
|
|
index = 0
|
|
while index < len(sorted_items):
|
|
if sorted_items[index].status == "completed":
|
|
index += 1
|
|
continue
|
|
start = index
|
|
while index < len(sorted_items) and sorted_items[index].status != "completed":
|
|
index += 1
|
|
segment = sorted_items[start:index]
|
|
prev_anchor = sorted_items[start - 1] if start > 0 else None
|
|
next_anchor = sorted_items[index] if index < len(sorted_items) else None
|
|
proposed = sorted(new_date if item.id == moved_id else item.scheduled_date for item in segment)
|
|
dates: list[date] = []
|
|
for raw in proposed:
|
|
value = _place(raw, dates[-1] if dates else None, weekdays, taken)
|
|
if prev_anchor is not None and value <= prev_anchor.scheduled_date:
|
|
raise HTTPException(400, "Дата должна быть позже уже выполненной тренировки")
|
|
if next_anchor is not None and value >= next_anchor.scheduled_date:
|
|
raise HTTPException(400, "Дата должна быть раньше уже выполненной следующей тренировки")
|
|
dates.append(value)
|
|
taken.add(value)
|
|
for item, value in zip(segment, dates):
|
|
assigned[item.id] = value
|
|
|
|
return assigned
|
|
|
|
|
|
def plan_weekday_change(workouts: list, weekdays: list[int], start_from: date) -> dict[int, date]:
|
|
open_items = sorted(
|
|
(w for w in workouts if w.status != "completed"),
|
|
key=lambda w: _order(w.week_number, w.day_index),
|
|
)
|
|
taken = {w.scheduled_date for w in workouts if w.status == "completed"}
|
|
assigned: dict[int, date] = {}
|
|
cursor = start_from
|
|
for item in open_items:
|
|
date_value = _on_or_after(cursor, weekdays)
|
|
while date_value in taken:
|
|
date_value = _step(date_value, weekdays, 1)
|
|
assigned[item.id] = date_value
|
|
taken.add(date_value)
|
|
cursor = _add(date_value, 1)
|
|
return assigned
|