master
mgifos 8 years ago
parent ffa80c5a74
commit 2fd790e87c
  1. 1
      build.sbt
  2. 53
      src/main/scala/com.github.mgifos.workouts/model/Duration.scala
  3. 48
      src/main/scala/com.github.mgifos.workouts/model/Step.scala
  4. 41
      src/main/scala/com.github.mgifos.workouts/model/Target.scala
  5. 26
      src/main/scala/com.github.mgifos.workouts/model/Workout.scala
  6. 138
      src/test/resources/run-fast.json
  7. 32
      src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala

@ -6,5 +6,6 @@ scalaVersion := "2.12.4"
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"com.github.tototoshi" %% "scala-csv" % "1.3.5", "com.github.tototoshi" %% "scala-csv" % "1.3.5",
"com.typesafe.play" %% "play-json" % "2.6.9",
"org.scalatest" %% "scalatest" % "3.0.5" % "test" "org.scalatest" %% "scalatest" % "3.0.5" % "test"
) )

@ -1,17 +1,57 @@
package com.github.mgifos.workouts.model package com.github.mgifos.workouts.model
import com.github.mgifos.workouts.model.DistanceUnits.DistanceUnit import com.github.mgifos.workouts.model.DistanceUnits.DistanceUnit
import play.api.libs.json.{ JsNull, JsObject, Json }
sealed trait Duration sealed trait Duration {
def json: JsObject
}
case class DistanceDuration(distance: Float, unit: DistanceUnit) extends Duration case class DistanceDuration(distance: Float, unit: DistanceUnit) extends Duration {
override def json: JsObject = Json.obj(
"endCondition" -> Json.obj(
"conditionTypeKey" -> "distance",
"conditionTypeId" -> 3),
"preferredEndConditionUnit" -> Json.obj(
"unitKey" -> unit.fullName),
"endConditionValue" -> unit.toMeters(distance),
"endConditionCompare" -> JsNull,
"endConditionZone" -> JsNull)
}
object DistanceUnits extends Enumeration { object DistanceUnits extends Enumeration {
type DistanceUnit = Value type DistanceUnit = DistVal
val km, mi, m = Value val km = Value("km", "kilometer", _ * 1000F)
val mi = Value("mi", "mile", _ * 1609.344F)
val m = Value("m", "meter", _ * 1F)
class DistVal(name: String, val fullName: String, val toMeters: (Float) => Float) extends Val(nextId, name)
protected final def Value(name: String, fullName: String, toMeters: (Float) => Float): DistVal = new DistVal(name, fullName, toMeters)
def named(name: String): DistVal = withName(name).asInstanceOf[DistVal]
} }
case class TimeDuration(hours: Int = 0, minutes: Int = 0, seconds: Int = 0) extends Duration case class TimeDuration(minutes: Int = 0, seconds: Int = 0) extends Duration {
override def json: JsObject = Json.obj(
"endCondition" -> Json.obj(
"conditionTypeKey" -> "time",
"conditionTypeId" -> 2),
"preferredEndConditionUnit" -> JsNull,
"endConditionValue" -> (minutes * 60 + seconds),
"endConditionCompare" -> JsNull,
"endConditionZone" -> JsNull)
}
object LapButtonPressed extends Duration {
override def json: JsObject = Json.obj(
"endCondition" -> Json.obj(
"conditionTypeKey" -> "lap.button",
"conditionTypeId" -> 1),
"preferredEndConditionUnit" -> JsNull,
"endConditionValue" -> JsNull,
"endConditionCompare" -> JsNull,
"endConditionZone" -> JsNull)
}
object Duration { object Duration {
@ -19,8 +59,9 @@ object Duration {
private val MinutesRx = """^(\d{1,3}):(\d{2})$""".r private val MinutesRx = """^(\d{1,3}):(\d{2})$""".r
def parse(x: String): Duration = x match { def parse(x: String): Duration = x match {
case DistanceRx(quantity, _, unit) => DistanceDuration(quantity.toFloat, DistanceUnits.withName(unit)) case DistanceRx(quantity, _, unit) => DistanceDuration(quantity.toFloat, DistanceUnits.named(unit))
case MinutesRx(minutes, seconds) => TimeDuration(minutes = minutes.toInt, seconds = seconds.toInt) case MinutesRx(minutes, seconds) => TimeDuration(minutes = minutes.toInt, seconds = seconds.toInt)
case "lap-button" => LapButtonPressed
case _ => throw new IllegalArgumentException(s"Duration cannot be parsed $x") case _ => throw new IllegalArgumentException(s"Duration cannot be parsed $x")
} }
} }

@ -1,30 +1,60 @@
package com.github.mgifos.workouts.model package com.github.mgifos.workouts.model
sealed trait Step { import play.api.libs.json.{ JsNull, JsObject, JsValue, Json }
trait Step {
def `type`: String def `type`: String
def typeId: Int
def json(order: Int): JsValue
} }
abstract class DurationStep(stepType: String, duration: Duration, target: Option[Target] = None) extends Step { abstract class DurationStep(
override def `type` = stepType override val `type`: String,
override val typeId: Int) extends Step {
def duration: Duration
def target: Option[Target]
def json(order: Int): JsObject = Json.obj(
"type" -> "ExecutableStepDTO",
"stepId" -> JsNull,
"stepOrder" -> order,
"childStepId" -> JsNull,
"description" -> JsNull,
"stepType" -> Json.obj(
"stepTypeId" -> typeId,
"stepTypeKey" -> `type`)) ++ duration.json ++ target.fold(NoTarget.json)(_.json)
} }
case class WarmupStep(duration: Duration, target: Option[Target] = None) extends DurationStep("warmup", duration) case class WarmupStep(duration: Duration, target: Option[Target] = None) extends DurationStep("warmup", 1)
case class RunStep(duration: Duration, target: Option[Target] = None) extends DurationStep("run", duration) case class CooldownStep(duration: Duration, target: Option[Target] = None) extends DurationStep("cooldown", 2)
case class RecoverStep(duration: Duration, target: Option[Target] = None) extends DurationStep("recover", duration) case class RunStep(duration: Duration, target: Option[Target] = None) extends DurationStep("interval", 3)
case class CooldownStep(duration: Duration, target: Option[Target] = None) extends DurationStep("cooldown", duration) case class RecoverStep(duration: Duration, target: Option[Target] = None) extends DurationStep("recovery", 4)
case class RepeatStep(count: Int, definition: Seq[DurationStep]) extends Step { case class RepeatStep(count: Int, steps: Seq[Step]) extends Step {
override def `type` = "repeat" override def `type` = "repeat"
override def typeId = 6
override def json(order: Int) = Json.obj(
"stepId" -> JsNull,
"stepOrder" -> 2,
"stepType" -> Json.obj(
"stepTypeId" -> 6,
"stepTypeKey" -> "repeat"),
"numberOfIterations" -> count,
"smartRepeat" -> false,
"childStepId" -> 1,
"workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) },
"type" -> "RepeatGroupDTO")
} }
object Step { object Step {
private val StepRx = """^(-\s\w*:\s.*)((\n\s{1,}-\s.*)*)$""".r private val StepRx = """^(-\s\w*:\s.*)((\n\s{1,}-\s.*)*)$""".r
private val StepHeader = """^\s*-\s*(\w*):(.*)$""".r private val StepHeader = """^\s*-\s*(\w*):(.*)$""".r
private val ParamsRx = """^([\w\.:\s]+)\s*(@(.*))?$""".r private val ParamsRx = """^([\w-\.:\s]+)\s*(@(.*))?$""".r
def parse(x: String): Step = x match { def parse(x: String): Step = x match {
case StepRx(header, subSteps, _) if subSteps.nonEmpty => header match { case StepRx(header, subSteps, _) if subSteps.nonEmpty => header match {

@ -1,13 +1,48 @@
package com.github.mgifos.workouts.model package com.github.mgifos.workouts.model
sealed trait Target import play.api.libs.json.{ JsNull, JsObject, Json }
case class HrZoneTarget(zone: Int) extends Target sealed trait Target {
case class PaceTarget(from: Pace, to: Pace) extends Target def json: JsObject
}
case class HrZoneTarget(zone: Int) extends Target {
override def json = Json.obj(
"targetType" -> Json.obj(
"workoutTargetTypeId" -> 4,
"workoutTargetTypeKey" -> "heart.rate.zone"),
"targetValueOne" -> "",
"targetValueTwo" -> "",
"zoneNumber" -> zone.toString)
}
case class PaceTarget(from: Pace, to: Pace) extends Target {
override def json = Json.obj(
"targetType" -> Json.obj(
"workoutTargetTypeId" -> 6,
"workoutTargetTypeKey" -> "pace.zone"),
"targetValueOne" -> from.speed,
"targetValueTwo" -> to.speed,
"zoneNumber" -> JsNull)
}
object NoTarget extends Target {
override def json = Json.obj(
"targetType" -> Json.obj(
"workoutTargetTypeId" -> 1,
"workoutTargetTypeKey" -> "no.target"),
"targetValueOne" -> JsNull,
"targetValueTwo" -> JsNull,
"zoneNumber" -> JsNull)
}
case class Pace(exp: String) { case class Pace(exp: String) {
def minutes: Int = exp.trim.takeWhile(_ != ':').toInt def minutes: Int = exp.trim.takeWhile(_ != ':').toInt
def seconds: Int = exp.trim.split(":").last.toInt def seconds: Int = exp.trim.split(":").last.toInt
/**
* @return Speed in m/s
*/
def speed: Double = 1000D / (minutes * 60 + seconds)
} }
object Target { object Target {

@ -1,15 +1,35 @@
package com.github.mgifos.workouts.model package com.github.mgifos.workouts.model
trait Workout import play.api.libs.json.{ JsValue, Json }
trait Workout {
def json: JsValue
}
case class WorkoutDef(name: String, steps: Seq[Step] = Nil) extends Workout { case class WorkoutDef(name: String, steps: Seq[Step] = Nil) extends Workout {
def toRef: WorkoutRef = WorkoutRef(name) def toRef: WorkoutRef = WorkoutRef(name)
def withStep(step: Step): WorkoutDef = WorkoutDef(name, steps :+ step) def withStep(step: Step): WorkoutDef = WorkoutDef(name, steps :+ step)
def json: JsValue = Json.obj(
"sportType" -> Json.obj(
"sportTypeId" -> 1,
"sportTypeKey" -> "running"),
"workoutName" -> name,
"workoutSegments" -> Json.arr(
Json.obj(
"segmentOrder" -> 1,
"sportType" -> Json.obj(
"sportTypeId" -> 1,
"sportTypeKey" -> "running"),
"workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) })))
} }
case class WorkoutRef(name: String) extends Workout case class WorkoutRef(name: String) extends Workout {
def json: JsValue = Json.obj()
}
case class WorkoutNote(note: String) extends Workout case class WorkoutNote(note: String) extends Workout {
def json: JsValue = Json.obj()
}
object Workout { object Workout {

@ -0,0 +1,138 @@
{
"sportType": {
"sportTypeId": 1,
"sportTypeKey": "running"
},
"workoutName": "run-fast",
"workoutSegments": [
{
"segmentOrder": 1,
"sportType": {
"sportTypeId": 1,
"sportTypeKey": "running"
},
"workoutSteps": [
{
"type": "ExecutableStepDTO",
"stepId": null,
"stepOrder": 1,
"childStepId": null,
"description": null,
"stepType": {
"stepTypeId": 1,
"stepTypeKey": "warmup"
},
"endCondition": {
"conditionTypeKey": "time",
"conditionTypeId": 2
},
"preferredEndConditionUnit": null,
"endConditionValue": 600,
"endConditionCompare": null,
"endConditionZone": null,
"targetType": {
"workoutTargetTypeId": 1,
"workoutTargetTypeKey": "no.target"
},
"targetValueOne": null,
"targetValueTwo": null,
"zoneNumber": null
},
{
"stepId": null,
"stepOrder": 2,
"stepType": {
"stepTypeId": 6,
"stepTypeKey": "repeat"
},
"numberOfIterations": 2,
"smartRepeat": false,
"childStepId": 1,
"workoutSteps": [
{
"type": "ExecutableStepDTO",
"stepId": null,
"stepOrder": 1,
"childStepId": null,
"description": null,
"stepType": {
"stepTypeId": 3,
"stepTypeKey": "interval"
},
"endCondition": {
"conditionTypeKey": "distance",
"conditionTypeId": 3
},
"preferredEndConditionUnit": {
"unitKey": "meter"
},
"endConditionValue": 1500,
"endConditionCompare": null,
"endConditionZone": null,
"targetType": {
"workoutTargetTypeId": 6,
"workoutTargetTypeKey": "pace.zone"
},
"targetValueOne": 3.7037037037037037,
"targetValueTwo": 3.3333333333333335,
"zoneNumber": null
},
{
"type": "ExecutableStepDTO",
"stepId": null,
"stepOrder": 2,
"childStepId": null,
"description": null,
"stepType": {
"stepTypeId": 4,
"stepTypeKey": "recovery"
},
"endCondition": {
"conditionTypeKey": "time",
"conditionTypeId": 2
},
"preferredEndConditionUnit": null,
"endConditionValue": 90,
"endConditionCompare": null,
"endConditionZone": null,
"targetType": {
"workoutTargetTypeId": 4,
"workoutTargetTypeKey": "heart.rate.zone"
},
"targetValueOne": "",
"targetValueTwo": "",
"zoneNumber": "2"
}
],
"type": "RepeatGroupDTO"
},
{
"type": "ExecutableStepDTO",
"stepId": null,
"stepOrder": 3,
"childStepId": null,
"description": null,
"stepType": {
"stepTypeId": 2,
"stepTypeKey": "cooldown"
},
"endCondition": {
"conditionTypeKey": "lap.button",
"conditionTypeId": 1
},
"preferredEndConditionUnit": null,
"endConditionValue": null,
"endConditionCompare": null,
"endConditionZone": null,
"targetType": {
"workoutTargetTypeId": 1,
"workoutTargetTypeKey": "no.target"
},
"targetValueOne": null,
"targetValueTwo": null,
"zoneNumber": null
}
]
}
]
}

@ -2,9 +2,20 @@ package com.github.mgifos.workouts.model
import org.scalatest.{ FlatSpec, Matchers } import org.scalatest.{ FlatSpec, Matchers }
import com.github.mgifos.workouts.model.DistanceUnits._ import com.github.mgifos.workouts.model.DistanceUnits._
import play.api.libs.json.Json
class WorkoutSpec extends FlatSpec with Matchers { class WorkoutSpec extends FlatSpec with Matchers {
/*
workout: 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"
"Workout" should "parse correctly" in { "Workout" should "parse correctly" in {
Workout.parseDef("") should be('left) Workout.parseDef("") should be('left)
Workout.parseDef("workout") should be('left) Workout.parseDef("workout") should be('left)
@ -13,22 +24,19 @@ class WorkoutSpec extends FlatSpec with Matchers {
Workout.parseDef("workout run-fast") should be('left) Workout.parseDef("workout run-fast") should be('left)
Workout.parseDef(" workout: run-fast") should be('left) Workout.parseDef(" workout: run-fast") should be('left)
/*
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.parseDef(testWO) should be( Workout.parseDef(testWO) should be(
Right( Right(
WorkoutDef("run-fast", Seq( WorkoutDef("run-fast", Seq(
WarmupStep(TimeDuration(minutes = 10)), WarmupStep(TimeDuration(minutes = 10)),
RepeatStep(2, Seq( RepeatStep(2, Seq(
RunStep(DistanceDuration(1500, m), Some(HrZoneTarget(4))), RunStep(DistanceDuration(1500, m), Some(PaceTarget(Pace("4:30"), Pace("5:00")))),
RecoverStep(TimeDuration(0, 1, 30), Some(HrZoneTarget(2))))), RecoverStep(TimeDuration(1, 30), Some(HrZoneTarget(2))))),
CooldownStep(TimeDuration(minutes = 5)))))) CooldownStep(LapButtonPressed)))))
}
"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))
} }
} }

Loading…
Cancel
Save