Spaces:
Running
Running
File size: 3,829 Bytes
557ee65 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | from typing import List, Optional, Literal
from datetime import date, timedelta, datetime
import uuid
from domain.training.run import Run
from domain.training.planned_session import PlannedSession
from domain.runner.goal import Goal
from domain.runner.profile import RunnerProfile
def match_run_to_session(run: Run, sessions: List[PlannedSession]) -> Optional[uuid.UUID]:
"""
Matches a run to a planned session based on date and distance.
Rules:
- abs(run_date - planned_date) <= 1 day
- run_distance >= 0.8 * target_distance
- Match only one session (first match wins)
"""
run_date = run.start_time.date()
run_dist_km = run.total_distance_m / 1000.0
for session in sessions:
if session.completed_run_id is not None:
continue
date_diff = abs((run_date - session.planned_date).days)
if date_diff <= 1 and run_dist_km >= (0.8 * session.target_distance_km):
return session.id
return None
def classify_week(sessions: List[PlannedSession], current_volume: float, goal_volume: float) -> str:
"""
Classifies the week's structure based on completed sessions.
Returns canonical string: strong_week, structured_but_light, rebuild_week, reset_week.
"""
if not sessions:
return "reset_week"
long_run_completed = any(s.session_type == "long_run" and s.completed_run_id for s in sessions)
weekday_sessions = [s for s in sessions if s.session_type == "weekday"]
weekdays_completed = sum(1 for s in weekday_sessions if s.completed_run_id)
# Heuristics
if long_run_completed:
if weekdays_completed >= 2 or current_volume >= goal_volume:
return "strong_week"
else:
return "structured_but_light"
else:
if weekdays_completed >= 2 or (current_volume > 0 and current_volume >= 0.5 * goal_volume):
return "rebuild_week"
else:
return "reset_week"
def generate_week_template(
runner_id: uuid.UUID,
week_start: date,
goal: Optional[Goal],
profile: Optional[RunnerProfile],
previous_sessions: List[PlannedSession] = None,
) -> List[PlannedSession]:
"""
Generates a week template (list of PlannedSession).
Logic:
- Default 3 weekday + 1 long run
- Long run distance based on goal (e.g. 30–40% weekly volume)
- Weekday runs evenly distributed
- Respect baseline weekly distance
"""
# 1. Determine Target Weekly Volume
baseline = profile.baseline_weekly_km if (profile and profile.baseline_weekly_km is not None) else 20.0
target_volume = baseline
if goal and goal.type == "volume" and goal.target_value is not None:
target_volume = goal.target_value
# Simple growth logic if previous week was successful (optional/not requested but good to have)
# For now, stay simple as per requirements.
# 2. Distribute Volume
long_run_dist = target_volume * 0.35
weekday_total = target_volume - long_run_dist
weekday_count = 3
weekday_dist = weekday_total / weekday_count
# 3. Assign Dates
# Mon (0), Tue (1), Wed (2), Thu (3), Fri (4), Sat (5), Sun (6)
# We'll use Tue, Thu, Fri for weekdays and Sun for long run
template_dates = {
1: ("weekday", weekday_dist),
3: ("weekday", weekday_dist),
4: ("weekday", weekday_dist),
6: ("long_run", long_run_dist),
}
sessions = []
for day_offset, (s_type, dist) in template_dates.items():
sessions.append(
PlannedSession(
runner_id=runner_id,
week_start_date=week_start,
session_type=s_type,
planned_date=week_start + timedelta(days=day_offset),
target_distance_km=round(dist, 1),
)
)
return sessions
|