| 81 |
81 |
|
/// Append a normal (non-diagnostic) working set. `set_index` is
|
| 82 |
82 |
|
/// auto-assigned as one past the current max for this (date, exercise).
|
| 83 |
83 |
|
/// Returns the new row id.
|
| 84 |
|
- |
///
|
| 85 |
|
- |
/// Primitive inputs are validated through the newtype constructors
|
| 86 |
|
- |
/// before any SQL runs. A follow-up commit will lift the signature to
|
| 87 |
|
- |
/// take `Load`/`Reps`/`Rpe` directly so parsing lives at the TUI
|
| 88 |
|
- |
/// boundary.
|
| 89 |
84 |
|
pub fn append_set(
|
| 90 |
85 |
|
&self,
|
| 91 |
86 |
|
exercise_id: i64,
|
| 92 |
87 |
|
date: NaiveDate,
|
| 93 |
|
- |
load: f64,
|
| 94 |
|
- |
reps: i32,
|
| 95 |
|
- |
rpe: i32,
|
|
88 |
+ |
load: Load,
|
|
89 |
+ |
reps: Reps,
|
|
90 |
+ |
rpe: Rpe,
|
| 96 |
91 |
|
) -> Result<i64, Error> {
|
| 97 |
|
- |
let load = Load::new(load)?;
|
| 98 |
|
- |
let reps = Reps::new(reps)?;
|
| 99 |
|
- |
let rpe = Rpe::new(rpe)?;
|
| 100 |
92 |
|
self.append_set_inner(exercise_id, date, load, reps, rpe, false)
|
| 101 |
93 |
|
}
|
| 102 |
94 |
|
|
| 107 |
99 |
|
&self,
|
| 108 |
100 |
|
exercise_id: i64,
|
| 109 |
101 |
|
date: NaiveDate,
|
| 110 |
|
- |
load: f64,
|
| 111 |
|
- |
reps: i32,
|
| 112 |
|
- |
rpe: i32,
|
|
102 |
+ |
load: Load,
|
|
103 |
+ |
reps: Reps,
|
|
104 |
+ |
rpe: Rpe,
|
| 113 |
105 |
|
) -> Result<i64, Error> {
|
| 114 |
|
- |
let load = Load::new(load)?;
|
| 115 |
|
- |
let reps = Reps::new(reps)?;
|
| 116 |
|
- |
let rpe = Rpe::new(rpe)?;
|
| 117 |
106 |
|
self.append_set_inner(exercise_id, date, load, reps, rpe, true)
|
| 118 |
107 |
|
}
|
| 119 |
108 |
|
|
| 280 |
269 |
|
fn append_and_list_round_trip() {
|
| 281 |
270 |
|
let (db, ex) = setup();
|
| 282 |
271 |
|
let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
|
| 283 |
|
- |
db.append_set(ex, date, 100.0, 5, 3).unwrap();
|
| 284 |
|
- |
db.append_set(ex, date, 100.0, 5, 3).unwrap();
|
| 285 |
|
- |
db.append_set(ex, date, 100.0, 5, 4).unwrap();
|
|
272 |
+ |
db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
273 |
+ |
db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
274 |
+ |
db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
|
| 286 |
275 |
|
let list = db.list_sets_for_session(ex, date).unwrap();
|
| 287 |
276 |
|
assert_eq!(list.len(), 3);
|
| 288 |
277 |
|
assert_eq!(list[0].set_index, 1);
|
| 291 |
280 |
|
}
|
| 292 |
281 |
|
|
| 293 |
282 |
|
#[test]
|
| 294 |
|
- |
fn append_rejects_rpe_out_of_range() {
|
| 295 |
|
- |
let (db, ex) = setup();
|
| 296 |
|
- |
let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
|
| 297 |
|
- |
assert!(db.append_set(ex, date, 100.0, 5, 0).is_err());
|
| 298 |
|
- |
assert!(db.append_set(ex, date, 100.0, 5, 6).is_err());
|
| 299 |
|
- |
}
|
| 300 |
|
- |
|
| 301 |
|
- |
#[test]
|
| 302 |
|
- |
fn append_rejects_negative_reps() {
|
| 303 |
|
- |
let (db, ex) = setup();
|
| 304 |
|
- |
let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
|
| 305 |
|
- |
assert!(db.append_set(ex, date, 100.0, -1, 3).is_err());
|
|
283 |
+ |
fn newtype_constructors_reject_out_of_range_values() {
|
|
284 |
+ |
// Validation now lives on the value constructors; append_set can
|
|
285 |
+ |
// never be called with a bad Rpe or Reps because you can't
|
|
286 |
+ |
// construct them. These tests confirm the constructors themselves
|
|
287 |
+ |
// still guard.
|
|
288 |
+ |
let _ = setup();
|
|
289 |
+ |
assert!(Rpe::new(0).is_err());
|
|
290 |
+ |
assert!(Rpe::new(6).is_err());
|
|
291 |
+ |
assert!(Reps::new(-1).is_err());
|
|
292 |
+ |
assert!(Load::new(-1.0).is_err());
|
| 306 |
293 |
|
}
|
| 307 |
294 |
|
|
| 308 |
295 |
|
#[test]
|
| 313 |
300 |
|
.unwrap();
|
| 314 |
301 |
|
let d1 = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
|
| 315 |
302 |
|
let d2 = NaiveDate::from_ymd_opt(2026, 7, 19).unwrap();
|
| 316 |
|
- |
db.append_set(ex1, d1, 100.0, 5, 3).unwrap();
|
| 317 |
|
- |
db.append_set(ex1, d2, 105.0, 5, 3).unwrap();
|
| 318 |
|
- |
db.append_set(ex2, d1, 80.0, 5, 3).unwrap();
|
|
303 |
+ |
db.append_set(ex1, d1, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
304 |
+ |
db.append_set(ex1, d2, Load::new(105.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
305 |
+ |
db.append_set(ex2, d1, Load::new(80.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
| 319 |
306 |
|
assert_eq!(db.list_sets_for_session(ex1, d1).unwrap().len(), 1);
|
| 320 |
307 |
|
assert_eq!(db.list_sets_for_session(ex1, d2).unwrap().len(), 1);
|
| 321 |
308 |
|
assert_eq!(db.list_sets_for_session(ex2, d1).unwrap().len(), 1);
|
| 326 |
313 |
|
let (db, ex) = setup();
|
| 327 |
314 |
|
let d1 = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
|
| 328 |
315 |
|
let d2 = NaiveDate::from_ymd_opt(2026, 7, 3).unwrap();
|
| 329 |
|
- |
db.append_set(ex, d1, 100.0, 5, 3).unwrap();
|
| 330 |
|
- |
db.append_set(ex, d1, 105.0, 5, 4).unwrap();
|
| 331 |
|
- |
db.append_set(ex, d2, 100.0, 5, 3).unwrap();
|
|
316 |
+ |
db.append_set(ex, d1, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
317 |
+ |
db.append_set(ex, d1, Load::new(105.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
|
|
318 |
+ |
db.append_set(ex, d2, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
| 332 |
319 |
|
let series = db.top_loads_over_time(ex).unwrap();
|
| 333 |
320 |
|
assert_eq!(series, vec![(d1, 105.0), (d2, 100.0)]);
|
| 334 |
321 |
|
}
|
| 346 |
333 |
|
// 100 x 5 @ RPE 5 -> e1RM 116.67
|
| 347 |
334 |
|
// 90 x 8 @ RPE 3 -> e1RM 90 * (1 + 10/30) = 120.0
|
| 348 |
335 |
|
// 120 x 3 @ RPE 5 -> e1RM 132.0 <- winner
|
| 349 |
|
- |
db.append_set(ex, date, 100.0, 5, 5).unwrap();
|
| 350 |
|
- |
db.append_set(ex, date, 90.0, 8, 3).unwrap();
|
| 351 |
|
- |
db.append_set(ex, date, 120.0, 3, 5).unwrap();
|
|
336 |
+ |
db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(5).unwrap()).unwrap();
|
|
337 |
+ |
db.append_set(ex, date, Load::new(90.0).unwrap(), Reps::new(8).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
338 |
+ |
db.append_set(ex, date, Load::new(120.0).unwrap(), Reps::new(3).unwrap(), Rpe::new(5).unwrap()).unwrap();
|
| 352 |
339 |
|
let e = db.best_e1rm(ex).unwrap().unwrap();
|
| 353 |
340 |
|
assert!((e - 132.0).abs() < 1e-6);
|
| 354 |
341 |
|
}
|
| 357 |
344 |
|
fn best_e1rm_ignores_warmup_rpe_1_sets() {
|
| 358 |
345 |
|
let (db, ex) = setup();
|
| 359 |
346 |
|
let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
|
| 360 |
|
- |
db.append_set(ex, date, 200.0, 10, 1).unwrap();
|
|
347 |
+ |
db.append_set(ex, date, Load::new(200.0).unwrap(), Reps::new(10).unwrap(), Rpe::new(1).unwrap()).unwrap();
|
| 361 |
348 |
|
assert_eq!(db.best_e1rm(ex).unwrap(), None);
|
| 362 |
349 |
|
}
|
| 363 |
350 |
|
|
| 366 |
353 |
|
let (db, ex) = setup();
|
| 367 |
354 |
|
let d1 = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
|
| 368 |
355 |
|
let d2 = NaiveDate::from_ymd_opt(2026, 7, 3).unwrap();
|
| 369 |
|
- |
db.append_diagnostic_set(ex, d1, 60.0, 5, 3).unwrap();
|
| 370 |
|
- |
db.append_diagnostic_set(ex, d1, 80.0, 5, 4).unwrap();
|
| 371 |
|
- |
db.append_set(ex, d2, 72.0, 5, 3).unwrap();
|
|
356 |
+ |
db.append_diagnostic_set(ex, d1, Load::new(60.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
357 |
+ |
db.append_diagnostic_set(ex, d1, Load::new(80.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
|
|
358 |
+ |
db.append_set(ex, d2, Load::new(72.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
| 372 |
359 |
|
let sessions = db.list_sessions_for_exercise(ex).unwrap();
|
| 373 |
360 |
|
assert_eq!(sessions.len(), 1, "diagnostic day should not appear");
|
| 374 |
361 |
|
assert_eq!(sessions[0][0].session_date, d2);
|
| 380 |
367 |
|
let (db, ex) = setup();
|
| 381 |
368 |
|
let d1 = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
|
| 382 |
369 |
|
let d2 = NaiveDate::from_ymd_opt(2026, 7, 3).unwrap();
|
| 383 |
|
- |
db.append_diagnostic_set(ex, d1, 120.0, 5, 4).unwrap();
|
| 384 |
|
- |
db.append_set(ex, d2, 80.0, 5, 3).unwrap();
|
|
370 |
+ |
db.append_diagnostic_set(ex, d1, Load::new(120.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
|
|
371 |
+ |
db.append_set(ex, d2, Load::new(80.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
| 385 |
372 |
|
let series = db.top_loads_over_time(ex).unwrap();
|
| 386 |
373 |
|
assert_eq!(series, vec![(d2, 80.0)]);
|
| 387 |
374 |
|
}
|
| 390 |
377 |
|
fn diagnostic_sets_still_count_toward_e1rm() {
|
| 391 |
378 |
|
let (db, ex) = setup();
|
| 392 |
379 |
|
let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
|
| 393 |
|
- |
db.append_diagnostic_set(ex, date, 120.0, 3, 4).unwrap();
|
|
380 |
+ |
db.append_diagnostic_set(ex, date, Load::new(120.0).unwrap(), Reps::new(3).unwrap(), Rpe::new(4).unwrap()).unwrap();
|
| 394 |
381 |
|
assert!(db.best_e1rm(ex).unwrap().is_some());
|
| 395 |
382 |
|
}
|
| 396 |
383 |
|
|
| 399 |
386 |
|
let (db, ex) = setup();
|
| 400 |
387 |
|
let d1 = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
|
| 401 |
388 |
|
let d2 = NaiveDate::from_ymd_opt(2026, 7, 5).unwrap();
|
| 402 |
|
- |
db.append_diagnostic_set(ex, d1, 70.0, 5, 3).unwrap();
|
| 403 |
|
- |
db.append_diagnostic_set(ex, d1, 80.0, 5, 4).unwrap();
|
| 404 |
|
- |
db.append_diagnostic_set(ex, d2, 60.0, 5, 3).unwrap();
|
| 405 |
|
- |
db.append_diagnostic_set(ex, d2, 90.0, 5, 4).unwrap();
|
|
389 |
+ |
db.append_diagnostic_set(ex, d1, Load::new(70.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
390 |
+ |
db.append_diagnostic_set(ex, d1, Load::new(80.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
|
|
391 |
+ |
db.append_diagnostic_set(ex, d2, Load::new(60.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
392 |
+ |
db.append_diagnostic_set(ex, d2, Load::new(90.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(4).unwrap()).unwrap();
|
| 406 |
393 |
|
// Most recent diagnostic (d2) had a top of 90.
|
| 407 |
394 |
|
assert_eq!(db.diagnostic_seed(ex).unwrap(), Some(90.0));
|
| 408 |
395 |
|
}
|
| 411 |
398 |
|
fn diagnostic_seed_none_when_no_diagnostic_history() {
|
| 412 |
399 |
|
let (db, ex) = setup();
|
| 413 |
400 |
|
let date = NaiveDate::from_ymd_opt(2026, 7, 5).unwrap();
|
| 414 |
|
- |
db.append_set(ex, date, 100.0, 5, 3).unwrap();
|
|
401 |
+ |
db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
| 415 |
402 |
|
assert_eq!(db.diagnostic_seed(ex).unwrap(), None);
|
| 416 |
403 |
|
}
|
| 417 |
404 |
|
|
| 419 |
406 |
|
fn delete_removes_and_gap_is_fine() {
|
| 420 |
407 |
|
let (db, ex) = setup();
|
| 421 |
408 |
|
let date = NaiveDate::from_ymd_opt(2026, 7, 18).unwrap();
|
| 422 |
|
- |
let a = db.append_set(ex, date, 100.0, 5, 3).unwrap();
|
| 423 |
|
- |
db.append_set(ex, date, 100.0, 5, 3).unwrap();
|
|
409 |
+ |
let a = db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
|
410 |
+ |
db.append_set(ex, date, Load::new(100.0).unwrap(), Reps::new(5).unwrap(), Rpe::new(3).unwrap()).unwrap();
|
| 424 |
411 |
|
db.delete_set(a).unwrap();
|
| 425 |
412 |
|
let list = db.list_sets_for_session(ex, date).unwrap();
|
| 426 |
413 |
|
assert_eq!(list.len(), 1);
|