max / ripgrow
18 files changed,
+389 insertions,
-91 deletions
| @@ -10,33 +10,14 @@ use std::path::Path; | |||
| 10 | 10 | use rusqlite::{Connection, OptionalExtension, params}; | |
| 11 | 11 | ||
| 12 | 12 | use crate::error::Error; | |
| 13 | + | use crate::values::LoadUnit; | |
| 13 | 14 | ||
| 14 | 15 | const MIGRATIONS: &[&str] = &[MIGRATION_001, MIGRATION_002]; | |
| 15 | 16 | ||
| 16 | - | /// Unit preference for a profile. Stored as `'kg'` or `'lb'`; the choice is | |
| 17 | - | /// made at profile-create time and hard to change safely later. | |
| 18 | - | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 19 | - | pub enum Unit { | |
| 20 | - | Kg, | |
| 21 | - | Lb, | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | impl Unit { | |
| 25 | - | pub fn as_str(&self) -> &'static str { | |
| 26 | - | match self { | |
| 27 | - | Unit::Kg => "kg", | |
| 28 | - | Unit::Lb => "lb", | |
| 29 | - | } | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | pub fn parse(s: &str) -> Result<Self, Error> { | |
| 33 | - | match s { | |
| 34 | - | "kg" => Ok(Unit::Kg), | |
| 35 | - | "lb" => Ok(Unit::Lb), | |
| 36 | - | other => Err(Error::InvalidUnit(other.to_string())), | |
| 37 | - | } | |
| 38 | - | } | |
| 39 | - | } | |
| 17 | + | // Historically `Unit` lived here as the profile's weight-unit preference. | |
| 18 | + | // It moved into `values` as `LoadUnit` (same type, better name — it also | |
| 19 | + | // describes each exercise's load column). Older imports still get `Unit` | |
| 20 | + | // via `crate::Unit`. | |
| 40 | 21 | ||
| 41 | 22 | pub struct Db { | |
| 42 | 23 | conn: Connection, | |
| @@ -67,7 +48,7 @@ impl Db { | |||
| 67 | 48 | ||
| 68 | 49 | /// Insert the `meta` row on a freshly-created profile. Idempotent guard: | |
| 69 | 50 | /// returns Ok without touching the row if one already exists. | |
| 70 | - | pub fn init_profile(&self, profile_name: &str, unit: Unit) -> Result<(), Error> { | |
| 51 | + | pub fn init_profile(&self, profile_name: &str, unit: LoadUnit) -> Result<(), Error> { | |
| 71 | 52 | let exists: bool = self | |
| 72 | 53 | .conn | |
| 73 | 54 | .query_row("SELECT 1 FROM meta LIMIT 1", [], |_| Ok(true)) | |
| @@ -91,12 +72,12 @@ impl Db { | |||
| 91 | 72 | .optional()?) | |
| 92 | 73 | } | |
| 93 | 74 | ||
| 94 | - | pub fn unit(&self) -> Result<Option<Unit>, Error> { | |
| 75 | + | pub fn unit(&self) -> Result<Option<LoadUnit>, Error> { | |
| 95 | 76 | let s: Option<String> = self | |
| 96 | 77 | .conn | |
| 97 | 78 | .query_row("SELECT unit FROM meta LIMIT 1", [], |row| row.get(0)) | |
| 98 | 79 | .optional()?; | |
| 99 | - | s.map(|v| Unit::parse(&v)).transpose() | |
| 80 | + | s.map(|v| LoadUnit::parse(&v)).transpose() | |
| 100 | 81 | } | |
| 101 | 82 | ||
| 102 | 83 | pub fn conn(&self) -> &Connection { | |
| @@ -205,16 +186,16 @@ mod tests { | |||
| 205 | 186 | #[test] | |
| 206 | 187 | fn init_profile_writes_meta_row_and_is_idempotent() { | |
| 207 | 188 | let db = Db::open_in_memory().unwrap(); | |
| 208 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 209 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 189 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 190 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 210 | 191 | assert_eq!(db.profile_name().unwrap().as_deref(), Some("self")); | |
| 211 | - | assert_eq!(db.unit().unwrap(), Some(Unit::Kg)); | |
| 192 | + | assert_eq!(db.unit().unwrap(), Some(LoadUnit::Kg)); | |
| 212 | 193 | } | |
| 213 | 194 | ||
| 214 | 195 | #[test] | |
| 215 | 196 | fn rpe_check_constraint_rejects_out_of_range() { | |
| 216 | 197 | let db = Db::open_in_memory().unwrap(); | |
| 217 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 198 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 218 | 199 | db.conn | |
| 219 | 200 | .execute( | |
| 220 | 201 | "INSERT INTO exercises (name, resistance_type, load_unit, increment) \ | |
| @@ -244,7 +225,7 @@ mod tests { | |||
| 244 | 225 | #[test] | |
| 245 | 226 | fn sets_unique_constraint_on_session_exercise_set() { | |
| 246 | 227 | let db = Db::open_in_memory().unwrap(); | |
| 247 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 228 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 248 | 229 | db.conn | |
| 249 | 230 | .execute( | |
| 250 | 231 | "INSERT INTO exercises (name, resistance_type, load_unit, increment) \ |
| @@ -29,6 +29,12 @@ pub enum Error { | |||
| 29 | 29 | #[error("reps must be non-negative, got {0}")] | |
| 30 | 30 | InvalidReps(i32), | |
| 31 | 31 | ||
| 32 | + | #[error("load must be a finite non-negative number, got {0}")] | |
| 33 | + | InvalidLoad(f64), | |
| 34 | + | ||
| 35 | + | #[error("increment must be a finite non-negative number, got {0}")] | |
| 36 | + | InvalidIncrement(f64), | |
| 37 | + | ||
| 32 | 38 | #[error("could not parse {field}: {value:?}")] | |
| 33 | 39 | ParseField { field: &'static str, value: String }, | |
| 34 | 40 | } |
| @@ -242,7 +242,7 @@ impl Db { | |||
| 242 | 242 | #[cfg(test)] | |
| 243 | 243 | mod tests { | |
| 244 | 244 | use super::*; | |
| 245 | - | use crate::db::Unit; | |
| 245 | + | use crate::values::LoadUnit; | |
| 246 | 246 | use crate::templates::ResistanceType; | |
| 247 | 247 | ||
| 248 | 248 | fn ex(id: i64, name: &str, tags: Vec<i64>) -> Exercise { | |
| @@ -377,7 +377,7 @@ mod tests { | |||
| 377 | 377 | ||
| 378 | 378 | fn setup() -> Db { | |
| 379 | 379 | let db = Db::open_in_memory().unwrap(); | |
| 380 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 380 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 381 | 381 | db | |
| 382 | 382 | } | |
| 383 | 383 |
| @@ -0,0 +1,116 @@ | |||
| 1 | + | //! Every tunable number in ripgrow that isn't a measurement. | |
| 2 | + | //! | |
| 3 | + | //! Progression, generation, diagnostic escalation, and the e1RM estimator | |
| 4 | + | //! all sit on top of policy choices — ratios, formulas, and mappings we | |
| 5 | + | //! wrote down without a randomized-controlled-trial to back them up. | |
| 6 | + | //! Putting them in one file makes them auditable ("show me every | |
| 7 | + | //! heuristic in this app": `grep heuristics`), and makes each one | |
| 8 | + | //! findable by name in the modules that use it instead of hiding as a | |
| 9 | + | //! literal in a match arm. | |
| 10 | + | //! | |
| 11 | + | //! When you change a number here, write down why in its docstring, and | |
| 12 | + | //! make sure the corresponding module's tests still describe the intent. | |
| 13 | + | ||
| 14 | + | use crate::values::Rpe; | |
| 15 | + | ||
| 16 | + | // ---- progression state machine -------------------------------------------- | |
| 17 | + | ||
| 18 | + | /// Fraction of the previous top load applied when transitioning INTO | |
| 19 | + | /// Deloading. 0.9 is the number most strength coaches use as a rule of | |
| 20 | + | /// thumb; there's no principled reason it isn't 0.85 or 0.92. Kept | |
| 21 | + | /// aligned with `DIAGNOSTIC_WORKING_WEIGHT_RATIO` so a fresh calibration | |
| 22 | + | /// and a post-deload rebuild converge on the same starting point. | |
| 23 | + | pub const DELOAD_RATIO: f64 = 0.9; | |
| 24 | + | ||
| 25 | + | // ---- diagnostic escalation ------------------------------------------------ | |
| 26 | + | ||
| 27 | + | /// Multiplier for the next diagnostic set when the last one felt easy | |
| 28 | + | /// (RPE 1 or 2). 15% jumps are aggressive; the goal is to reach a | |
| 29 | + | /// meaningful top set in a small number of attempts, not to be gentle. | |
| 30 | + | pub const DIAGNOSTIC_EASY_STEP_UP: f64 = 1.15; | |
| 31 | + | ||
| 32 | + | /// Multiplier when the last diagnostic set was moderate (RPE 3). Half | |
| 33 | + | /// the easy jump; there's no data behind this specific value. | |
| 34 | + | pub const DIAGNOSTIC_MEDIUM_STEP_UP: f64 = 1.075; | |
| 35 | + | ||
| 36 | + | /// Fraction of the diagnostic's top load taken as the first working | |
| 37 | + | /// weight. Same value as `DELOAD_RATIO` on purpose. | |
| 38 | + | pub const DIAGNOSTIC_WORKING_WEIGHT_RATIO: f64 = 0.9; | |
| 39 | + | ||
| 40 | + | /// Safety cap on diagnostic set count so a stream of RPE 1s doesn't | |
| 41 | + | /// escalate forever. | |
| 42 | + | pub const DIAGNOSTIC_MAX_SETS: usize = 8; | |
| 43 | + | ||
| 44 | + | /// Target reps per diagnostic set. Fixed at 5; the diagnostic is a | |
| 45 | + | /// probe, not a hypertrophy session. | |
| 46 | + | pub const DIAGNOSTIC_TARGET_REPS: i32 = 5; | |
| 47 | + | ||
| 48 | + | // ---- generation scoring --------------------------------------------------- | |
| 49 | + | ||
| 50 | + | /// Points added per tag marked Ready. | |
| 51 | + | pub const READY_TAG_BONUS: f64 = 1.0; | |
| 52 | + | ||
| 53 | + | /// Points subtracted per tag marked Tired. | |
| 54 | + | pub const TIRED_TAG_PENALTY: f64 = 1.0; | |
| 55 | + | ||
| 56 | + | /// Days since last performed / this = recency bonus, capped by | |
| 57 | + | /// `RECENCY_BONUS_CAP`. Weekly cadence assumption. | |
| 58 | + | pub const RECENCY_BONUS_DIVISOR_DAYS: f64 = 7.0; | |
| 59 | + | ||
| 60 | + | /// Upper bound on the recency bonus. Also the value awarded to | |
| 61 | + | /// never-done exercises so novelty is preferred over stale rotations. | |
| 62 | + | pub const RECENCY_BONUS_CAP: f64 = 3.0; | |
| 63 | + | ||
| 64 | + | /// How much each tag's contribution is dampened after being picked. The | |
| 65 | + | /// scaling factor is `1 / (1 + saturation)`, so `1.0` here means the | |
| 66 | + | /// second occurrence of a tag is worth half as much. | |
| 67 | + | pub const SATURATION_STEP: f64 = 1.0; | |
| 68 | + | ||
| 69 | + | // ---- e1RM estimator ------------------------------------------------------- | |
| 70 | + | ||
| 71 | + | /// Divisor in the Epley-style formula | |
| 72 | + | /// `load * (1 + (reps + rir) / EPLEY_DIVISOR)`. Boyd Epley's original | |
| 73 | + | /// value from 1985. Not backed by an ripgrow-specific fit. | |
| 74 | + | pub const EPLEY_DIVISOR: f64 = 30.0; | |
| 75 | + | ||
| 76 | + | /// Label carried on every `Estimate` produced by the e1RM helper. If we | |
| 77 | + | /// swap the estimator, updating this label is a required part of the | |
| 78 | + | /// change so downstream UI never lies about provenance. | |
| 79 | + | pub const E1RM_METHOD_LABEL: &str = "epley on reps + rir (from rpe)"; | |
| 80 | + | ||
| 81 | + | /// Reps in reserve implied by a self-reported RPE. This mapping is a | |
| 82 | + | /// policy choice, not a fact: RPE 5 is called "maximum effort" but a | |
| 83 | + | /// lifter with an inflated sense of ceiling might miss the rep at RPE 5 | |
| 84 | + | /// (0 RIR) while another leaves 1 rep in the tank. Kept simple on | |
| 85 | + | /// purpose; a per-user calibration would go here. | |
| 86 | + | /// | |
| 87 | + | /// `None` for RPE 1 (warmup) — a warmup set carries no signal about the | |
| 88 | + | /// ceiling. | |
| 89 | + | pub fn rir_from_rpe(rpe: Rpe) -> Option<i32> { | |
| 90 | + | match rpe.get() { | |
| 91 | + | 5 => Some(0), | |
| 92 | + | 4 => Some(1), | |
| 93 | + | 3 => Some(2), | |
| 94 | + | 2 => Some(3), | |
| 95 | + | _ => None, | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | #[cfg(test)] | |
| 100 | + | mod tests { | |
| 101 | + | use super::*; | |
| 102 | + | ||
| 103 | + | #[test] | |
| 104 | + | fn rir_map_covers_two_through_five() { | |
| 105 | + | assert_eq!(rir_from_rpe(Rpe::new(5).unwrap()), Some(0)); | |
| 106 | + | assert_eq!(rir_from_rpe(Rpe::new(4).unwrap()), Some(1)); | |
| 107 | + | assert_eq!(rir_from_rpe(Rpe::new(3).unwrap()), Some(2)); | |
| 108 | + | assert_eq!(rir_from_rpe(Rpe::new(2).unwrap()), Some(3)); | |
| 109 | + | assert_eq!(rir_from_rpe(Rpe::new(1).unwrap()), None); | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | #[test] | |
| 113 | + | fn deload_and_diagnostic_ratios_agree() { | |
| 114 | + | assert_eq!(DELOAD_RATIO, DIAGNOSTIC_WORKING_WEIGHT_RATIO); | |
| 115 | + | } | |
| 116 | + | } |
| @@ -8,19 +8,23 @@ pub mod diagnostic; | |||
| 8 | 8 | pub mod error; | |
| 9 | 9 | pub mod estimator; | |
| 10 | 10 | pub mod generation; | |
| 11 | + | pub mod heuristics; | |
| 11 | 12 | pub mod profiles; | |
| 12 | 13 | pub mod progression; | |
| 13 | 14 | pub mod seed; | |
| 14 | 15 | pub mod sets; | |
| 15 | 16 | pub mod templates; | |
| 17 | + | pub mod values; | |
| 16 | 18 | ||
| 17 | - | pub use db::{Db, Unit}; | |
| 19 | + | pub use db::Db; | |
| 18 | 20 | pub use diagnostic::{DiagnosticSet, DiagnosticStep, next_step as next_diagnostic_step}; | |
| 19 | 21 | pub use error::Error; | |
| 20 | - | pub use estimator::{estimate_e1rm, rir_from_rpe}; | |
| 22 | + | pub use estimator::estimate_e1rm; | |
| 21 | 23 | pub use generation::{PickedSlot, Readiness}; | |
| 24 | + | pub use heuristics::rir_from_rpe; | |
| 22 | 25 | pub use profiles::{Profile, create_profile_in, list_profiles, profiles_dir, slugify}; | |
| 23 | 26 | pub use progression::{Prescription, PrescriptionResult, SessionOutcome, State}; | |
| 24 | 27 | pub use seed::seed_starter_content; | |
| 25 | 28 | pub use sets::SessionSet; | |
| 26 | 29 | pub use templates::{Exercise, ResistanceType, Tag}; | |
| 30 | + | pub use values::{Estimate, Increment, Load, LoadUnit, Reps, Rpe}; |
| @@ -9,7 +9,8 @@ | |||
| 9 | 9 | use std::fs; | |
| 10 | 10 | use std::path::{Path, PathBuf}; | |
| 11 | 11 | ||
| 12 | - | use crate::db::{Db, Unit}; | |
| 12 | + | use crate::db::Db; | |
| 13 | + | use crate::values::LoadUnit; | |
| 13 | 14 | use crate::error::Error; | |
| 14 | 15 | ||
| 15 | 16 | /// Return the profiles directory, creating it if missing. | |
| @@ -94,7 +95,7 @@ pub fn list_profiles_in(dir: &Path) -> Result<Vec<Profile>, Error> { | |||
| 94 | 95 | pub fn create_profile_in( | |
| 95 | 96 | dir: &Path, | |
| 96 | 97 | display_name: &str, | |
| 97 | - | unit: Unit, | |
| 98 | + | unit: LoadUnit, | |
| 98 | 99 | ) -> Result<(Profile, Db), Error> { | |
| 99 | 100 | let slug = slugify(display_name); | |
| 100 | 101 | let path = dir.join(format!("{slug}.db")); | |
| @@ -137,7 +138,7 @@ mod tests { | |||
| 137 | 138 | #[test] | |
| 138 | 139 | fn create_and_list_round_trip() { | |
| 139 | 140 | let t = tmp(); | |
| 140 | - | let (p, _db) = create_profile_in(t.path(), "self", Unit::Kg).unwrap(); | |
| 141 | + | let (p, _db) = create_profile_in(t.path(), "self", LoadUnit::Kg).unwrap(); | |
| 141 | 142 | assert_eq!(p.slug, "self"); | |
| 142 | 143 | let listed = list_profiles_in(t.path()).unwrap(); | |
| 143 | 144 | assert_eq!(listed.len(), 1); | |
| @@ -147,16 +148,16 @@ mod tests { | |||
| 147 | 148 | #[test] | |
| 148 | 149 | fn create_profile_writes_meta_row() { | |
| 149 | 150 | let t = tmp(); | |
| 150 | - | let (_, db) = create_profile_in(t.path(), "Jane Doe", Unit::Lb).unwrap(); | |
| 151 | + | let (_, db) = create_profile_in(t.path(), "Jane Doe", LoadUnit::Lb).unwrap(); | |
| 151 | 152 | assert_eq!(db.profile_name().unwrap().as_deref(), Some("Jane Doe")); | |
| 152 | - | assert_eq!(db.unit().unwrap(), Some(Unit::Lb)); | |
| 153 | + | assert_eq!(db.unit().unwrap(), Some(LoadUnit::Lb)); | |
| 153 | 154 | } | |
| 154 | 155 | ||
| 155 | 156 | #[test] | |
| 156 | 157 | fn create_profile_rejects_collision() { | |
| 157 | 158 | let t = tmp(); | |
| 158 | - | create_profile_in(t.path(), "self", Unit::Kg).unwrap(); | |
| 159 | - | let err = create_profile_in(t.path(), "SELF", Unit::Kg); | |
| 159 | + | create_profile_in(t.path(), "self", LoadUnit::Kg).unwrap(); | |
| 160 | + | let err = create_profile_in(t.path(), "SELF", LoadUnit::Kg); | |
| 160 | 161 | assert!(err.is_err(), "second create with same slug must error"); | |
| 161 | 162 | } | |
| 162 | 163 | ||
| @@ -165,7 +166,7 @@ mod tests { | |||
| 165 | 166 | let t = tmp(); | |
| 166 | 167 | std::fs::write(t.path().join("notes.txt"), b"x").unwrap(); | |
| 167 | 168 | std::fs::write(t.path().join("readme"), b"x").unwrap(); | |
| 168 | - | create_profile_in(t.path(), "self", Unit::Kg).unwrap(); | |
| 169 | + | create_profile_in(t.path(), "self", LoadUnit::Kg).unwrap(); | |
| 169 | 170 | let listed = list_profiles_in(t.path()).unwrap(); | |
| 170 | 171 | assert_eq!(listed.len(), 1); | |
| 171 | 172 | assert_eq!(listed[0].slug, "self"); |
| @@ -352,13 +352,13 @@ impl Db { | |||
| 352 | 352 | #[cfg(test)] | |
| 353 | 353 | mod db_tests { | |
| 354 | 354 | use super::*; | |
| 355 | - | use crate::db::Unit; | |
| 355 | + | use crate::values::LoadUnit; | |
| 356 | 356 | use crate::templates::ResistanceType; | |
| 357 | 357 | use chrono::NaiveDate; | |
| 358 | 358 | ||
| 359 | 359 | fn setup() -> (Db, i64) { | |
| 360 | 360 | let db = Db::open_in_memory().unwrap(); | |
| 361 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 361 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 362 | 362 | let id = db | |
| 363 | 363 | .create_exercise("squat", ResistanceType::Freeweight, "kg", 2.5, &[]) | |
| 364 | 364 | .unwrap(); |
| @@ -4,7 +4,8 @@ | |||
| 4 | 4 | //! first open. The user is free to edit or delete anything here; the seed | |
| 5 | 5 | //! runs once at profile creation and is never re-applied. | |
| 6 | 6 | ||
| 7 | - | use crate::db::{Db, Unit}; | |
| 7 | + | use crate::db::Db; | |
| 8 | + | use crate::values::LoadUnit; | |
| 8 | 9 | use crate::error::Error; | |
| 9 | 10 | use crate::templates::ResistanceType; | |
| 10 | 11 | ||
| @@ -146,8 +147,8 @@ const SEED_EXERCISES: &[SeedExercise] = &[ | |||
| 146 | 147 | /// profile. Idempotent: pre-existing tags are reused via `upsert_tag`; | |
| 147 | 148 | /// pre-existing exercises by name are skipped (`UNIQUE` constraint on | |
| 148 | 149 | /// `exercises.name` would otherwise error). Load unit and increment are | |
| 149 | - | /// derived from the profile's `Unit`. | |
| 150 | - | pub fn seed_starter_content(db: &Db, unit: Unit) -> Result<(), Error> { | |
| 150 | + | /// derived from the profile's `LoadUnit`. | |
| 151 | + | pub fn seed_starter_content(db: &Db, unit: LoadUnit) -> Result<(), Error> { | |
| 151 | 152 | for tag in SEED_TAGS { | |
| 152 | 153 | db.upsert_tag(tag)?; | |
| 153 | 154 | } | |
| @@ -174,17 +175,17 @@ pub fn seed_starter_content(db: &Db, unit: Unit) -> Result<(), Error> { | |||
| 174 | 175 | Ok(()) | |
| 175 | 176 | } | |
| 176 | 177 | ||
| 177 | - | fn increment_for(rt: ResistanceType, unit: Unit) -> f64 { | |
| 178 | + | fn increment_for(rt: ResistanceType, unit: LoadUnit) -> f64 { | |
| 178 | 179 | match rt { | |
| 179 | 180 | ResistanceType::Bodyweight => 0.0, | |
| 180 | 181 | ResistanceType::CardioTime | ResistanceType::CardioDistance => 0.0, | |
| 181 | 182 | ResistanceType::Freeweight => match unit { | |
| 182 | - | Unit::Kg => 2.5, | |
| 183 | - | Unit::Lb => 5.0, | |
| 183 | + | LoadUnit::Kg => 2.5, | |
| 184 | + | LoadUnit::Lb => 5.0, | |
| 184 | 185 | }, | |
| 185 | 186 | ResistanceType::Machine => match unit { | |
| 186 | - | Unit::Kg => 5.0, | |
| 187 | - | Unit::Lb => 10.0, | |
| 187 | + | LoadUnit::Kg => 5.0, | |
| 188 | + | LoadUnit::Lb => 10.0, | |
| 188 | 189 | }, | |
| 189 | 190 | } | |
| 190 | 191 | } | |
| @@ -195,14 +196,14 @@ mod tests { | |||
| 195 | 196 | ||
| 196 | 197 | fn fresh() -> Db { | |
| 197 | 198 | let db = Db::open_in_memory().unwrap(); | |
| 198 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 199 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 199 | 200 | db | |
| 200 | 201 | } | |
| 201 | 202 | ||
| 202 | 203 | #[test] | |
| 203 | 204 | fn seed_creates_expected_counts() { | |
| 204 | 205 | let db = fresh(); | |
| 205 | - | seed_starter_content(&db, Unit::Kg).unwrap(); | |
| 206 | + | seed_starter_content(&db, LoadUnit::Kg).unwrap(); | |
| 206 | 207 | assert_eq!(db.list_tags().unwrap().len(), SEED_TAGS.len()); | |
| 207 | 208 | assert_eq!(db.list_exercises().unwrap().len(), SEED_EXERCISES.len()); | |
| 208 | 209 | } | |
| @@ -210,15 +211,15 @@ mod tests { | |||
| 210 | 211 | #[test] | |
| 211 | 212 | fn seed_is_idempotent_when_run_twice() { | |
| 212 | 213 | let db = fresh(); | |
| 213 | - | seed_starter_content(&db, Unit::Kg).unwrap(); | |
| 214 | - | seed_starter_content(&db, Unit::Kg).unwrap(); | |
| 214 | + | seed_starter_content(&db, LoadUnit::Kg).unwrap(); | |
| 215 | + | seed_starter_content(&db, LoadUnit::Kg).unwrap(); | |
| 215 | 216 | assert_eq!(db.list_exercises().unwrap().len(), SEED_EXERCISES.len()); | |
| 216 | 217 | } | |
| 217 | 218 | ||
| 218 | 219 | #[test] | |
| 219 | 220 | fn seed_respects_unit_for_increments() { | |
| 220 | 221 | let db = fresh(); | |
| 221 | - | seed_starter_content(&db, Unit::Lb).unwrap(); | |
| 222 | + | seed_starter_content(&db, LoadUnit::Lb).unwrap(); | |
| 222 | 223 | let squat = db | |
| 223 | 224 | .list_exercises() | |
| 224 | 225 | .unwrap() | |
| @@ -248,7 +249,7 @@ mod tests { | |||
| 248 | 249 | #[test] | |
| 249 | 250 | fn seed_wires_up_tags() { | |
| 250 | 251 | let db = fresh(); | |
| 251 | - | seed_starter_content(&db, Unit::Kg).unwrap(); | |
| 252 | + | seed_starter_content(&db, LoadUnit::Kg).unwrap(); | |
| 252 | 253 | let squat = db | |
| 253 | 254 | .list_exercises() | |
| 254 | 255 | .unwrap() |
| @@ -245,12 +245,12 @@ impl Db { | |||
| 245 | 245 | #[cfg(test)] | |
| 246 | 246 | mod tests { | |
| 247 | 247 | use super::*; | |
| 248 | - | use crate::db::Unit; | |
| 248 | + | use crate::values::LoadUnit; | |
| 249 | 249 | use crate::templates::ResistanceType; | |
| 250 | 250 | ||
| 251 | 251 | fn setup() -> (Db, i64) { | |
| 252 | 252 | let db = Db::open_in_memory().unwrap(); | |
| 253 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 253 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 254 | 254 | let id = db | |
| 255 | 255 | .create_exercise("squat", ResistanceType::Freeweight, "kg", 2.5, &[]) | |
| 256 | 256 | .unwrap(); |
| @@ -236,11 +236,11 @@ impl Db { | |||
| 236 | 236 | #[cfg(test)] | |
| 237 | 237 | mod tests { | |
| 238 | 238 | use super::*; | |
| 239 | - | use crate::db::Unit; | |
| 239 | + | use crate::values::LoadUnit; | |
| 240 | 240 | ||
| 241 | 241 | fn setup() -> Db { | |
| 242 | 242 | let db = Db::open_in_memory().unwrap(); | |
| 243 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 243 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 244 | 244 | db | |
| 245 | 245 | } | |
| 246 | 246 |
| @@ -0,0 +1,189 @@ | |||
| 1 | + | //! Validated domain primitives. | |
| 2 | + | //! | |
| 3 | + | //! Newtype wrappers around the raw numeric types used across ripgrow. | |
| 4 | + | //! Their point is not run-time safety (the schema also enforces most of | |
| 5 | + | //! these invariants); the point is that a function signature says exactly | |
| 6 | + | //! what it works with. `fn append_set(rpe: i32)` will accept `0` or `-1`; | |
| 7 | + | //! `fn append_set(rpe: Rpe)` will not. | |
| 8 | + | //! | |
| 9 | + | //! Every constructor is fallible and returns `crate::Error`. Accessors | |
| 10 | + | //! return the raw inner value. | |
| 11 | + | ||
| 12 | + | use crate::error::Error; | |
| 13 | + | ||
| 14 | + | /// Rate of Perceived Exertion on ripgrow's 1-5 scale. RPE is a | |
| 15 | + | /// self-report, not a measurement; even a "correct" value is subjective. | |
| 16 | + | /// The scale mapping (RPE 5 = maximum effort, RPE 1 = warmup) is documented | |
| 17 | + | /// in `heuristics::RIR_FROM_RPE`. | |
| 18 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] | |
| 19 | + | pub struct Rpe(i32); | |
| 20 | + | ||
| 21 | + | impl Rpe { | |
| 22 | + | pub const MIN: Rpe = Rpe(1); | |
| 23 | + | pub const MAX: Rpe = Rpe(5); | |
| 24 | + | ||
| 25 | + | pub fn new(n: i32) -> Result<Self, Error> { | |
| 26 | + | if (1..=5).contains(&n) { | |
| 27 | + | Ok(Rpe(n)) | |
| 28 | + | } else { | |
| 29 | + | Err(Error::InvalidRpe(n)) | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | pub fn get(self) -> i32 { | |
| 34 | + | self.0 | |
| 35 | + | } | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | /// Reps performed in a set. A real measurement (someone counted). | |
| 39 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] | |
| 40 | + | pub struct Reps(i32); | |
| 41 | + | ||
| 42 | + | impl Reps { | |
| 43 | + | pub fn new(n: i32) -> Result<Self, Error> { | |
| 44 | + | if n >= 0 { | |
| 45 | + | Ok(Reps(n)) | |
| 46 | + | } else { | |
| 47 | + | Err(Error::InvalidReps(n)) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | pub fn get(self) -> i32 { | |
| 52 | + | self.0 | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | /// Weight moved on a set, in the exercise's [`LoadUnit`]. A real | |
| 57 | + | /// measurement (whatever the plate stack or the barbell reads). | |
| 58 | + | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] | |
| 59 | + | pub struct Load(f64); | |
| 60 | + | ||
| 61 | + | impl Load { | |
| 62 | + | /// Zero load — for bodyweight or before-plates entries. | |
| 63 | + | pub const ZERO: Load = Load(0.0); | |
| 64 | + | ||
| 65 | + | pub fn new(v: f64) -> Result<Self, Error> { | |
| 66 | + | if v.is_finite() && v >= 0.0 { | |
| 67 | + | Ok(Load(v)) | |
| 68 | + | } else { | |
| 69 | + | Err(Error::InvalidLoad(v)) | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | pub fn get(self) -> f64 { | |
| 74 | + | self.0 | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | /// Smallest legal load change for an exercise. May be zero (bodyweight, | |
| 79 | + | /// cardio). A real measurement of the equipment. | |
| 80 | + | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] | |
| 81 | + | pub struct Increment(f64); | |
| 82 | + | ||
| 83 | + | impl Increment { | |
| 84 | + | pub const ZERO: Increment = Increment(0.0); | |
| 85 | + | ||
| 86 | + | pub fn new(v: f64) -> Result<Self, Error> { | |
| 87 | + | if v.is_finite() && v >= 0.0 { | |
| 88 | + | Ok(Increment(v)) | |
| 89 | + | } else { | |
| 90 | + | Err(Error::InvalidIncrement(v)) | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | pub fn get(self) -> f64 { | |
| 95 | + | self.0 | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | /// Weight unit for the load column. Both the profile's default unit and | |
| 100 | + | /// each exercise's `load_unit` use this type. Timed and distance efforts | |
| 101 | + | /// have implicit units (seconds, meters) and do not carry a `LoadUnit`. | |
| 102 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | |
| 103 | + | pub enum LoadUnit { | |
| 104 | + | Kg, | |
| 105 | + | Lb, | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | impl LoadUnit { | |
| 109 | + | pub fn as_str(&self) -> &'static str { | |
| 110 | + | match self { | |
| 111 | + | LoadUnit::Kg => "kg", | |
| 112 | + | LoadUnit::Lb => "lb", | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | pub fn parse(s: &str) -> Result<Self, Error> { | |
| 117 | + | match s { | |
| 118 | + | "kg" => Ok(LoadUnit::Kg), | |
| 119 | + | "lb" => Ok(LoadUnit::Lb), | |
| 120 | + | other => Err(Error::InvalidUnit(other.to_string())), | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | /// Wrapper around a value derived from a formula rather than measured. | |
| 126 | + | /// The `method` field carries a short human-readable label naming the | |
| 127 | + | /// formula so that UI code can render "e1RM (rpe-adjusted Epley)" instead | |
| 128 | + | /// of a bare number that pretends to be an observation. | |
| 129 | + | /// | |
| 130 | + | /// This exists so the type system pushes back on treating estimates as | |
| 131 | + | /// ground truth. Anywhere a function returns `Estimate<T>` instead of | |
| 132 | + | /// `T`, that is a load-bearing signal that the caller is looking at a | |
| 133 | + | /// heuristic. | |
| 134 | + | #[derive(Debug, Clone, Copy, PartialEq)] | |
| 135 | + | pub struct Estimate<T> { | |
| 136 | + | pub value: T, | |
| 137 | + | pub method: &'static str, | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | impl<T> Estimate<T> { | |
| 141 | + | pub fn new(value: T, method: &'static str) -> Self { | |
| 142 | + | Estimate { value, method } | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | #[cfg(test)] | |
| 147 | + | mod tests { | |
| 148 | + | use super::*; | |
| 149 | + | ||
| 150 | + | #[test] | |
| 151 | + | fn rpe_validates_range() { | |
| 152 | + | assert!(Rpe::new(0).is_err()); | |
| 153 | + | assert!(Rpe::new(6).is_err()); | |
| 154 | + | assert_eq!(Rpe::new(3).unwrap().get(), 3); | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | #[test] | |
| 158 | + | fn reps_rejects_negative() { | |
| 159 | + | assert!(Reps::new(-1).is_err()); | |
| 160 | + | assert_eq!(Reps::new(0).unwrap().get(), 0); | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | #[test] | |
| 164 | + | fn load_rejects_negative_and_nan() { | |
| 165 | + | assert!(Load::new(-0.1).is_err()); | |
| 166 | + | assert!(Load::new(f64::NAN).is_err()); | |
| 167 | + | assert!(Load::new(f64::INFINITY).is_err()); | |
| 168 | + | assert_eq!(Load::new(100.0).unwrap().get(), 100.0); | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | #[test] | |
| 172 | + | fn increment_rejects_negative() { | |
| 173 | + | assert!(Increment::new(-1.0).is_err()); | |
| 174 | + | assert_eq!(Increment::ZERO.get(), 0.0); | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | #[test] | |
| 178 | + | fn load_unit_round_trip() { | |
| 179 | + | assert_eq!(LoadUnit::parse("kg").unwrap(), LoadUnit::Kg); | |
| 180 | + | assert_eq!(LoadUnit::Lb.as_str(), "lb"); | |
| 181 | + | assert!(LoadUnit::parse("stone").is_err()); | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | #[test] | |
| 185 | + | fn estimate_carries_method_label() { | |
| 186 | + | let e = Estimate::new(150.0, "epley"); | |
| 187 | + | assert_eq!(e.method, "epley"); | |
| 188 | + | } | |
| 189 | + | } |
| @@ -13,7 +13,7 @@ use ratatui::style::{Modifier, Style}; | |||
| 13 | 13 | use ratatui::text::{Line, Span}; | |
| 14 | 14 | use ratatui::widgets::{Block, Borders, Tabs}; | |
| 15 | 15 | use ripgrow_core::{ | |
| 16 | - | Db, Unit, create_profile_in, list_profiles, profiles_dir, seed_starter_content, | |
| 16 | + | Db, LoadUnit, create_profile_in, list_profiles, profiles_dir, seed_starter_content, | |
| 17 | 17 | }; | |
| 18 | 18 | ||
| 19 | 19 | use crate::screens::diagnostic::DiagnosticScreen; | |
| @@ -274,7 +274,7 @@ impl App { | |||
| 274 | 274 | }); | |
| 275 | 275 | } | |
| 276 | 276 | ||
| 277 | - | fn create_profile(&mut self, display_name: &str, unit: Unit) { | |
| 277 | + | fn create_profile(&mut self, display_name: &str, unit: LoadUnit) { | |
| 278 | 278 | let dir = match profiles_dir() { | |
| 279 | 279 | Ok(d) => d, | |
| 280 | 280 | Err(e) => { |
| @@ -597,7 +597,7 @@ fn subsequence_match(haystack: &str, needle: &str) -> bool { | |||
| 597 | 597 | mod tests { | |
| 598 | 598 | use super::*; | |
| 599 | 599 | use crossterm::event::KeyEvent; | |
| 600 | - | use ripgrow_core::{ResistanceType, Unit}; | |
| 600 | + | use ripgrow_core::{ResistanceType, LoadUnit}; | |
| 601 | 601 | ||
| 602 | 602 | fn key(c: KeyCode) -> KeyEvent { | |
| 603 | 603 | KeyEvent::new(c, KeyModifiers::empty()) | |
| @@ -605,7 +605,7 @@ mod tests { | |||
| 605 | 605 | ||
| 606 | 606 | fn setup() -> (Db, i64) { | |
| 607 | 607 | let db = Db::open_in_memory().unwrap(); | |
| 608 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 608 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 609 | 609 | let sq = db | |
| 610 | 610 | .create_exercise("squat", ResistanceType::Freeweight, "kg", 2.5, &[]) | |
| 611 | 611 | .unwrap(); |
| @@ -519,7 +519,7 @@ fn direction_glyph(db: &Db, ex: &Exercise, next_load: f64) -> &'static str { | |||
| 519 | 519 | mod tests { | |
| 520 | 520 | use super::*; | |
| 521 | 521 | use crossterm::event::KeyEvent; | |
| 522 | - | use ripgrow_core::{ResistanceType, Unit}; | |
| 522 | + | use ripgrow_core::{ResistanceType, LoadUnit}; | |
| 523 | 523 | ||
| 524 | 524 | fn key(c: KeyCode) -> KeyEvent { | |
| 525 | 525 | KeyEvent::new(c, KeyModifiers::empty()) | |
| @@ -527,7 +527,7 @@ mod tests { | |||
| 527 | 527 | ||
| 528 | 528 | fn setup() -> (Db, Vec<i64>, Vec<i64>) { | |
| 529 | 529 | let db = Db::open_in_memory().unwrap(); | |
| 530 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 530 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 531 | 531 | let t1 = db.upsert_tag("chest").unwrap(); | |
| 532 | 532 | let t2 = db.upsert_tag("back").unwrap(); | |
| 533 | 533 | let t3 = db.upsert_tag("legs").unwrap(); |
| @@ -245,7 +245,7 @@ fn state_name(s: State) -> &'static str { | |||
| 245 | 245 | mod tests { | |
| 246 | 246 | use super::*; | |
| 247 | 247 | use crossterm::event::KeyEvent; | |
| 248 | - | use ripgrow_core::{ResistanceType, Unit}; | |
| 248 | + | use ripgrow_core::{ResistanceType, LoadUnit}; | |
| 249 | 249 | ||
| 250 | 250 | fn key(c: KeyCode) -> KeyEvent { | |
| 251 | 251 | KeyEvent::new(c, KeyModifiers::empty()) | |
| @@ -253,7 +253,7 @@ mod tests { | |||
| 253 | 253 | ||
| 254 | 254 | fn setup() -> (Db, i64) { | |
| 255 | 255 | let db = Db::open_in_memory().unwrap(); | |
| 256 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 256 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 257 | 257 | let id = db | |
| 258 | 258 | .create_exercise("squat", ResistanceType::Freeweight, "kg", 2.5, &[]) | |
| 259 | 259 | .unwrap(); | |
| @@ -273,7 +273,7 @@ mod tests { | |||
| 273 | 273 | #[test] | |
| 274 | 274 | fn j_k_wraps_selection() { | |
| 275 | 275 | let db = Db::open_in_memory().unwrap(); | |
| 276 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 276 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 277 | 277 | db.create_exercise("a", ResistanceType::Freeweight, "kg", 2.5, &[]) | |
| 278 | 278 | .unwrap(); | |
| 279 | 279 | db.create_exercise("b", ResistanceType::Freeweight, "kg", 2.5, &[]) |
| @@ -381,7 +381,7 @@ fn subsequence_match(haystack: &str, needle: &str) -> bool { | |||
| 381 | 381 | mod tests { | |
| 382 | 382 | use super::*; | |
| 383 | 383 | use crossterm::event::KeyEvent; | |
| 384 | - | use ripgrow_core::{ResistanceType, Unit}; | |
| 384 | + | use ripgrow_core::{ResistanceType, LoadUnit}; | |
| 385 | 385 | ||
| 386 | 386 | fn key(c: KeyCode) -> KeyEvent { | |
| 387 | 387 | KeyEvent::new(c, KeyModifiers::empty()) | |
| @@ -389,7 +389,7 @@ mod tests { | |||
| 389 | 389 | ||
| 390 | 390 | fn setup() -> (Db, i64) { | |
| 391 | 391 | let db = Db::open_in_memory().unwrap(); | |
| 392 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 392 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 393 | 393 | let sq = db | |
| 394 | 394 | .create_exercise("squat", ResistanceType::Freeweight, "kg", 2.5, &[]) | |
| 395 | 395 | .unwrap(); | |
| @@ -477,7 +477,7 @@ mod tests { | |||
| 477 | 477 | #[test] | |
| 478 | 478 | fn bracket_keys_change_date() { | |
| 479 | 479 | let db = Db::open_in_memory().unwrap(); | |
| 480 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 480 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 481 | 481 | let mut screen = LogScreen::load(&db).unwrap(); | |
| 482 | 482 | let start = screen.date; | |
| 483 | 483 | screen.on_key(&db, key(KeyCode::Char('['))); |
| @@ -12,7 +12,7 @@ use ratatui::layout::{Constraint, Direction, Layout, Rect}; | |||
| 12 | 12 | use ratatui::style::{Modifier, Style}; | |
| 13 | 13 | use ratatui::text::Line; | |
| 14 | 14 | use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; | |
| 15 | - | use ripgrow_core::{Profile, Unit}; | |
| 15 | + | use ripgrow_core::{Profile, LoadUnit}; | |
| 16 | 16 | ||
| 17 | 17 | pub struct ProfilePicker { | |
| 18 | 18 | pub profiles: Vec<Profile>, | |
| @@ -25,7 +25,7 @@ pub enum PickerAction { | |||
| 25 | 25 | None, | |
| 26 | 26 | Quit, | |
| 27 | 27 | Open(PathBuf), | |
| 28 | - | Create { display_name: String, unit: Unit }, | |
| 28 | + | Create { display_name: String, unit: LoadUnit }, | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | 31 | impl ProfilePicker { | |
| @@ -134,13 +134,13 @@ impl ProfilePicker { | |||
| 134 | 134 | #[derive(Default)] | |
| 135 | 135 | pub struct NewProfileModal { | |
| 136 | 136 | pub name: String, | |
| 137 | - | pub unit: Option<Unit>, | |
| 137 | + | pub unit: Option<LoadUnit>, | |
| 138 | 138 | } | |
| 139 | 139 | ||
| 140 | 140 | enum ModalAction { | |
| 141 | 141 | None, | |
| 142 | 142 | Cancel, | |
| 143 | - | Submit { display_name: String, unit: Unit }, | |
| 143 | + | Submit { display_name: String, unit: LoadUnit }, | |
| 144 | 144 | } | |
| 145 | 145 | ||
| 146 | 146 | impl NewProfileModal { | |
| @@ -164,9 +164,9 @@ impl NewProfileModal { | |||
| 164 | 164 | } | |
| 165 | 165 | KeyCode::Tab => { | |
| 166 | 166 | self.unit = Some(match self.unit { | |
| 167 | - | None => Unit::Kg, | |
| 168 | - | Some(Unit::Kg) => Unit::Lb, | |
| 169 | - | Some(Unit::Lb) => Unit::Kg, | |
| 167 | + | None => LoadUnit::Kg, | |
| 168 | + | Some(LoadUnit::Kg) => LoadUnit::Lb, | |
| 169 | + | Some(LoadUnit::Lb) => LoadUnit::Kg, | |
| 170 | 170 | }); | |
| 171 | 171 | ModalAction::None | |
| 172 | 172 | } | |
| @@ -182,8 +182,8 @@ impl NewProfileModal { | |||
| 182 | 182 | let modal = centered_rect(60, 30, area); | |
| 183 | 183 | let unit_str = match self.unit { | |
| 184 | 184 | None => "(tab to choose kg or lb)", | |
| 185 | - | Some(Unit::Kg) => "kg", | |
| 186 | - | Some(Unit::Lb) => "lb", | |
| 185 | + | Some(LoadUnit::Kg) => "kg", | |
| 186 | + | Some(LoadUnit::Lb) => "lb", | |
| 187 | 187 | }; | |
| 188 | 188 | let text = format!( | |
| 189 | 189 | "name: {}_\nunit: {}\n\nenter save tab toggle unit esc cancel", | |
| @@ -279,7 +279,7 @@ mod tests { | |||
| 279 | 279 | match p.on_key(key(KeyCode::Enter)) { | |
| 280 | 280 | PickerAction::Create { display_name, unit } => { | |
| 281 | 281 | assert_eq!(display_name, "jane"); | |
| 282 | - | assert_eq!(unit, Unit::Kg); | |
| 282 | + | assert_eq!(unit, LoadUnit::Kg); | |
| 283 | 283 | } | |
| 284 | 284 | _ => panic!("expected Create action"), | |
| 285 | 285 | } | |
| @@ -291,10 +291,10 @@ mod tests { | |||
| 291 | 291 | let mut m = NewProfileModal::default(); | |
| 292 | 292 | assert_eq!(m.unit, None); | |
| 293 | 293 | m.on_key(key(KeyCode::Tab)); | |
| 294 | - | assert_eq!(m.unit, Some(Unit::Kg)); | |
| 294 | + | assert_eq!(m.unit, Some(LoadUnit::Kg)); | |
| 295 | 295 | m.on_key(key(KeyCode::Tab)); | |
| 296 | - | assert_eq!(m.unit, Some(Unit::Lb)); | |
| 296 | + | assert_eq!(m.unit, Some(LoadUnit::Lb)); | |
| 297 | 297 | m.on_key(key(KeyCode::Tab)); | |
| 298 | - | assert_eq!(m.unit, Some(Unit::Kg)); | |
| 298 | + | assert_eq!(m.unit, Some(LoadUnit::Kg)); | |
| 299 | 299 | } | |
| 300 | 300 | } |
| @@ -530,7 +530,7 @@ fn text_edit(target: &mut String, key: KeyEvent) { | |||
| 530 | 530 | mod tests { | |
| 531 | 531 | use super::*; | |
| 532 | 532 | use crossterm::event::KeyEvent; | |
| 533 | - | use ripgrow_core::Unit; | |
| 533 | + | use ripgrow_core::LoadUnit; | |
| 534 | 534 | ||
| 535 | 535 | fn key(c: KeyCode) -> KeyEvent { | |
| 536 | 536 | KeyEvent::new(c, KeyModifiers::empty()) | |
| @@ -542,7 +542,7 @@ mod tests { | |||
| 542 | 542 | ||
| 543 | 543 | fn fresh_db() -> Db { | |
| 544 | 544 | let db = Db::open_in_memory().unwrap(); | |
| 545 | - | db.init_profile("self", Unit::Kg).unwrap(); | |
| 545 | + | db.init_profile("self", LoadUnit::Kg).unwrap(); | |
| 546 | 546 | db | |
| 547 | 547 | } | |
| 548 | 548 |