Move pace math out of the dashboard utility bulk
Pace-adjusted high calculations were embedded in dashboard-utils alongside chart, profile, and modal helpers. This moves the pure pace model and reusable HM time helpers into focused modules while keeping dashboard-utils re-export compatibility for older callers. Constraint: Preserve existing pace wording, thresholds, and calculation output. Rejected: Split all remaining dashboard-utils helpers at once | model/chart/modal helpers have wider call surfaces and should move in separate reversible passes. Confidence: high Scope-risk: narrow Reversibility: clean Tested: TypeScript diagnostics for pace-utils, time-utils, dashboard-utils, and FutureForecastModal Tested: npm run build Not-tested: Bundle analyzer size comparison.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
export function normalizeHm(value?: string | null) {
|
||||
const match = String(value || "").match(/(\d{1,2}):(\d{2})/);
|
||||
if (!match) return null;
|
||||
const hour = Number.parseInt(match[1], 10);
|
||||
const minute = Number.parseInt(match[2], 10);
|
||||
if (
|
||||
!Number.isFinite(hour) ||
|
||||
!Number.isFinite(minute) ||
|
||||
hour < 0 ||
|
||||
hour > 23 ||
|
||||
minute < 0 ||
|
||||
minute > 59
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function hmToMinutes(value?: string | null) {
|
||||
const normalized = normalizeHm(value);
|
||||
if (!normalized) return null;
|
||||
const [hourText, minuteText] = normalized.split(":");
|
||||
const hour = Number.parseInt(hourText || "", 10);
|
||||
const minute = Number.parseInt(minuteText || "", 10);
|
||||
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
export function interpolateSeriesAtMinutes(
|
||||
times: string[],
|
||||
values: Array<number | null | undefined>,
|
||||
currentMinutes: number,
|
||||
) {
|
||||
const points = times
|
||||
.map((time, index) => {
|
||||
const minute = hmToMinutes(time);
|
||||
const value = values[index];
|
||||
return minute != null && value != null && Number.isFinite(Number(value))
|
||||
? { minute, value: Number(value) }
|
||||
: null;
|
||||
})
|
||||
.filter((point): point is { minute: number; value: number } => point != null);
|
||||
|
||||
if (!points.length) return null;
|
||||
|
||||
const exact = points.find((point) => point.minute === currentMinutes);
|
||||
if (exact) return exact.value;
|
||||
|
||||
let left: { minute: number; value: number } | null = null;
|
||||
let right: { minute: number; value: number } | null = null;
|
||||
|
||||
for (const point of points) {
|
||||
if (point.minute < currentMinutes) {
|
||||
left = point;
|
||||
continue;
|
||||
}
|
||||
if (point.minute > currentMinutes) {
|
||||
right = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (left && right) {
|
||||
const span = right.minute - left.minute;
|
||||
if (span <= 0) return left.value;
|
||||
const ratio = (currentMinutes - left.minute) / span;
|
||||
return Number((left.value + (right.value - left.value) * ratio).toFixed(1));
|
||||
}
|
||||
if (left) return left.value;
|
||||
if (right) return right.value;
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user