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.

Commit
d900887ad4933e8b66f5b20de2e01496dd8aa24c
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/core/dune
index 601c9a9d..d0b9624c 100644..100644
@@ -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
index 7c72f0a3..f8fbdfc6 100644..100644
@@ -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
index 3abccf44..368145bf 100644..100644
@@ -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
index e6d53c35..046870a1 100644..100644
@@ -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
index 80c7687a..d2d5333e 100644..100644
@@ -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
index 9f23f6f5..228884fe 100644..100644
@@ -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) ]