From b2aaefaf56de205a942b176d8ded75fda9e947f4 Mon Sep 17 00:00:00 2001 From: mgifos Date: Sun, 17 Jun 2018 22:20:48 +0200 Subject: [PATCH] Supported cycling workouts #16 --- README.md | 22 +++++--- images/15k-wo.png | Bin 0 -> 30201 bytes .../model/Step.scala | 4 +- .../model/Target.scala | 21 +++++++ .../model/Workout.scala | 25 ++++++--- src/test/resources/ultra-80k-runnersworld.csv | 52 +++++++++--------- .../mgifos/workouts/model/StepSpec.scala | 4 +- .../mgifos/workouts/model/WorkoutSpec.scala | 29 ++++++---- 8 files changed, 100 insertions(+), 57 deletions(-) create mode 100644 images/15k-wo.png diff --git a/README.md b/README.md index 27a87d3..57d98d2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ is a command line tool to define, import, schedule and share GarminConnect worko An example of a workout definition notation: ```sh -workout: 15k, 3x3.2k @HMP +running: 15k, 3x3.2k @HMP - warmup: 2km @z2 - repeat: 3 - run: 3200m @ 5:05-4:50 @@ -12,7 +12,7 @@ workout: 15k, 3x3.2k @HMP - cooldown: lap-button ``` and the tool's job is actually to translate it to this: -![15k workout](https://i.imgur.com/vxXNV7w.png) +![15k workout](https://raw.githubusercontent.com/mgifos/quick-plan/master/images/15k-wo.png) ## File format @@ -22,10 +22,10 @@ An example of 2-weeks training plan, containing 2 workout definitions, 4 referen | Week | Mon | Tue | Wed | Thu | Fri | Sat | Sun | | ----:| --- | --- | --- | --- | --- | --- | --- | -| 1 | ``workout: run-fast``
``- warmup: 10:00 @ z2``
``- repeat: 3``
  ``- run: 1.5km @ 5:10-4:40``
  ``- recover: 500m @ z2``
``- cooldown: 05:00``|rest|rest|run-fast|rest|rest|rest| -| 2 | run-fast| ``workout: long-15``
``- run: 15 km @ z2``|rest|run-fast|rest|rest|long-15| +| 1 | ``running: run-fast``
``- warmup: 10:00 @ z2``
``- repeat: 3``
  ``- run: 1.5km @ 5:10-4:40``
  ``- recover: 500m @ z2``
``- cooldown: 05:00``|rest|rest|run-fast|rest|rest|rest| +| 2 | run-fast| ``cycling: cycle-wo``
``- bike: 15 km @ 20.0-30kph``|rest|run-fast|rest|rest|cycle-wo| -Checkout a [complete training plan for 80K ultra](https://docs.google.com/spreadsheets/d/1b1ZzrAFrjd-kvPq11zlbE2bWn2IQmUy0lBqIOFjqbwk/edit?usp=sharing). It was originally published in an article of Runner's world website - here's [the link](https://www.runnersworld.com/ultrarunning/the-ultimate-ultramarathon-training-plan). +Checkout a [complete training plan for 80K ultra](https://docs.google.com/spreadsheets/d/1b1ZzrAFrjd-kvPq11zlbE2bWn2IQmUy0lBqIOFjqbwk/edit?usp=sharing). It was originally published in an article on Runner's world website - here's [the link](https://www.runnersworld.com/ultrarunning/the-ultimate-ultramarathon-training-plan). ## Installation @@ -67,11 +67,13 @@ quick-plan schedule -n 2018-04-29 -x -e your-mail-address@example.com ultra-80k- ``` ## Workout notation -The reserved keywords of the notation are: workout, warmup, cooldown, run, repeat, recover and lap-button. +The reserved keywords of the notation are: workout, warmup, cooldown, run, bike, repeat, recover and lap-button. **``** := `
+` -**`
`** := `workout: ` +**`
`** := `: ` + +**``** := (running | cycling) **``** := `[\u0020-\u007F]+` (printable ascii characters) @@ -79,7 +81,7 @@ The reserved keywords of the notation are: workout, warmup, cooldown, run, repea **``** := ` | ` -**``** := `(warmup | cooldown | run | recover): [@ ]` +**``** := `(warmup | cooldown | run | bike | recover): [@ ]` **``** := `repeat: ( - )+` @@ -95,8 +97,12 @@ The reserved keywords of the notation are: workout, warmup, cooldown, run, repea **``** := ` - ` +**``** := ` - kph` + **``** := `:` +**``** := `\d{1,3}(\.\d)?` + **``** := `\d{1,2}` **``** := `\d{2}` \ No newline at end of file diff --git a/images/15k-wo.png b/images/15k-wo.png new file mode 100644 index 0000000000000000000000000000000000000000..9bcec7c57794a5c444c351f3b626bc09e6a25707 GIT binary patch literal 30201 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 name match { case "warmup" => WarmupStep.tupled(expect(params)) - case "run" => RunStep.tupled(expect(params)) + case "run" | "bike" => IntervalStep.tupled(expect(params)) case "recover" => RecoverStep.tupled(expect(params)) case "cooldown" => CooldownStep.tupled(expect(params)) case _ => throw new IllegalArgumentException(s"Duration step type was expected, $name") diff --git a/src/main/scala/com.github.mgifos.workouts/model/Target.scala b/src/main/scala/com.github.mgifos.workouts/model/Target.scala index 837176d..5aaf12f 100644 --- a/src/main/scala/com.github.mgifos.workouts/model/Target.scala +++ b/src/main/scala/com.github.mgifos.workouts/model/Target.scala @@ -15,6 +15,7 @@ case class HrZoneTarget(zone: Int) extends Target { "targetValueTwo" -> "", "zoneNumber" -> zone.toString) } + case class PaceTarget(from: Pace, to: Pace) extends Target { override def json = Json.obj( "targetType" -> Json.obj( @@ -25,6 +26,16 @@ case class PaceTarget(from: Pace, to: Pace) extends Target { "zoneNumber" -> JsNull) } +case class SpeedTarget(from: KphSpeed, to: KphSpeed) extends Target { + override def json = Json.obj( + "targetType" -> Json.obj( + "workoutTargetTypeId" -> 5, + "workoutTargetTypeKey" -> "speed.zone"), + "targetValueOne" -> from.speed, + "targetValueTwo" -> to.speed, + "zoneNumber" -> JsNull) +} + object NoTarget extends Target { override def json = Json.obj( "targetType" -> Json.obj( @@ -45,12 +56,22 @@ case class Pace(exp: String) { def speed: Double = 1000D / (minutes * 60 + seconds) } +case class KphSpeed(exp: String) { + + /** + * @return Speed in m/s + */ + def speed: Double = exp.toDouble * 10 / 36 +} + object Target { private val HrZoneRx = """^z(\d)$""".r private val PaceRangeRx = """^(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})$""".r + private val SpeedRangeRx = """^(\d{1,3}(\.\d{1})?)\s*-\s*(\d{1,3}(\.\d{1})?)\s*kph$""".r def parse(x: String): Target = x.trim match { case HrZoneRx(zone) => HrZoneTarget(zone.toInt) + case SpeedRangeRx(from, _, to, _) => SpeedTarget(KphSpeed(from), KphSpeed(to)) case PaceRangeRx(from, to) => PaceTarget(Pace(from), Pace(to)) case _ => throw new IllegalArgumentException(s"Unknown target specification: $x") } diff --git a/src/main/scala/com.github.mgifos.workouts/model/Workout.scala b/src/main/scala/com.github.mgifos.workouts/model/Workout.scala index 50ea901..84d9af7 100644 --- a/src/main/scala/com.github.mgifos.workouts/model/Workout.scala +++ b/src/main/scala/com.github.mgifos.workouts/model/Workout.scala @@ -1,25 +1,26 @@ package com.github.mgifos.workouts.model import play.api.libs.json.{ JsValue, Json } +import Workout._ trait Workout { def json: JsValue } -case class WorkoutDef(name: String, steps: Seq[Step] = Nil) extends Workout { +case class WorkoutDef(sport: String, name: String, steps: Seq[Step] = Nil) extends Workout { def toRef: WorkoutRef = WorkoutRef(name) - def withStep(step: Step): WorkoutDef = WorkoutDef(name, steps :+ step) + def withStep(step: Step): WorkoutDef = WorkoutDef(sport, name, steps :+ step) def json: JsValue = Json.obj( "sportType" -> Json.obj( - "sportTypeId" -> 1, - "sportTypeKey" -> "running"), + "sportTypeId" -> sportId(sport), + "sportTypeKey" -> sport), "workoutName" -> name, "workoutSegments" -> Json.arr( Json.obj( "segmentOrder" -> 1, "sportType" -> Json.obj( - "sportTypeId" -> 1, - "sportTypeKey" -> "running"), + "sportTypeId" -> sportId(sport), + "sportTypeKey" -> sport), "workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) }))) } @@ -33,7 +34,7 @@ case class WorkoutNote(note: String) extends Workout { object Workout { - private val WorkoutName = """^workout:\s([\u0020-\u007F]+)((\n\s*\-\s[a-z]+:.*)*)$""".r + private val WorkoutHeader = """^(running|cycling):\s([\u0020-\u007F]+)((\n\s*\-\s[a-z]+:.*)*)$""".r private val NextStepRx = """^((-\s\w*:\s.*)((\n\s{1,}-\s.*)*))(([\s].*)*)$""".r def parseDef(x: String): Either[String, WorkoutDef] = { @@ -45,13 +46,19 @@ object Workout { case _ => Left(s"Input string cannot be parsed to Workout: $steps") } x match { - case WorkoutName(name, steps, _) => loop(WorkoutDef(name), steps.trim) + case WorkoutHeader(sport, name, steps, _) => loop(WorkoutDef(sport, 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 WorkoutHeader(_, name, _, _) => WorkoutRef(name) case _ => WorkoutRef(x.trim) } + + def sportId(sport: String) = sport match { + case "running" => 1 + case "cycling" => 2 + case _ => throw new IllegalArgumentException("Only running and cycling workouts are supported.") + } } diff --git a/src/test/resources/ultra-80k-runnersworld.csv b/src/test/resources/ultra-80k-runnersworld.csv index cba56d8..e06bd27 100644 --- a/src/test/resources/ultra-80k-runnersworld.csv +++ b/src/test/resources/ultra-80k-runnersworld.csv @@ -1,114 +1,114 @@ WEEK,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday,Estimated km,,Duration,Avg km,Miles -1,,"workout: 14k, 4x 1.6k @TMP +1,,"running: 14k, 4x 1.6k @TMP - warmup: 2km @z2 - repeat: 4 - run: 1600m @ 5:00-4:30 - recover: 900m @z2 - run: 2km -- cooldown: lap-button","workout: 8k jog +- cooldown: lap-button","running: 8k jog - run: 8km @z2 -- cooldown: lap-button","workout: 11-15k, middle 5k @MP +- cooldown: lap-button","running: 11-15k, middle 5k @MP - warmup: 4km @z2 - run: 5km @ 5:40-5:30 - run: 6km @z2 -- cooldown: lap-button",,"workout: 1.5h run +- cooldown: lap-button",,"running: 1.5h run - run: 90:00 -- cooldown: lap-button","workout: 3h run +- cooldown: lap-button","running: 3h run - run: 180:00 - cooldown: lap-button",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 +3,,"running: 14k, 2x1.6k @HMP - warmup: 4km @z2 - repeat: 2 - run: 1.6km @ 5:05-4:50 - recover: 1.4km @z2 - run: 4km @z2 -- cooldown: lap-button",8k jog,"11-15k, middle 5k @MP",,"workout: 2h run +- cooldown: lap-button",8k jog,"11-15k, middle 5k @MP",,"running: 2h run - run: 120:00 -- cooldown: lap-button","workout: 3.5h run +- cooldown: lap-button","running: 3.5h run - run: 210:00 - cooldown: lap-button",90.0,,0:30:00,5.0,3.1 -4,,"workout: 10k, 3x1.6k @TMP +4,,"running: 10k, 3x1.6k @TMP - warmup: 1500m @z2 - repeat: 3 - run: 1600m @ 5:00-4:30 - recover: 900m @z2 - run: 1km @z2 -- cooldown: lap-button",8k jog,"workout: 10k, middle 3.2k @MP +- cooldown: lap-button",8k jog,"running: 10k, middle 3.2k @MP - warmup: 4km @z2 - run: 3200m @ 5:40-5:30 - run: 2800m @z2 - cooldown: lap-button",,1.5h run,2h run,63.0,,0:40:00,6.7,4.1 -5,,"workout: 15k, 6x1.6k @TMP +5,,"running: 15k, 6x1.6k @TMP - warmup: 2km @z2 - repeat: 6 - run: 1600m @ 5:00-4:30 - recover: 400m @z2 - run: 1km -- cooldown: lap-button",8k jog,"workout: 15k, middle 5k @MP +- cooldown: lap-button",8k jog,"running: 15k, middle 5k @MP - warmup: 5km @z2 - run: 5km @ 5:40-5:30 - run: 5km @z2 -- cooldown: lap-button",,"workout: 3.5-4h run +- cooldown: lap-button",,"running: 3.5-4h run - run 225:00 - cooldown: lap-button",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 +7,,"running: 15k, 6x1.6k @HMP - warmup: 2km @z2 - repeat: 6 - run: 1600m @ 5:05-4:50 - recover: 400m @z2 - run: 1km -- cooldown: lap-button",8k jog,"15k, middle 5k @MP",,3.5-4h run,"workout: 3h run, last @MP +- cooldown: lap-button",8k jog,"15k, middle 5k @MP",,3.5-4h run,"running: 3h run, last @MP - run: 120:00 - run: 60:00 @ 5:40-5:30 - cooldown: lap-button",104.7,,1:00:00,10.0,6.2 -8,,"workout: 15k, 3x3.2k @HMP +8,,"running: 15k, 3x3.2k @HMP - warmup: 2km @z2 - repeat: 3 - run: 3200m @ 5:05-4:50 - recover: 800m @z2 - run: 1km @z2 -- cooldown: lap-button",8k jog,"15k, middle 5k @MP",,2h run,"workout: 2.5h run +- cooldown: lap-button",8k jog,"15k, middle 5k @MP",,2h run,"running: 2.5h run - run: 150:00 - cooldown: lap-button",83.0,,1:20:00,13.3,8.3 -9,,"15k, 6x1.6k @TMP",8k jog,"15k, middle 5k @MP",,"workout: 4h run +9,,"15k, 6x1.6k @TMP",8k jog,"15k, middle 5k @MP",,"running: 4h run - run: 240:00 -- cooldown: lap-button","workout: 3.5h run, last @MP +- cooldown: lap-button","running: 3.5h run, last @MP - run: 150:00 - run: 60:00 @ 5:40-5:30 - cooldown: lap-button",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 +12,,"15k, 6x1.6k @TMP",8k jog,"15k, middle 5k @MP",,4h run,"running: 5h run - run: 300:00 - cooldown: lap-button",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 +14,,"running: 15k, 4x1.6k @TMP - warmup: 3km @z2 - repeat: 4 - run: 1600m @ 5:00-4:30 - recover: 1400m @z2 - run: 1km @z2 - cooldown: lap-button",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 +15,,"running: 11k, 3x1.6k @MP - warmup: 1.5km @z2 - repeat: 3 - run: 1600m @ 5:40-5:30 - recover: 1400m @z2 - run: 500m @z2 -- cooldown: lap-button",8k jog,"workout: 11k, middle 5k @MP +- cooldown: lap-button",8k jog,"running: 11k, middle 5k @MP - warmup: 3km @z2 - run: 5km @ 5:40-5:30 - run: 3km @z2 -- cooldown: lap-button",,1.5h run,"workout: easy 1h jog +- cooldown: lap-button",,1.5h run,"running: easy 1h jog - run: 60:00 - cooldown: lap-button",55.0,,4:00:00,40.0,24.8 -16,,"workout: 10k, middle 5k @HMP +16,,"running: 10k, middle 5k @HMP - warmup: 3km @z2 - run: 5km @ 5:05-4:30 - run: 2km @z2 -- cooldown: lap-button",8k jog,"workout: easy 5k jog +- cooldown: lap-button",8k jog,"running: easy 5k jog - run: 5km - cooldown: lap-button",,Race day!,,103.0,,5:00:00,50.0,31.1 ,,,,,,,,"1,517.0",,,, diff --git a/src/test/scala/com/github/mgifos/workouts/model/StepSpec.scala b/src/test/scala/com/github/mgifos/workouts/model/StepSpec.scala index 8f75398..d011bc7 100644 --- a/src/test/scala/com/github/mgifos/workouts/model/StepSpec.scala +++ b/src/test/scala/com/github/mgifos/workouts/model/StepSpec.scala @@ -11,11 +11,11 @@ class StepSpec extends FlatSpec with Matchers { a[AssertionError] should be thrownBy Step.parse("- warmup: 5km\n - run: 10km\n - recover: 100m") Step.parse("- warmup: 5km") should be(WarmupStep(DistanceDuration(5, km))) - Step.parse("- run: 2km @ 5:00-4:50") should be(RunStep(DistanceDuration(2, km), Some(PaceTarget(Pace("5:00"), Pace("4:50"))))) + Step.parse("- run: 2km @ 5:00-4:50") should be(IntervalStep(DistanceDuration(2, km), Some(PaceTarget(Pace("5:00"), Pace("4:50"))))) Step.parse("- recover: 500m @z2") should be(RecoverStep(DistanceDuration(500, m), Some(HrZoneTarget(2)))) Step.parse("- cooldown: 05:00") should be(CooldownStep(TimeDuration(minutes = 5))) Step.parse("- repeat: 3\n - run: 10km\n - recover: 100m") should be(RepeatStep(3, List( - RunStep(DistanceDuration(10, km)), + IntervalStep(DistanceDuration(10, km)), RecoverStep(DistanceDuration(100, m))))) } } diff --git a/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala b/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala index 947efc2..caf1d3b 100644 --- a/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala +++ b/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala @@ -7,29 +7,29 @@ import play.api.libs.json.Json class WorkoutSpec extends FlatSpec with Matchers { /* - workout: run-fast + running: run-fast - warmup: 10:00 - repeat: 2 - run: 1500m @ 4:30-5:00 - recover: 01:30 @ z2 - cooldown: lap-button */ - val testWO = "workout: run-fast\n- warmup: 10:00\n- repeat: 2\n - run: 1500m @ 4:30-5:00\n - recover: 01:30 @ z2\n- cooldown: lap-button" + val testWO = "running: run-fast\n- warmup: 10:00\n- repeat: 2\n - run: 1500m @ 4:30-5:00\n - recover: 01:30 @ z2\n- cooldown: lap-button" "Workout" should "parse correctly" in { 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.parseDef("running") should be('left) + Workout.parseDef("running:") should be('left) + Workout.parseDef("running !") should be('left) + Workout.parseDef("running run-fast") should be('left) + Workout.parseDef(" running: run-fast") should be('left) Workout.parseDef(testWO) should be( Right( - WorkoutDef("run-fast", Seq( + WorkoutDef("running", "run-fast", Seq( WarmupStep(TimeDuration(minutes = 10)), RepeatStep(2, Seq( - RunStep(DistanceDuration(1500, m), Some(PaceTarget(Pace("4:30"), Pace("5:00")))), + IntervalStep(DistanceDuration(1500, m), Some(PaceTarget(Pace("4:30"), Pace("5:00")))), RecoverStep(TimeDuration(1, 30), Some(HrZoneTarget(2))))), CooldownStep(LapButtonPressed))))) } @@ -45,10 +45,19 @@ class WorkoutSpec extends FlatSpec with Matchers { } } - "Workout" should "dump json correctly" in { val is = getClass.getClassLoader.getResourceAsStream("run-fast.json") val expectJson = Json.parse(is) Workout.parseDef(testWO).map(_.json) should be(Right(expectJson)) } + + "Workout" should "support cycling ws" in { + val testBike = "cycling: cycle-test\n- warmup: 5:00\n- bike: 20km @ 20.0-100kph\n- cooldown: lap-button" + Workout.parseDef(testBike) should be( + Right( + WorkoutDef("cycling", "cycle-test", Seq( + WarmupStep(TimeDuration(minutes = 5)), + IntervalStep(DistanceDuration(20, km), Some(SpeedTarget(KphSpeed("20.0"), KphSpeed("100")))), + CooldownStep(LapButtonPressed))))) + } }