From 05eaef7b7d9721e87b3171cf385664394ee73fb4 Mon Sep 17 00:00:00 2001 From: mgifos Date: Wed, 12 Dec 2018 23:01:12 +0100 Subject: [PATCH] Nested repeats #38 --- .../model/Step.scala | 71 ++++++++++++------- .../mgifos/workouts/model/WorkoutSpec.scala | 58 ++++++++++++--- 2 files changed, 91 insertions(+), 38 deletions(-) diff --git a/src/main/scala/com.github.mgifos.workouts/model/Step.scala b/src/main/scala/com.github.mgifos.workouts/model/Step.scala index 224e395..a84085f 100644 --- a/src/main/scala/com.github.mgifos.workouts/model/Step.scala +++ b/src/main/scala/com.github.mgifos.workouts/model/Step.scala @@ -50,38 +50,55 @@ case class RepeatStep(count: Int, steps: Seq[Step]) extends Step { object Step { - private val StepRx = """^(-\s\w*:\s.*)(([\r\n]+\s{1,}-\s.*)*)$""".r - private val StepHeader = """^\s*-\s*(\w*):(.*)$""".r - private val ParamsRx = """^([\w-\.:\s]+)\s*(@(.*))?$""".r + def parse(text: String)(implicit msys: MeasurementSystems.MeasurementSystem): Step = { - def parse(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): Step = x match { - case StepRx(header, subSteps, _) if subSteps.nonEmpty => - header match { + def loop(depth: Int)(x: String): Step = { + + val indent = depth * 2 + + val StepRx = raw"""^(\s{$indent}-\s\w*:\s.*)(([\r\n]+\s{1,}-\s.*)*)$$""".r + val StepHeader = raw"""^\s{$indent}-\s*(\w*):(.*)$$""".r + val ParamsRx = """^([\w-\.:\s]+)\s*(@(.*))?$""".r + + def parseDurationStep(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): DurationStep = x match { case StepHeader(name, params) => - if (name != "repeat") throw new IllegalArgumentException(s"'$name' cannot contain sub-steps, it must be 'repeat'") - RepeatStep(params.trim.toInt, subSteps.trim.lines.toList.map(parseDurationStep)) - case _ => throw new IllegalArgumentException(s"Cannot parse repeat step $header") + name match { + case "warmup" => WarmupStep.tupled(expect(params)) + case "run" | "bike" | "go" => IntervalStep.tupled(expect(params)) + case "recover" => RecoverStep.tupled(expect(params)) + case "cooldown" => CooldownStep.tupled(expect(params)) + case _ => throw new IllegalArgumentException(s"'$name' is not a duration step type") + } + case _ => throw new IllegalArgumentException(s"Cannot parse duration step: $x") } - case StepRx(header, "", null) => parseDurationStep(header) - case _ => throw new IllegalArgumentException(s"Cannot parse step:$x") - } - private def parseDurationStep(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): DurationStep = x match { - case StepHeader(name, params) => - name match { - case "warmup" => WarmupStep.tupled(expect(params)) - case "run" | "bike" | "go" => IntervalStep.tupled(expect(params)) - case "recover" => RecoverStep.tupled(expect(params)) - case "cooldown" => CooldownStep.tupled(expect(params)) - case _ => throw new IllegalArgumentException(s"'$name' is not a duration step type") + def expect(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): (Duration, Option[Target]) = x.trim match { + case ParamsRx(duration, _, target) => + val maybeTarget = Option(target).filter(_.trim.nonEmpty).map(Target.parse) + (Duration.parse(duration.trim), maybeTarget) + case raw => throw new IllegalArgumentException(s"Cannot parse step parameters $raw") } - case _ => throw new IllegalArgumentException(s"Cannot parse duration step: $x") - } - private def expect(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): (Duration, Option[Target]) = x.trim match { - case ParamsRx(duration, _, target) => - val maybeTarget = Option(target).filter(_.trim.nonEmpty).map(Target.parse) - (Duration.parse(duration.trim), maybeTarget) - case raw => throw new IllegalArgumentException(s"Cannot parse step parameters $raw") + x match { + case StepRx(header, subdef, _) if subdef.nonEmpty => + header match { + case StepHeader(name, params) => + if (name != "repeat") throw new IllegalArgumentException(s"'$name' cannot contain sub-steps, it must be 'repeat'") + val next = subdef.replaceFirst("[\n\r]*", "") + val nextIndent = indent + 2 + val steps = next.split(raw"""[\n\r]{1,2}\s{$nextIndent}-""").toList match { + case head :: tail => head :: tail.map(" " * nextIndent + "-" + _) + case original => original + } + RepeatStep(params.trim.toInt, steps.map(loop(depth + 1))) + case _ => throw new IllegalArgumentException(s"Cannot parse repeat step $header") + } + case StepRx(header, "", null) => parseDurationStep(header) + case _ => throw new IllegalArgumentException(s"Cannot parse step:$x") + } + } + + loop(0)(text) } + } 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 86c9c57..fbb0bda 100644 --- a/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala +++ b/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala @@ -1,7 +1,7 @@ package com.github.mgifos.workouts.model import com.github.mgifos.workouts.model.DistanceUnits._ -import org.scalatest.{ Matchers, WordSpec } +import org.scalatest.{Matchers, WordSpec} import play.api.libs.json.Json class WorkoutSpec extends WordSpec with Matchers { @@ -28,12 +28,21 @@ class WorkoutSpec extends WordSpec with Matchers { } "parse a workout definition correctly" in { - Workout.parse(testWO) shouldBe WorkoutDef("running", "run-fast", Seq( - WarmupStep(TimeDuration(minutes = 10)), - RepeatStep(2, Seq( - IntervalStep(DistanceDuration(1500, m), Some(PaceTarget(Pace(msys.distance, "4:30"), Pace(msys.distance, "5:00")))), - RecoverStep(TimeDuration(1, 30), Some(HrZoneTarget(2))))), - CooldownStep(LapButtonPressed))) + Workout.parse(testWO) shouldBe WorkoutDef( + "running", + "run-fast", + Seq( + WarmupStep(TimeDuration(minutes = 10)), + RepeatStep( + 2, + Seq( + IntervalStep(DistanceDuration(1500, m), Some(PaceTarget(Pace(msys.distance, "4:30"), Pace(msys.distance, "5:00")))), + RecoverStep(TimeDuration(1, 30), Some(HrZoneTarget(2))) + ) + ), + CooldownStep(LapButtonPressed) + ) + ) } "parse various printable workout-names correctly" in { @@ -49,10 +58,15 @@ class WorkoutSpec extends WordSpec with Matchers { "parse cycling workouts" in { val testBike = "cycling: cycle-test\r\n- warmup: 5:00\n- bike: 20km @ 20.0-100kph\r- cooldown: lap-button" - Workout.parse(testBike) shouldBe WorkoutDef("cycling", "cycle-test", Seq( - WarmupStep(TimeDuration(minutes = 5)), - IntervalStep(DistanceDuration(20, km), Some(SpeedTarget(Speed(km, "20.0"), Speed(km, "100")))), - CooldownStep(LapButtonPressed))) + Workout.parse(testBike) shouldBe WorkoutDef( + "cycling", + "cycle-test", + Seq( + WarmupStep(TimeDuration(minutes = 5)), + IntervalStep(DistanceDuration(20, km), Some(SpeedTarget(Speed(km, "20.0"), Speed(km, "100")))), + CooldownStep(LapButtonPressed) + ) + ) } "validate on a workout definition level" in { @@ -75,6 +89,28 @@ class WorkoutSpec extends WordSpec with Matchers { case WorkoutStepFailure(_, "'20x' is not a valid target specification") => } } + + "parse nested repeats" in { + val testNested = + "running: nested repeats\n- warmup: 20:00\n- repeat: 2\n - repeat: 4\n - run: 0.6km @ 4:25-4:15\n - recover: 1:00\n - recover: 05:00\n- cooldown: lap-button" + Workout.parse(testNested) shouldBe WorkoutDef( + "running", + "nested repeats", + Seq( + WarmupStep(TimeDuration(20)), + RepeatStep( + count = 2, + Seq( + RepeatStep(count = 4, + Seq(IntervalStep(DistanceDuration(0.6f, km), Some(PaceTarget(Pace(km, "4:25"), Pace(km, "4:15")))), + RecoverStep(TimeDuration(1, 0)))), + RecoverStep(TimeDuration(5, 0)) + ) + ), + CooldownStep(LapButtonPressed) + ) + ) + } } "Workout should" should {