parent
bd4125a2b8
commit
916951f328
@ -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> ``- run: 1.5km @ 5:10-4:40``<br> ``- 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| |
||||||
@ -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) |
||||||
|
} |
||||||
@ -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 |
package com.github.mgifos.workouts.model |
||||||
|
|
||||||
case class Workout(name: String, steps: Seq[Step] = Nil) { |
trait Workout |
||||||
def withStep(step: Step): Workout = Workout(name, steps :+ step) |
|
||||||
|
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 { |
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 |
private val NextStepRx = """^((-\s\w*:\s.*)((\n\s{1,}-\s.*)*))(([\s].*)*)$""".r |
||||||
|
|
||||||
def parse(x: String) = { |
def parseDef(x: String): Either[String, WorkoutDef] = { |
||||||
def loop(w: Workout, steps: String): Workout = steps match { |
def loop(w: WorkoutDef, steps: String): Either[String, WorkoutDef] = steps match { |
||||||
case NextStepRx(next, _, _, _, rest, _) => |
case NextStepRx(next, _, _, _, rest, _) => |
||||||
val newWorkout = w.withStep(Step.parse(next.trim)) |
val newWorkout = w.withStep(Step.parse(next.trim)) |
||||||
if (rest.trim.isEmpty) newWorkout |
if (rest.trim.isEmpty) Right(newWorkout) |
||||||
else loop(newWorkout, rest.trim) |
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 { |
x match { |
||||||
case WorkoutName(name, steps, _) => |
case WorkoutName(name, steps, _) => loop(WorkoutDef(name), steps.trim) |
||||||
loop(Workout(name), steps.trim) |
case _ => Left(s"Input string cannot be parsed to Workout: $x") |
||||||
case _ => throw new IllegalArgumentException(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) |
||||||
|
} |
||||||
} |
} |
||||||
|
|||||||
|
Loading…
Reference in new issue