247 lines
6.1 KiB
C
247 lines
6.1 KiB
C
/**
|
|
* @file measured_max.c
|
|
* @brief Explicit measured-max and working-load implementation.
|
|
*/
|
|
|
|
#include "trainlog/measured_max.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
static bool measured_point_better(
|
|
const TrainlogExercisePerformancePoint *candidate,
|
|
const TrainlogExercisePerformancePoint *current
|
|
)
|
|
{
|
|
if (
|
|
candidate == NULL ||
|
|
candidate->has_performance == 0
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
current == NULL ||
|
|
current->has_performance == 0
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
candidate->load_mode !=
|
|
current->load_mode
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
switch (candidate->load_mode) {
|
|
case TRAINLOG_LOAD_EXTERNAL:
|
|
if (candidate->has_weight == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
candidate->weight_kg >
|
|
current->weight_kg
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return
|
|
candidate->weight_kg ==
|
|
current->weight_kg &&
|
|
candidate->metric_value >
|
|
current->metric_value;
|
|
|
|
case TRAINLOG_LOAD_ASSISTANCE:
|
|
if (candidate->has_weight == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
candidate->weight_kg <
|
|
current->weight_kg
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return
|
|
candidate->weight_kg ==
|
|
current->weight_kg &&
|
|
candidate->metric_value >
|
|
current->metric_value;
|
|
|
|
case TRAINLOG_LOAD_NONE:
|
|
default:
|
|
return
|
|
candidate->metric_value >
|
|
current->metric_value;
|
|
}
|
|
}
|
|
|
|
TrainlogStatus trainlog_measured_max_summarize(
|
|
const TrainlogExercisePerformancePoint *points,
|
|
size_t count,
|
|
TrainlogMeasuredMaxSummary *output
|
|
)
|
|
{
|
|
size_t index;
|
|
|
|
if (
|
|
output == NULL ||
|
|
(
|
|
count > 0U &&
|
|
points == NULL
|
|
)
|
|
) {
|
|
return
|
|
TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
(void)memset(
|
|
output,
|
|
0,
|
|
sizeof(*output)
|
|
);
|
|
|
|
for (
|
|
index = 0U;
|
|
index < count;
|
|
++index
|
|
) {
|
|
const TrainlogExercisePerformancePoint *point =
|
|
&points[index];
|
|
|
|
/* INVARIANT: the database read model emits work immediately before
|
|
* the MAX fact when one occurrence owns both. Summarization consumes
|
|
* that ABI-safe pair as one test whose measured result is the MAX. */
|
|
if (point->has_explicit_max == 0 && index + 1U < count &&
|
|
points[index + 1U].has_explicit_max != 0 &&
|
|
strcmp(point->session_id, points[index + 1U].session_id) == 0 &&
|
|
strcmp(point->started_at, points[index + 1U].started_at) == 0 &&
|
|
strcmp(point->equipment_id, points[index + 1U].equipment_id) == 0 &&
|
|
point->tracking_mode == points[index + 1U].tracking_mode &&
|
|
point->load_mode == points[index + 1U].load_mode) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
point->session_type !=
|
|
TRAINLOG_SESSION_MAX_TEST
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
++output->test_count;
|
|
|
|
if (point->has_performance == 0 && point->has_explicit_max == 0) {
|
|
continue;
|
|
}
|
|
|
|
++output->successful_test_count;
|
|
|
|
if (!output->found) {
|
|
output->found = true;
|
|
output->current = *point;
|
|
output->has_record = true;
|
|
output->record = *point;
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
point->load_mode ==
|
|
output->current.load_mode &&
|
|
measured_point_better(
|
|
point,
|
|
&output->record
|
|
)
|
|
) {
|
|
output->record = *point;
|
|
}
|
|
}
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
TrainlogStatus trainlog_measured_max_working_load(
|
|
const TrainlogExercisePerformancePoint *measured,
|
|
double percentage,
|
|
double increment_kg,
|
|
double *output_kg
|
|
)
|
|
{
|
|
double raw;
|
|
double rounded;
|
|
|
|
if (
|
|
measured == NULL ||
|
|
output_kg == NULL ||
|
|
measured->session_type !=
|
|
TRAINLOG_SESSION_MAX_TEST ||
|
|
measured->has_performance == 0 ||
|
|
measured->load_mode !=
|
|
TRAINLOG_LOAD_EXTERNAL ||
|
|
measured->has_weight == 0 ||
|
|
!isfinite(measured->weight_kg) ||
|
|
measured->weight_kg <= 0.0 ||
|
|
!isfinite(percentage) ||
|
|
percentage <= 0.0 ||
|
|
percentage > 100.0 ||
|
|
!isfinite(increment_kg) ||
|
|
increment_kg <= 0.0
|
|
) {
|
|
return
|
|
TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
|
|
raw =
|
|
measured->weight_kg *
|
|
percentage /
|
|
100.0;
|
|
|
|
rounded =
|
|
floor(
|
|
raw /
|
|
increment_kg +
|
|
0.5
|
|
) *
|
|
increment_kg;
|
|
|
|
if (rounded <= 0.0) {
|
|
rounded =
|
|
increment_kg;
|
|
}
|
|
|
|
*output_kg = rounded;
|
|
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
TrainlogStatus trainlog_measured_max_target_load(
|
|
const TrainlogLatestExplicitMax *latest,
|
|
const char *equipment_id,
|
|
TrainlogLoadMode equipment_load_semantics,
|
|
int percent,
|
|
double *output_kg
|
|
)
|
|
{
|
|
double result;
|
|
/* WHY: %MAX is an arithmetic shortcut explicitly chosen by the user, not
|
|
* a training recommendation. CONTRACT: only the newest explicit MAX for
|
|
* this exercise and exact equipment context may cross into the calculator;
|
|
* assistance and missing/unknown contexts are rejected. */
|
|
if (latest == NULL || equipment_id == NULL || output_kg == NULL ||
|
|
!latest->found || equipment_id[0] == '\0' ||
|
|
strcmp(latest->equipment_id, equipment_id) != 0 ||
|
|
equipment_load_semantics != TRAINLOG_LOAD_EXTERNAL ||
|
|
!isfinite(latest->max_weight_kg) || latest->max_weight_kg <= 0.0 ||
|
|
percent < 1 || percent > 100)
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
result = latest->max_weight_kg * (double)percent / 100.0;
|
|
if (!isfinite(result) || result <= 0.0)
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
/* INVARIANT: callers persist only this resulting target weight. The MAX
|
|
* identity and selected percentage remain transient UI state. */
|
|
*output_kg = result;
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|