This commit is contained in:
larssand
2026-08-24 21:28:38 +02:00
parent 355a1ad3b0
commit 3aeb77a042
13 changed files with 642 additions and 8 deletions

View File

@@ -147,6 +147,9 @@ function buildPublicTrainingStats(laps) {
if (!controlTargetMs || lapTimes[index] > controlTargetMs) break;
controlledLapStreak += 1;
}
const stints = splitPublicTrainingStints(laps);
const bestThreeConsecutive = getPublicBestConsecutiveLaps(stints, 3);
const bestFiveMinuteResult = getPublicBestDurationResult(stints, 5 * 60 * 1000);
return {
totalLaps: laps.length,
@@ -159,9 +162,80 @@ function buildPublicTrainingStats(laps) {
consistencySpreadPct: recentAverageMs ? ((percentile(0.9) - percentile(0.1)) / recentAverageMs) * 100 : 0,
controlledLapStreak,
dailySummaries,
stintCount: stints.length,
bestThreeConsecutive,
bestFiveMinuteResult,
};
}
function splitPublicTrainingStints(laps, breakThresholdMs = 2 * 60 * 1000) {
const stints = [];
(Array.isArray(laps) ? laps : []).forEach((lap) => {
const previousStint = stints.at(-1);
const previousLap = previousStint?.laps.at(-1);
const lapStartedAt = Math.max(0, Number(lap.timestamp || 0) - Number(lap.lapMs || 0));
const breakMs = previousLap ? Math.max(0, Number(lap.timestamp || 0) - Number(previousLap.timestamp || 0)) : 0;
const startsNewStint = !previousStint
|| String(previousLap.sessionId || "") !== String(lap.sessionId || "")
|| breakMs > breakThresholdMs;
const normalizedLap = {
...lap,
excludedFromPace: Boolean(previousLap && breakMs > breakThresholdMs && Number(lap.lapMs || 0) > breakThresholdMs),
};
if (startsNewStint) {
stints.push({ sessionId: String(lap.sessionId || ""), laps: [normalizedLap] });
} else {
previousStint.laps.push(normalizedLap);
}
});
return stints;
}
function getPublicBestConsecutiveLaps(stints, lapCount) {
let best = null;
(Array.isArray(stints) ? stints : []).forEach((stint) => {
const paceLaps = stint.laps.filter((lap) => !lap.excludedFromPace);
for (let index = 0; index <= paceLaps.length - lapCount; index += 1) {
const laps = paceLaps.slice(index, index + lapCount);
const totalMs = laps.reduce((sum, lap) => sum + Number(lap.lapMs || 0), 0);
const candidate = { lapCount, totalMs, averageLapMs: Math.round(totalMs / lapCount) };
if (!best || candidate.totalMs < best.totalMs) best = candidate;
}
});
return best;
}
function getPublicBestDurationResult(stints, targetMs) {
let best = null;
(Array.isArray(stints) ? stints : []).forEach((stint) => {
const paceLaps = stint.laps.filter((lap) => !lap.excludedFromPace);
for (let startIndex = 0; startIndex < paceLaps.length; startIndex += 1) {
const firstLap = paceLaps[startIndex];
const startedAt = Math.max(0, Number(firstLap.timestamp || 0) - Number(firstLap.lapMs || 0));
const cutoffAt = startedAt + targetMs;
const counted = [];
for (let index = startIndex; index < paceLaps.length; index += 1) {
const lap = paceLaps[index];
const lapStartedAt = Math.max(0, Number(lap.timestamp || 0) - Number(lap.lapMs || 0));
if (lapStartedAt >= cutoffAt) break;
counted.push(lap);
}
const lastLap = counted.at(-1);
if (!lastLap || Number(lastLap.timestamp || 0) < cutoffAt) continue;
const totalMs = Number(lastLap.timestamp || 0) - startedAt;
const lapCount = counted.length;
const candidate = {
targetMs,
lapCount,
totalMs,
averageLapMs: Math.round(counted.reduce((sum, lap) => sum + Number(lap.lapMs || 0), 0) / lapCount),
};
if (!best || candidate.lapCount > best.lapCount || (candidate.lapCount === best.lapCount && candidate.totalMs < best.totalMs)) best = candidate;
}
});
return best;
}
function normalizePublicLogoDataUrl(value) {
const logo = String(value || "").trim();
return logo.length <= 2_000_000 && /^data:image\/(?:png|jpeg|webp);base64,/i.test(logo) ? logo : "";
@@ -192,4 +266,7 @@ module.exports = {
hasPublicTrainingShare,
normalizePublicTrainingSince,
normalizePublicTrainingWindow,
splitPublicTrainingStints,
getPublicBestConsecutiveLaps,
getPublicBestDurationResult,
};