Plans


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))]
        ]
    }
}

Utils


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)

Plan


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]:
[26 Oct 2017 - Event Day,
 04 Sep 2017 - RunEasy,
 11 Sep 2017 - RunEasy,
 18 Sep 2017 - RunEasy,
 25 Sep 2017 - RunEasy,
 02 Oct 2017 - RunEasy,
 09 Oct 2017 - RunEasy,
 16 Oct 2017 - RunEasy,
 23 Oct 2017 - RunEasy,
 06 Sep 2017 - Interval,
 20 Sep 2017 - Interval,
 04 Oct 2017 - Interval,
 18 Oct 2017 - Interval,
 13 Sep 2017 - Hillsprint,
 27 Sep 2017 - Hillsprint,
 11 Oct 2017 - Hillsprint,
 25 Oct 2017 - Hillsprint,
 08 Sep 2017 - RunEasy,
 15 Sep 2017 - RunEasy,
 22 Sep 2017 - RunEasy,
 29 Sep 2017 - RunEasy,
 06 Oct 2017 - RunEasy,
 13 Oct 2017 - RunEasy,
 20 Oct 2017 - RunEasy,
 27 Oct 2017 - RunEasy]

In [ ]: