In [14]:
from collections import namedtuple
RuneasySettings = namedtuple(
'RuneasySettings', 'init_dur prog_freq rest race max_dur')
IntervalSettings = namedtuple(
'IntervalSettings', 'init_dur prog_freq rest race max_dur')
HillSettings = namedtuple(
'HillSettings', 'init_dur prog_freq rest race max_dur')
TempoSettings = namedtuple(
'TempoSettings', 'init_dur prog_freq rest race max_dur')
CrosstrainSettings = namedtuple(
'CrosstrainSettings', 'init_dur prog_freq rest race max_dur')
PLANS = {
"5k": {
"Beginner": [
[("runeasy", RuneasySettings(25, 3, 5, 5, 35))],
[("interval", RuneasySettings(10, 1, 5, 5, 35)),
("hillsprint", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 3, 5, 5, 35))]
],
"Intermediate": [
[("interval", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(10, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 9, 5, 10, 35))],
[("hillsprint", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(40, 3, 0, '50%', 55))]
],
"Advanced": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
]
},
"10k": {
"Beginner": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
],
"Intermediate": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
],
"Advanced": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
]
},
"half": {
"Beginner": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
],
"Intermediate": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
],
"Advanced": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
]
},
"full": {
"Beginner": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
],
"Intermediate": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
],
"Advanced": [
[("runeasy", RuneasySettings(5, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(10, 1, 5, 5, 35)),
("interval", RuneasySettings(25, 1, 5, 5, 35))],
[("runeasy", RuneasySettings(30, 1, 5, 5, 35))]
]
}
}
In [15]:
''' Utility Functions '''
import json
from datetime import timedelta
def next_weekday(now, weekday):
'''
datetime, int -> datetime
'''
days_ahead = weekday - now.weekday() + 7
return now + timedelta(days_ahead)
def week_type(week, length):
if week == (length - 1):
return 'race'
elif rest_week(week, length):
return 'rest'
else:
return 'prog'
def rest_week(week, plan_length):
'''
int -> boolean
Determine if current week is a rest week.
Plans work on a 4 week block, with every 4th week being an easier week.
Runner has at least 2 weeks, and a maximum of 5 weeks before they get an
easier week. So if they were on a 6 week plan they would only have an
easier week on race week.
Returns True if rest week and False if progression week.
'''
build_up = plan_length % 4
if week <= build_up and build_up < 3:
return False
elif (week - build_up + 1) % 4 == 0:
return True
else:
return False
def rest_duration(rest, dur):
'''
Return rest week duration based on whether rest duration reduction is applied as an absolute
or % value
'''
if isinstance(rest, str):
return (float(rest.strip('%')) / 100) * dur
else:
return dur - rest
def open_json(file_name):
try:
with open(file_name, 'r') as i:
return json.load(i)
except FileNotFoundError:
raise FileNotFoundError('Check {} exists'.format(file_name))
class WorkoutEncoder(json.JSONEncoder):
def default(self, o):
try:
to_serialize = {
'title': o.title,
'start': o.date,
'color': o.color,
'textColor': o.textColor,
}
return to_serialize
except AttributeError:
return super().default(o)
In [16]:
from datetime import date, timedelta, datetime
import types
import os
class Exercise:
def __init__(self, description, duration, intensity='Normal'):
self.description = description
self.duration = duration
self.intensity = intensity
def __repr__(self):
'''
Return a more human-readable representation
'''
return '{0}({1})'.format(self.description, self.duration)
@staticmethod
def mins_to_seconds_formatter(dur_in_mins):
return "{}s".format(int(dur_in_mins * 60))
class WorkoutSet:
def __init__(self, reps):
self.reps = reps
self.exercises = []
self._duration = 0
def __repr__(self):
'''
Return a more human-readable representation
'''
ex = ', '.join(str(exercise) for exercise in self.exercises)
return '{0}x ({1})'.format(self.reps, ex)
@property
def duration(self):
return self.reps * self._duration
def add_exercise(self, exercise):
self.exercises.append(exercise)
self._duration += exercise.duration
class Workout:
'''
Represents a workout session
'''
formatting_dict = {
'Event Day': {'color': '#001F3F',
'textColor': 'hsla(210, 100%, 75%, 1.0)'},
'RunEasy': {'color': '#2ECC40',
'textColor': 'hsla(127, 63%, 15%, 1.0)'},
'Interval': {'color': '#FF4136',
'textColor': 'hsla(3, 100%, 25%, 1.0)'},
'Hillsprint': {'color': '#FFDC00',
'textColor': 'hsla(52, 100%, 20%, 1.0)'},
'Tempo': {'color': '#0074D9',
'textColor': 'hsla(208, 100%, 85%, 1.0)'}
}
def __init__(self, date, title):
self.date = date
self.title = title
self.duration = 0
self.workoutsets = []
def __repr__(self):
'''
Return a more human-readable representation
'''
return '{0} - {1}'.format(self.date.strftime('%d %b %Y'),
self.title)
def __str__(self):
'''
Return a more human-readable representation
'''
# TODO: add if for EventDay
ws = '\n'.join('{0}x {1}'.format(workoutset.reps,
workoutset.exercises) for workoutset in self.workoutsets)
return '{0}\n{1}'.format(self.title, ws)
@property
def color(self):
return self.formatting_dict[self.title]['color']
@property
def textColor(self):
return self.formatting_dict[self.title]['textColor']
def add_workoutset(self, workoutset):
self.workoutsets.append(workoutset)
self.duration += workoutset.duration
class Progression:
def __init__(self, start_date, length, progressions):
self.start_date = start_date
self.length = length
self.sessions = []
progress_dict = {
"runeasy": self.runeasy,
"interval": self.interval,
"hillsprint": self.hillsprint
}
start = 0
step = len(progressions)
for progression in progressions:
self.sessions += [wk for wk in progress_dict[progression[0]]
(start, step, progression[1])]
start += 1
def runeasy(self, start, step, settings):
'''
'''
wk = start
dur = settings.init_dur
while wk < self.length:
if week_type(wk, self.length) == 'prog':
if (wk + 1) % settings.prog_freq == 0 and dur < settings.max_dur:
dur += 5
wk_dur = dur
elif week_type(wk, self.length) == 'rest':
wk_dur = rest_duration(settings.rest, dur)
else:
wk_dur = rest_duration(settings.race, dur)
# Build workout
date = self.start_date + timedelta(weeks=wk)
w = Workout(date, 'RunEasy')
ws = WorkoutSet(1)
e = Exercise('Easy', wk_dur)
ws.add_exercise(e)
w.add_workoutset(ws)
yield w
wk += step
def hillsprint(self, start, step, settings):
'''
'''
wk = start
dur = settings.init_dur
while wk < self.length:
if week_type(wk, self.length) == 'prog':
if (wk + 1) % settings.prog_freq == 0 and dur < settings.max_dur:
dur += 5
wk_dur = dur
elif week_type(wk, self.length) == 'rest':
wk_dur = rest_duration(settings.rest, dur)
else:
wk_dur = rest_duration(settings.race, dur)
# Build workout
date = self.start_date + timedelta(weeks=wk)
w = Workout(date, 'Hillsprint')
ws = WorkoutSet(1)
e = Exercise('Easy', wk_dur)
ws.add_exercise(e)
w.add_workoutset(ws)
yield w
wk += step
def interval(self, start, step, settings):
'''
'''
wk = start
dur = settings.init_dur
while wk < self.length:
if week_type(wk, self.length) == 'prog':
if (wk + 1) % settings.prog_freq == 0 and dur < settings.max_dur:
dur += 5
wk_dur = dur
elif week_type(wk, self.length) == 'rest':
wk_dur = rest_duration(settings.rest, dur)
else:
wk_dur = rest_duration(settings.race, dur)
# Build workout
date = self.start_date + timedelta(weeks=wk)
w = Workout(date, 'Interval')
durs = [10, wk_dur, 10]
for d in durs:
ws = WorkoutSet(1)
e = Exercise('Easy', d)
ws.add_exercise(e)
w.add_workoutset(ws)
yield w
wk += step
class Plan:
'''
Represents running training plan for prescribed event and level.
'''
def __init__(self, distance, level, start_date, event_date, event_title):
self.distance = distance
self.level = level
self.start_date = start_date
self._event_date = event_date
self.event_title = event_title
# Populate schedule with event
self.schedule = [Workout(self._event_date, 'Event Day')]
@property
def length(self):
'''
Length of the training plan in weeks
'''
return self.weeks_between_dates(self.start_date, self._event_date)
def create(self, days):
'''
Creates schedule based on ability level and training days
'''
details = PLANS[self.distance][self.level]
for day, detail in zip(days, details):
session_start = next_weekday(self.start_date, day)
p = Progression(session_start, self.length, detail)
self.schedule += p.sessions
def __repr__(self):
'''
Return a more human-readable representation
'''
return "{0} week {1} Plan for the {2}".format(self.length,
self.level,
self.event_title)
@property
def event_date(self):
'''
Event date property, formatted as a string
'''
return self._event_date.strftime('%d %b %Y')
@staticmethod
def weeks_between_dates(start_date, end_date):
'''
Return the number of weeks between two dates
'''
return int((next_weekday(end_date, 0) -
next_weekday(start_date, 0)).days / 7)
In [17]:
distance = "5k"
level = "Beginner"
start_date = date.today()
event_date = start_date + timedelta(weeks=8)
event = "EMF"
p = Plan(distance, level, start_date, event_date, event)
In [18]:
days = [0, 2, 4]
p.create(days)
In [19]:
p.schedule
Out[19]:
In [ ]: