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 |
||||
|
||||
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) |
||||
} |
||||
} |
||||
|
||||
|
Loading…
Reference in new issue