Weekly plan

master
mgifos 8 years ago
parent bd4125a2b8
commit 916951f328
  1. 6
      README
  2. 24
      README.md
  3. 1
      build.sbt
  4. 23
      src/main/scala/com.github.mgifos.workouts/Main.scala
  5. 1
      src/main/scala/com.github.mgifos.workouts/model/Duration.scala
  6. 6
      src/main/scala/com.github.mgifos.workouts/model/Week.scala
  7. 50
      src/main/scala/com.github.mgifos.workouts/model/WeeklyPlan.scala
  8. 31
      src/main/scala/com.github.mgifos.workouts/model/Workout.scala
  9. 91
      src/test/resources/ultra-80k-runnersworld.csv
  10. 28
      src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala

@ -1,6 +0,0 @@
Workout manager
=================================
Defines and schedules workouts

@ -0,0 +1,24 @@
# Workouts
The objectives of this project are to:
- enable easier notation for workouts (textual form)
- be able to specify weekly based training plans in e.g. spreadsheets that can be exported to CSV
- be able to import workouts from CSV file to Garmin Connect workout list
- be able to schedule imported workouts in Garmin Connect calendar automaticaly based on start/end date
An example of a workout definition notation:
```sh
workout: run-fast
- warmup: 10:00 @ z2
- repeat: 3
- run: 1.5km @ 5:10-4:40
- recover: 500m @ z2
- cooldown: 05:00
```
In CSV 1st row is reserved for heading, can be anything and the 1st column is reserved for a week number. The cells represent days and they are populated with workouts (definitions and references), so there's a limitation: a single work out per day/cell, for now. In case of the references, a workout needs to be defined first in one of the previous cells and then it can be referenced by name in the following cells.
An example of 2-weeks training plan, containing 2 workout definitions, 4 references and 6 training days in total:
| Week | Mon | Tue | Wed | Thu | Fri | Sat | Sun |
| ----:| --- | --- | --- | --- | --- | --- | --- |
| 1 | ``workout: run-fast``<br>``- warmup: 10:00 @ z2``<br>``- repeat: 3``<br>&nbsp;&nbsp;``- run: 1.5km @ 5:10-4:40``<br>&nbsp;&nbsp;``- recover: 500m @ z2``<br>``- cooldown: 05:00``|rest|rest|run-fast|rest|rest|rest|
| 2 | run-fast| ``workout: long-15`` <br> ``- run: 15 km @ z2``|rest|run-fast|rest|rest|long-15|

@ -5,5 +5,6 @@ version := "0.1"
scalaVersion := "2.12.4"
libraryDependencies ++= Seq(
"com.github.tototoshi" %% "scala-csv" % "1.3.5",
"org.scalatest" %% "scalatest" % "3.0.5" % "test"
)

@ -0,0 +1,23 @@
package com.github.mgifos.workouts
import java.nio.file.{ Files, Paths }
import java.time.LocalDate
import com.github.mgifos.workouts.model.WeeklyPlan
object Main extends App {
val csvBytes = Files.readAllBytes(Paths.get("src/test/resources/ultra-80k-runnersworld.csv"))
val wp = new WeeklyPlan(csvBytes)
println(wp.workouts)
val x = LocalDate.now()
val it = wp.get.zipWithIndex.map {
case (maybeWorkout, i) => x.plusDays(i) -> maybeWorkout
}
it.zipWithIndex.map { case (maybeScheduledWorkout, i) => s"day $i: $maybeScheduledWorkout" }.foreach(println)
}

@ -10,6 +10,7 @@ object DistanceUnits extends Enumeration {
type DistanceUnit = Value
val km, mi, m = Value
}
case class TimeDuration(hours: Int = 0, minutes: Int = 0, seconds: Int = 0) extends Duration
object Duration {

@ -0,0 +1,6 @@
package com.github.mgifos.workouts.model
object Week extends Enumeration {
type Week = Value
val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
}

@ -0,0 +1,50 @@
package com.github.mgifos.workouts.model
import com.github.tototoshi.csv.CSVReader
import scala.io.Source
class WeeklyPlan(csv: Array[Byte]) {
type Week = List[String]
private lazy val processed: Seq[Option[Workout]] = {
def weekPlan(week: Week, acc: Seq[Option[Workout]]): Seq[Option[Workout]] = Seq.tabulate(7) { weekDayNo =>
val maybeDayText = week.lift(weekDayNo + 1).flatMap(text => Option(text.trim).filter(_.nonEmpty))
maybeDayText.map { dayText =>
Workout.parseDef(dayText) match {
case Right(definition) => definition
case Left(_) => onlyDefs(acc).find(_.name == dayText).map(_.toRef).getOrElse(WorkoutNote(dayText))
}
}
}
def loop(weeks: List[Week], acc: Seq[Option[Workout]]): Seq[Option[Workout]] = weeks match {
case Nil => acc
case week :: rest => loop(rest, acc ++ weekPlan(week, acc))
}
loop(CSVReader.open(Source.fromBytes(csv)).all.filter(isAValidWeek), Seq())
}
/**
* @return all workout definitions defined in this plan
*/
def workouts: Seq[WorkoutDef] = onlyDefs(processed)
/**
* @return optional workout refs & notes of this plan per day
*/
def get: Seq[Option[Workout]] = processed.map {
case Some(x: WorkoutDef) => Some(x.toRef)
case x => x
}
private def isAValidWeek(w: Seq[String]) = w.headOption.exists(no => no.trim.nonEmpty && no.forall(_.isDigit))
private def onlyDefs(days: Seq[Option[Workout]]) = days.flatMap {
case Some(wdef: WorkoutDef) => Some(wdef)
case _ => None
}
}

@ -1,26 +1,37 @@
package com.github.mgifos.workouts.model
case class Workout(name: String, steps: Seq[Step] = Nil) {
def withStep(step: Step): Workout = Workout(name, steps :+ step)
trait Workout
case class WorkoutDef(name: String, steps: Seq[Step] = Nil) extends Workout {
def toRef: WorkoutRef = WorkoutRef(name)
def withStep(step: Step): WorkoutDef = WorkoutDef(name, steps :+ step)
}
case class WorkoutRef(name: String) extends Workout
case class WorkoutNote(note: String) extends Workout
object Workout {
private val WorkoutName = """^workout:\s([\w-]+)((\n\s*-\s[a-z]+:.*)*)$""".r
private val WorkoutName = """^workout:\s([\w \-,;:\.@]+)((\n\s*\-\s[a-z]+:.*)*)$""".r
private val NextStepRx = """^((-\s\w*:\s.*)((\n\s{1,}-\s.*)*))(([\s].*)*)$""".r
def parse(x: String) = {
def loop(w: Workout, steps: String): Workout = steps match {
def parseDef(x: String): Either[String, WorkoutDef] = {
def loop(w: WorkoutDef, steps: String): Either[String, WorkoutDef] = steps match {
case NextStepRx(next, _, _, _, rest, _) =>
val newWorkout = w.withStep(Step.parse(next.trim))
if (rest.trim.isEmpty) newWorkout
if (rest.trim.isEmpty) Right(newWorkout)
else loop(newWorkout, rest.trim)
case _ => throw new IllegalArgumentException(s"Input string cannot be parsed to Workout: $steps")
case _ => Left(s"Input string cannot be parsed to Workout: $steps")
}
x match {
case WorkoutName(name, steps, _) =>
loop(Workout(name), steps.trim)
case _ => throw new IllegalArgumentException(s"Input string cannot be parsed to Workout: $x")
case WorkoutName(name, steps, _) => loop(WorkoutDef(name), steps.trim)
case _ => Left(s"Input string cannot be parsed to Workout: $x")
}
}
def parseRef(x: String): WorkoutRef = x match {
case WorkoutName(name, _, _) => WorkoutRef(name)
case _ => WorkoutRef(x.trim)
}
}

@ -0,0 +1,91 @@
WEEK,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday,Estimated km,,Duration,Avg km,Miles
1,,"workout: 14k, 4x 1.6k @TMP
- warmup: 2km @z2
- repeat: 4
- run: 1600m @ 5:00-4:30
- recover: 900m @z2
- cooldown: 2km","workout: 8k jog
- run: 8km @z2","workout: 11-15k, middle 5k @MP
- warmup: 4km @z2
- run: 5km @ 5:40-5:30
- cooldown: 6km @z2",,"workout: 1.5h run
- run: 90:00","workout: 3h run
- run: 180:00",78.0,,0:10:00,1.7,1.0
2,,"14k, 4x 1.6k @TMP",8k jog,"11-15k, middle 5k @MP",,1.5h run,3h run,78.0,,0:15:00,2.5,1.6
3,,"workout: 14k, 2x1.6k @HMP
- warmup: 4km @z2
- repeat: 2
- run: 1.6km @ 5:05-4:50
- recover: 1.4km @z2
- cooldown: 4km @z2",8k jog,"11-15k, middle 5k @MP",,"workout: 2h run
- run 120:00","workout: 3.5h run
- run: 210:00",90.0,,0:30:00,5.0,3.1
4,,"workout: 10k, 3x1.6k @TMP
- warmup: 1500m @z2
- repeat: 3
- run: 1600m @ 5:00-4:30
- recover: 900m @z2
- cooldown: 1km",8k jog,"workout: 10k, middle 3.2k @MP
- warmup: 4km @z2
- run: 3200m @ 5:40-5:30
- cooldown: 2800m @z2",,1.5h run,2h run,63.0,,0:40:00,6.7,4.1
5,,"workout: 15k, 6x1.6k @TMP
- warmup: 2km @z2
- repeat: 6
- run: 1600m @ 5:00-4:30
- recover: 400m @z2
- cooldown: 1km",8k jog,"workout: 15k, middle 5k @MP
- warmup: 5km @z2
- run: 5km @ 5:40-5:30
- cooldown: 5km @z2",,"workout: 3.5-4h run
- run 225:00",3h run,104.7,,0:45:00,7.5,4.7
6,,"15k, 6x1.6k @TMP",8k jog,"15k, middle 5k @MP",,3.5-4h run,3h run,104.7,,0:50:00,8.3,5.2
7,,"workout: 15k, 6x1.6k @HMP
- warmup: 2km @z2
- repeat: 6
- run: 1600m @ 5:05-4:50
- recover: 400m @z2
- cooldown: 1km",8k jog,"15k, middle 5k @MP",,3.5-4h run,"workout: 3h run, last @MP
- run: 120:00
- run: 60:00 @ 5:40-5:30",104.7,,1:00:00,10.0,6.2
8,,"workout: 15k, 3x3.2k @HMP
- warmup: 2km @z2
- repeat: 3
- run: 3200m @ 5:05-4:50
- recover: 800m @z2
- cooldown: 1km",8k jog,"15k, middle 5k @MP",,2h run,"workout: 2.5h run
- run: 150:00",83.0,,1:20:00,13.3,8.3
9,,"15k, 6x1.6k @TMP",8k jog,"15k, middle 5k @MP",,"workout: 4h run
- run: 240:00","workout: 3.5h run, last @MP
- run: 150:00
- run: 60:00 @ 5:40-5:30",113.0,,1:30:00,15.0,9.3
10,,"15k, 6x1.6k @TMP",8k jog,"15k, middle 5k @MP",,4-hour run,"3.5h run, last @MP",113.0,,1:45:00,17.5,10.9
11,,"15k, 3x3.2k @HMP",8k jog,"15k, middle 5k @MP",,2.5h run,3h run,93.0,,2:00:00,20.0,12.4
12,,"15k, 6x1.6k @TMP",8k jog,"15k, middle 5k @MP",,4h run,"workout: 5h run
- run: 300:00",128.0,,2:30:00,25.0,15.5
13,,"15k, 6x1.6k @TMP",8k jog,"15k, middle 5k @MP",,4h run,5h run,128.0,,3:00:00,30.0,18.6
14,,"workout: 15k, 4x1.6k @TMP
- warmup: 3km @z2
- repeat: 2
- run: 1600m @ 5:00-4:30
- recover: 1400m @z2
- cooldown: 1km",8k jog,"15k, middle 5k @MP",,2h run,2h run,78.0,,3:30:00,35.0,21.7
15,,"workout: 11k, 3x1.6k @MP
- warmup: 1.5km @z2
- repeat: 3
- run: 1600m @ 5:40-5:30
- recover: 1400m @z2
- cooldown: 500m",8k jog,"workout: 11k, middle 5k @MP
- warmup: 3km @z2
- run: 5km @ 5:40-5:30
- cooldown: 3km @z2",,1.5h run,"workout: easy 1h jog
- run: 60:00",55.0,,4:00:00,40.0,24.8
16,,"workout: 10k, middle 5k @HMP
- warmup: 3km @z2
- run: 5km @ 5:05-4:30
- cooldown: 2km @z2",8k jog,"workout: easy 5k jog
- run: 5km",,80k race,Rest. (Duh.),103.0,,5:00:00,50.0,31.1
,,,,,,,,"1,517.0",,,,
,TMP,16k pace,,,,,,,,,,
,HMP,Half Marathon Pace,,,,,Avg min/km,0:06:00,,,,
,MP,Marathon Pace,,,,,,,,,,
1 WEEK Monday Tuesday Wednesday Thursday Friday Saturday Sunday Estimated km Duration Avg km Miles
2 1 workout: 14k, 4x 1.6k @TMP - warmup: 2km @z2 - repeat: 4 - run: 1600m @ 5:00-4:30 - recover: 900m @z2 - cooldown: 2km workout: 8k jog - run: 8km @z2 workout: 11-15k, middle 5k @MP - warmup: 4km @z2 - run: 5km @ 5:40-5:30 - cooldown: 6km @z2 workout: 1.5h run - run: 90:00 workout: 3h run - run: 180:00 78.0 0:10:00 1.7 1.0
3 2 14k, 4x 1.6k @TMP 8k jog 11-15k, middle 5k @MP 1.5h run 3h run 78.0 0:15:00 2.5 1.6
4 3 workout: 14k, 2x1.6k @HMP - warmup: 4km @z2 - repeat: 2 - run: 1.6km @ 5:05-4:50 - recover: 1.4km @z2 - cooldown: 4km @z2 8k jog 11-15k, middle 5k @MP workout: 2h run - run 120:00 workout: 3.5h run - run: 210:00 90.0 0:30:00 5.0 3.1
5 4 workout: 10k, 3x1.6k @TMP - warmup: 1500m @z2 - repeat: 3 - run: 1600m @ 5:00-4:30 - recover: 900m @z2 - cooldown: 1km 8k jog workout: 10k, middle 3.2k @MP - warmup: 4km @z2 - run: 3200m @ 5:40-5:30 - cooldown: 2800m @z2 1.5h run 2h run 63.0 0:40:00 6.7 4.1
6 5 workout: 15k, 6x1.6k @TMP - warmup: 2km @z2 - repeat: 6 - run: 1600m @ 5:00-4:30 - recover: 400m @z2 - cooldown: 1km 8k jog workout: 15k, middle 5k @MP - warmup: 5km @z2 - run: 5km @ 5:40-5:30 - cooldown: 5km @z2 workout: 3.5-4h run - run 225:00 3h run 104.7 0:45:00 7.5 4.7
7 6 15k, 6x1.6k @TMP 8k jog 15k, middle 5k @MP 3.5-4h run 3h run 104.7 0:50:00 8.3 5.2
8 7 workout: 15k, 6x1.6k @HMP - warmup: 2km @z2 - repeat: 6 - run: 1600m @ 5:05-4:50 - recover: 400m @z2 - cooldown: 1km 8k jog 15k, middle 5k @MP 3.5-4h run workout: 3h run, last @MP - run: 120:00 - run: 60:00 @ 5:40-5:30 104.7 1:00:00 10.0 6.2
9 8 workout: 15k, 3x3.2k @HMP - warmup: 2km @z2 - repeat: 3 - run: 3200m @ 5:05-4:50 - recover: 800m @z2 - cooldown: 1km 8k jog 15k, middle 5k @MP 2h run workout: 2.5h run - run: 150:00 83.0 1:20:00 13.3 8.3
10 9 15k, 6x1.6k @TMP 8k jog 15k, middle 5k @MP workout: 4h run - run: 240:00 workout: 3.5h run, last @MP - run: 150:00 - run: 60:00 @ 5:40-5:30 113.0 1:30:00 15.0 9.3
11 10 15k, 6x1.6k @TMP 8k jog 15k, middle 5k @MP 4-hour run 3.5h run, last @MP 113.0 1:45:00 17.5 10.9
12 11 15k, 3x3.2k @HMP 8k jog 15k, middle 5k @MP 2.5h run 3h run 93.0 2:00:00 20.0 12.4
13 12 15k, 6x1.6k @TMP 8k jog 15k, middle 5k @MP 4h run workout: 5h run - run: 300:00 128.0 2:30:00 25.0 15.5
14 13 15k, 6x1.6k @TMP 8k jog 15k, middle 5k @MP 4h run 5h run 128.0 3:00:00 30.0 18.6
15 14 workout: 15k, 4x1.6k @TMP - warmup: 3km @z2 - repeat: 2 - run: 1600m @ 5:00-4:30 - recover: 1400m @z2 - cooldown: 1km 8k jog 15k, middle 5k @MP 2h run 2h run 78.0 3:30:00 35.0 21.7
16 15 workout: 11k, 3x1.6k @MP - warmup: 1.5km @z2 - repeat: 3 - run: 1600m @ 5:40-5:30 - recover: 1400m @z2 - cooldown: 500m 8k jog workout: 11k, middle 5k @MP - warmup: 3km @z2 - run: 5km @ 5:40-5:30 - cooldown: 3km @z2 1.5h run workout: easy 1h jog - run: 60:00 55.0 4:00:00 40.0 24.8
17 16 workout: 10k, middle 5k @HMP - warmup: 3km @z2 - run: 5km @ 5:05-4:30 - cooldown: 2km @z2 8k jog workout: easy 5k jog - run: 5km 80k race Rest. (Duh.) 103.0 5:00:00 50.0 31.1
18 1,517.0
19 TMP 16k pace
20 HMP Half Marathon Pace Avg min/km 0:06:00
21 MP Marathon Pace

@ -6,12 +6,13 @@ import com.github.mgifos.workouts.model.DistanceUnits._
class WorkoutSpec extends FlatSpec with Matchers {
"Workout" should "parse correctly" in {
a[IllegalArgumentException] should be thrownBy Workout.parse("")
a[IllegalArgumentException] should be thrownBy Workout.parse("workout")
a[IllegalArgumentException] should be thrownBy Workout.parse("workout:")
a[IllegalArgumentException] should be thrownBy Workout.parse("workout: !")
a[IllegalArgumentException] should be thrownBy Workout.parse("workout run-fast")
a[IllegalArgumentException] should be thrownBy Workout.parse(" workout: run-fast")
Workout.parseDef("") should be('left)
Workout.parseDef("workout") should be('left)
Workout.parseDef("workout:") should be('left)
Workout.parseDef("workout: !") should be('left)
Workout.parseDef("workout run-fast") should be('left)
Workout.parseDef(" workout: run-fast") should be('left)
/*
workout: run-fast
- warmup: 10:00
@ -21,12 +22,13 @@ class WorkoutSpec extends FlatSpec with Matchers {
- cooldown: 5:00
*/
val testWO = "workout: run-fast\n- warmup: 10:00\n- repeat: 2\n - run: 1500m @ z4\n - recover: 01:30 @ z2\n- cooldown: 5:00"
Workout.parse(testWO) should be(
Workout("run-fast", Seq(
WarmupStep(TimeDuration(minutes = 10)),
RepeatStep(2, Seq(
RunStep(DistanceDuration(1500, m), Some(HrZoneTarget(4))),
RecoverStep(TimeDuration(0, 1, 30), Some(HrZoneTarget(2))))),
CooldownStep(TimeDuration(minutes = 5)))))
Workout.parseDef(testWO) should be(
Right(
WorkoutDef("run-fast", Seq(
WarmupStep(TimeDuration(minutes = 10)),
RepeatStep(2, Seq(
RunStep(DistanceDuration(1500, m), Some(HrZoneTarget(4))),
RecoverStep(TimeDuration(0, 1, 30), Some(HrZoneTarget(2))))),
CooldownStep(TimeDuration(minutes = 5))))))
}
}

Loading…
Cancel
Save