[OCaml] High Intensity Training Online
feat implement Workout_prescription
An ordered sequence of prescribed stimuli, identified so a routine can find its place in the cycle. Non-empty by construction; equality is by id, since a renamed or edited workout is still the same slot in the rotation. Volume needs no representation: each prescription is exactly one drive to failure, so the workout's volume is its length. HD1's "least amount required" is therefore structural rather than a quantity to compute or configure. Its prescriptions field replaces the previous set_groups: "set group" is no longer an entity. 44 Alcotests, building HD1's Day 1 — pec pre-exhaust, two delt isolations, triceps pre-exhaust.
Changed files
lib/core/dune
@@ -5,5 +5,5 @@
5
5
(library
6
6
(name hito_core)
7
7
(public_name hito.core)
8
Removed:
(modules units muscle exercise recovery prescription)
8
Added:
(modules units muscle exercise recovery prescription workout_prescription)
9
9
(wrapped false))
lib/core/workout_prescription.ml
@@ -1,13 +1,14 @@
1
Removed:
(* Blank slate: stub only. Interface (workout_prescription.mli) is the design
2
Removed:
surface. *)
3
Removed:
4
1
type id = string
5
Removed:
type t = unit
6
2
type error = Empty_workout
3
Added:
type t = { id : id; name : string; prescriptions : Prescription.t list }
7
4
8
Removed:
let make ~id:_ ~name:_ ~set_groups:_ = failwith "TODO"
9
Removed:
let id _ = failwith "TODO"
10
Removed:
let name _ = failwith "TODO"
11
Removed:
let set_groups _ = failwith "TODO"
12
Removed:
let equal _ _ = failwith "TODO"
13
Removed:
let pp _ _ = failwith "TODO"
5
Added:
let make ~id ~name ~prescriptions =
6
Added:
match prescriptions with
7
Added:
| [] -> Error Empty_workout
8
Added:
| _ -> Ok { id; name; prescriptions }
9
Added:
10
Added:
let id t = t.id
11
Added:
let name t = t.name
12
Added:
let prescriptions t = t.prescriptions
13
Added:
let equal a b = String.equal a.id b.id
14
Added:
let pp ppf t = Format.pp_print_string ppf t.name
lib/core/workout_prescription.mli
@@ -1,7 +1,10 @@
1
Removed:
(** A prescribed workout — one workout within a routine, e.g. HD1's "Workout A":
2
Removed:
an ordered sequence of set-group prescriptions. Static: no targets, no
3
Removed:
history. *)
1
Added:
(** One prescribed workout within a routine, such as HD1's Day 1: an ordered
2
Added:
sequence of prescribed stimuli.
4
3
4
Added:
Each prescription is exactly one drive to failure, so the workout's volume
5
Added:
is its length — HD1's "least amount required" is structural here, never a
6
Added:
quantity to compute or configure. Static: no history, no targets. *)
7
Added:
5
8
type t
6
9
type id = private string
7
10
type error = Empty_workout
@@ -9,17 +12,17 @@
9
12
val make :
10
13
id:string ->
11
14
name:string ->
12
Removed:
set_groups:Set_group_prescription.t list ->
15
Added:
prescriptions:Prescription.t list ->
13
16
(t, error) result
14
Removed:
(** [Error Empty_workout] if [set_groups] is empty. *)
17
Added:
(** [Error Empty_workout] if [prescriptions] is empty. *)
15
18
16
19
val id : t -> id
17
20
val name : t -> string
18
21
19
Removed:
val set_groups : t -> Set_group_prescription.t list
20
Removed:
(** In order; sequence matters for pre-exhaust pairings. *)
22
Added:
val prescriptions : t -> Prescription.t list
23
Added:
(** In performance order. *)
21
24
22
25
val equal : t -> t -> bool
23
Removed:
(** By {!id}. *)
26
Added:
(** By {!id}, which is what lets a routine find its place in the cycle. *)
24
27
25
28
val pp : Format.formatter -> t -> unit
test/dune
@@ -3,5 +3,11 @@
3
3
4
4
(test
5
5
(name test_hito)
6
Removed:
(modules test_hito test_units test_muscle test_exercise test_prescription)
6
Added:
(modules
7
Added:
test_hito
8
Added:
test_units
9
Added:
test_muscle
10
Added:
test_exercise
11
Added:
test_prescription
12
Added:
test_workout_prescription)
7
13
(libraries hito.core alcotest))
test/test_hito.ml
@@ -4,4 +4,4 @@
4
4
let () =
5
5
Alcotest.run "hito"
6
6
(Test_units.suite @ Test_muscle.suite @ Test_exercise.suite
7
Removed:
@ Test_prescription.suite)
7
Added:
@ Test_prescription.suite @ Test_workout_prescription.suite)
test/test_workout_prescription.ml
@@ -1,61 +1,85 @@
1
Removed:
(** Unit tests for {!Workout_prescription}, authored against its interface. *)
1
Added:
(** Unit tests for {!Workout_prescription}. *)
2
2
3
3
let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok"
4
4
5
Removed:
let by_name n =
6
Removed:
match
7
Removed:
List.find_opt (fun ex -> String.equal (Exercise.name ex) n) Exercise.catalog
8
Removed:
with
9
Removed:
| Some ex -> ex
10
Removed:
| None -> Alcotest.failf "no exercise named %s" n
5
Added:
let get id =
6
Added:
match Exercise.find id with
7
Added:
| Some e -> e
8
Added:
| None -> Alcotest.failf "catalog is missing %S" id
11
9
12
Removed:
let band lo hi =
13
Removed:
ok
14
Removed:
(Units.Rep_range.make
15
Removed:
~min:(ok (Units.Reps.of_int lo))
16
Removed:
~max:(ok (Units.Reps.of_int hi)))
10
Added:
let six_to_ten =
11
Added:
let reps n = ok (Units.Reps.of_int n) in
12
Added:
ok (Units.Rep_range.make ~min:(reps 6) ~max:(reps 10))
17
13
18
Removed:
let group name =
19
Removed:
Set_group_prescription.Straight
20
Removed:
(ok
21
Removed:
(Prescription.make ~exercise:(by_name name) ~target_reps:(band 6 8)
22
Removed:
~allowed_substitutes:[]))
14
Added:
let prescribe delivery =
15
Added:
ok (Prescription.make ~delivery ~rep_range:six_to_ten ~allowed_substitutes:[])
23
16
17
Added:
let single id = prescribe (Prescription.Single (get id))
18
Added:
19
Added:
let pre_exhaust ~isolation ~compound =
20
Added:
prescribe
21
Added:
(Prescription.Pre_exhaust
22
Added:
{ isolation = get isolation; compound = get compound })
23
Added:
24
Added:
(* HD1's Day 1: pecs pre-exhaust, two delt isolations, triceps pre-exhaust. *)
25
Added:
let day_one =
26
Added:
[
27
Added:
pre_exhaust ~isolation:"dumbbell-flyes" ~compound:"incline-press";
28
Added:
single "laterals";
29
Added:
single "bent-over-laterals";
30
Added:
pre_exhaust ~isolation:"lying-french-press" ~compound:"dips";
31
Added:
]
32
Added:
24
33
let tests =
25
34
[
26
Removed:
( "make rejects an empty workout",
35
Added:
( "HD1's Day 1 is prescribable, in order",
27
36
`Quick,
28
37
fun () ->
29
Removed:
Alcotest.(check bool)
30
Removed:
"empty rejected" true
31
Removed:
(Result.is_error
32
Removed:
(Workout_prescription.make ~id:"x" ~name:"X" ~set_groups:[])) );
33
Removed:
( "make accepts a non-empty workout and round-trips its fields",
34
Removed:
`Quick,
35
Removed:
fun () ->
36
38
let w =
37
39
ok
38
Removed:
(Workout_prescription.make ~id:"a" ~name:"Workout A"
39
Removed:
~set_groups:[ group "Back Squat"; group "Pulldown" ])
40
Added:
(Workout_prescription.make ~id:"ideal-day-1" ~name:"Day 1"
41
Added:
~prescriptions:day_one)
40
42
in
41
Removed:
Alcotest.(check string) "name" "Workout A" (Workout_prescription.name w);
43
Added:
Alcotest.(check string) "name" "Day 1" (Workout_prescription.name w);
44
Added:
Alcotest.(check string)
45
Added:
"id" "ideal-day-1"
46
Added:
(Workout_prescription.id w :> string);
42
47
Alcotest.(check int)
43
Removed:
"set groups" 2
44
Removed:
(List.length (Workout_prescription.set_groups w)) );
45
Removed:
( "equal compares by id",
48
Added:
"four stimuli" 4
49
Added:
(List.length (Workout_prescription.prescriptions w)) );
50
Added:
( "an empty workout is refused",
46
51
`Quick,
47
52
fun () ->
48
Removed:
let mk id =
53
Added:
match
54
Added:
Workout_prescription.make ~id:"empty" ~name:"Nothing"
55
Added:
~prescriptions:[]
56
Added:
with
57
Added:
| Error Workout_prescription.Empty_workout -> ()
58
Added:
| Ok _ -> Alcotest.fail "expected Empty_workout" );
59
Added:
( "equality is by id, not by content",
60
Added:
`Quick,
61
Added:
fun () ->
62
Added:
let a =
49
63
ok
50
Removed:
(Workout_prescription.make ~id ~name:"n"
51
Removed:
~set_groups:[ group "Deadlift" ])
64
Added:
(Workout_prescription.make ~id:"day-1" ~name:"Day 1"
65
Added:
~prescriptions:day_one)
52
66
in
67
Added:
let renamed =
68
Added:
ok
69
Added:
(Workout_prescription.make ~id:"day-1" ~name:"Renamed"
70
Added:
~prescriptions:[ single "curls" ])
71
Added:
in
72
Added:
let other =
73
Added:
ok
74
Added:
(Workout_prescription.make ~id:"day-2" ~name:"Day 1"
75
Added:
~prescriptions:day_one)
76
Added:
in
53
77
Alcotest.(check bool)
54
Removed:
"same id equal" true
55
Removed:
(Workout_prescription.equal (mk "a") (mk "a"));
78
Added:
"same id" true
79
Added:
(Workout_prescription.equal a renamed);
56
80
Alcotest.(check bool)
57
Removed:
"diff id unequal" false
58
Removed:
(Workout_prescription.equal (mk "a") (mk "b")) );
81
Added:
"different id" false
82
Added:
(Workout_prescription.equal a other) );
59
83
]
60
84
61
85
let suite = [ ("workout_prescription", tests) ]