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