Initial import of model and parsers

master
mgifos 8 years ago
parent 43ed552093
commit bd4125a2b8
  1. 3
      .gitignore
  2. 6
      README
  3. 9
      build.sbt
  4. 1
      project/build.properties
  5. 1
      project/plugins.sbt
  6. 26
      src/main/scala/com.github.mgifos.workouts/model/Duration.scala
  7. 58
      src/main/scala/com.github.mgifos.workouts/model/Step.scala
  8. 22
      src/main/scala/com.github.mgifos.workouts/model/Target.scala
  9. 26
      src/main/scala/com.github.mgifos.workouts/model/Workout.scala
  10. 30
      src/test/scala/com/github/mgifos/workouts/model/DurationSpec.scala
  11. 21
      src/test/scala/com/github/mgifos/workouts/model/StepSpec.scala
  12. 19
      src/test/scala/com/github/mgifos/workouts/model/TargetSpec.scala
  13. 32
      src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala

3
.gitignore vendored

@ -1,2 +1,5 @@
*.class *.class
*.log *.log
.idea
project/target
target

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

@ -0,0 +1,9 @@
name := "workouts"
version := "0.1"
scalaVersion := "2.12.4"
libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "3.0.5" % "test"
)

@ -0,0 +1 @@
sbt.version = 0.13.16

@ -0,0 +1 @@
addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.2")

@ -0,0 +1,26 @@
package com.github.mgifos.workouts.model
import com.github.mgifos.workouts.model.DistanceUnits.DistanceUnit
sealed trait Duration
case class DistanceDuration(distance: Float, unit: DistanceUnit) extends Duration
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 {
private val DistanceRx = """^(\d+([\.]\d+)?)\s*(km|mi|m)$""".r
private val MinutesRx = """^(\d{1,3}):(\d{2})$""".r
def parse(x: String): Duration = x match {
case DistanceRx(quantity, _, unit) => DistanceDuration(quantity.toFloat, DistanceUnits.withName(unit))
case MinutesRx(minutes, seconds) => TimeDuration(minutes = minutes.toInt, seconds = seconds.toInt)
case _ => throw new IllegalArgumentException(s"Duration cannot be parsed $x")
}
}

@ -0,0 +1,58 @@
package com.github.mgifos.workouts.model
sealed trait Step {
def `type`: String
}
abstract class DurationStep(stepType: String, duration: Duration, target: Option[Target] = None) extends Step {
override def `type` = stepType
}
case class WarmupStep(duration: Duration, target: Option[Target] = None) extends DurationStep("warmup", duration)
case class RunStep(duration: Duration, target: Option[Target] = None) extends DurationStep("run", duration)
case class RecoverStep(duration: Duration, target: Option[Target] = None) extends DurationStep("recover", duration)
case class CooldownStep(duration: Duration, target: Option[Target] = None) extends DurationStep("cooldown", duration)
case class RepeatStep(count: Int, definition: Seq[DurationStep]) extends Step {
override def `type` = "repeat"
}
object Step {
private val StepRx = """^(-\s\w*:\s.*)((\n\s{1,}-\s.*)*)$""".r
private val StepHeader = """^\s*-\s*(\w*):(.*)$""".r
private val ParamsRx = """^([\w\.:\s]+)\s*(@(.*))?$""".r
def parse(x: String): Step = x match {
case StepRx(header, subSteps, _) if subSteps.nonEmpty => header match {
case StepHeader(name, params) =>
assert(name == "repeat", "must be 'repeat' if contains sub-steps")
RepeatStep(params.trim.toInt, subSteps.trim.lines.toList.map(parseDurationStep))
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")
}
private def parseDurationStep(x: String): DurationStep = x match {
case StepHeader(name, params) => name match {
case "warmup" => WarmupStep.tupled(expect(params))
case "run" => RunStep.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")
}
case _ => throw new IllegalArgumentException(s"Cannot parse step type $x")
}
private def expect(x: String): (Duration, Option[Target]) = x match {
case ParamsRx(duration, _, target) =>
val maybeTarget = Option(target).filter(_.trim.nonEmpty).map(Target.parse)
(Duration.parse(duration.trim), maybeTarget)
case _ => throw new IllegalArgumentException(s"Cannot parse step parameters $x")
}
}

@ -0,0 +1,22 @@
package com.github.mgifos.workouts.model
sealed trait Target
case class HrZoneTarget(zone: Int) extends Target
case class PaceTarget(from: Pace, to: Pace) extends Target
case class Pace(exp: String) {
def minutes: Int = exp.trim.takeWhile(_ != ':').toInt
def seconds: Int = exp.trim.split(":").last.toInt
}
object Target {
private val HrZoneRx = """^z(\d)$""".r
private val PaceRangeRx = """^(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})$""".r
def parse(x: String): Target = x.trim match {
case HrZoneRx(zone) => HrZoneTarget(zone.toInt)
case PaceRangeRx(from, to) => PaceTarget(Pace(from), Pace(to))
case _ => throw new IllegalArgumentException(s"Unknown target specification: $x")
}
}

@ -0,0 +1,26 @@
package com.github.mgifos.workouts.model
case class Workout(name: String, steps: Seq[Step] = Nil) {
def withStep(step: Step): Workout = Workout(name, steps :+ step)
}
object Workout {
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 {
case NextStepRx(next, _, _, _, rest, _) =>
val newWorkout = w.withStep(Step.parse(next.trim))
if (rest.trim.isEmpty) newWorkout
else loop(newWorkout, rest.trim)
case _ => throw new IllegalArgumentException(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")
}
}
}

@ -0,0 +1,30 @@
package com.github.mgifos.workouts.model
import org.scalatest.{ FlatSpec, Matchers }
import com.github.mgifos.workouts.model.DistanceUnits._
class DurationSpec extends FlatSpec with Matchers {
"Duration" should "parse correctly" in {
a[IllegalArgumentException] should be thrownBy Duration.parse("")
a[IllegalArgumentException] should be thrownBy Duration.parse("5")
a[IllegalArgumentException] should be thrownBy Duration.parse("5k")
a[IllegalArgumentException] should be thrownBy Duration.parse("5 k")
a[IllegalArgumentException] should be thrownBy Duration.parse(":")
a[IllegalArgumentException] should be thrownBy Duration.parse("1:")
a[IllegalArgumentException] should be thrownBy Duration.parse("1:0")
a[IllegalArgumentException] should be thrownBy Duration.parse("01: 00")
a[IllegalArgumentException] should be thrownBy Duration.parse("00")
a[IllegalArgumentException] should be thrownBy Duration.parse(":00")
a[IllegalArgumentException] should be thrownBy Duration.parse("1234:00")
a[IllegalArgumentException] should be thrownBy Duration.parse("2:40:15")
Duration.parse("5km") should be(DistanceDuration(5, km))
Duration.parse("10 mi") should be(DistanceDuration(10, mi))
Duration.parse("7.5m") should be(DistanceDuration(7.5f, m))
Duration.parse("1:00") should be(TimeDuration(minutes = 1))
Duration.parse("123:00") should be(TimeDuration(minutes = 123))
}
}

@ -0,0 +1,21 @@
package com.github.mgifos.workouts.model
import com.github.mgifos.workouts.model.DistanceUnits._
import org.scalatest.{ FlatSpec, Matchers }
class StepSpec extends FlatSpec with Matchers {
"Step" should "parse correctly" in {
a[IllegalArgumentException] should be thrownBy Step.parse("")
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("- 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)),
RecoverStep(DistanceDuration(100, m)))))
}
}

@ -0,0 +1,19 @@
package com.github.mgifos.workouts.model
import org.scalatest.{ FlatSpec, Matchers }
class TargetSpec extends FlatSpec with Matchers {
"Target" should "parse correctly" in {
a[IllegalArgumentException] should be thrownBy Target.parse("")
Target.parse("z1") should be(HrZoneTarget(1))
Target.parse("z2") should be(HrZoneTarget(2))
val paceTarget = Target.parse("5:20-04:30").asInstanceOf[PaceTarget]
paceTarget.from.minutes should be(5)
paceTarget.to.seconds should be(30)
paceTarget should be(PaceTarget(Pace("5:20"), Pace("04:30")))
}
}

@ -0,0 +1,32 @@
package com.github.mgifos.workouts.model
import org.scalatest.{ FlatSpec, Matchers }
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: run-fast
- warmup: 10:00
- repeat: 2
- run: 1500m @ z4
- recover: 01:30 @ z2
- 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)))))
}
}
Loading…
Cancel
Save