Files
benchpress/backend/app/services/reschedule.py
T
2026-10-11 14:12:10 +03:00

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