export function getSessionTypeLabel(type, { t }) { const key = `session.${String(type || "").toLowerCase()}`; const translated = t(key); return translated === key ? String(type || "") : translated; } export function getStatusLabel(status, { t }) { const key = `status.${String(status || "").toLowerCase()}`; const translated = t(key); return translated === key ? String(status || "") : translated; } export function isUntimedSession(session) { return String(session?.type || "").toLowerCase() === "open_practice"; } export function getActiveSession({ state }) { return state.sessions.find((s) => s.id === state.activeSessionId) || null; } export function getSessionTargetMs(session, { isUntimedSession }) { if (isUntimedSession(session)) { return null; } return Math.max(1, Number(session?.durationMin || 0)) * 60 * 1000; } export function getSessionLapWindow(session, { state }) { const event = state.events.find((item) => item.id === session?.eventId); if (event?.mode !== "race") { return { minLapMs: 0, maxLapMs: Number.POSITIVE_INFINITY }; } const minLapMs = Math.max(0, Number(event?.raceConfig?.minLapMs || 0) || 0); const configuredMaxLapMs = Math.max(0, Number(event?.raceConfig?.maxLapMs || 60000) || 60000); const maxLapMs = configuredMaxLapMs > 0 ? Math.max(configuredMaxLapMs, minLapMs || 0) : Number.POSITIVE_INFINITY; return { minLapMs, maxLapMs }; } export function isCountedPassing(passing) { return passing?.validLap !== false; } export function getVisiblePassings(result, { isCountedPassing }) { return Array.isArray(result?.passings) ? result.passings.filter((passing) => isCountedPassing(passing)) : []; } export function getPassingValidationLabel(passing, { t }) { if (passing?.validLap === false) { if (passing.invalidReason === "below_min") { return t("timing.invalid_short"); } if (passing.invalidReason === "manual_invalid") { return t("timing.invalid_manual"); } return t("timing.invalid_long"); } return t("timing.valid_passing"); } export function getSessionTiming(session, nowTs = Date.now(), { getSessionTargetMs }) { const targetMs = getSessionTargetMs(session); const startedAt = Number(session?.startedAt || 0); const elapsedMs = startedAt ? Math.max(0, nowTs - startedAt) : 0; const followUpMs = Math.max(0, Number(session?.followUpSec || 0) || 0) * 1000; const followUpStartedAt = Number(session?.followUpStartedAt || 0) || 0; const followUpActive = Boolean(followUpStartedAt && followUpMs > 0); const followUpRemainingMs = followUpActive ? Math.max(0, followUpMs - Math.max(0, nowTs - followUpStartedAt)) : 0; return { targetMs, elapsedMs, remainingMs: targetMs === null ? null : Math.max(0, targetMs - elapsedMs), untimed: targetMs === null, followUpActive, followUpRemainingMs, }; } export function ensureSessionResult(sessionId, { state }) { if (!state.resultsBySession[sessionId]) { state.resultsBySession[sessionId] = { passings: [], competitors: {}, adjustments: [], }; } if (!Array.isArray(state.resultsBySession[sessionId].adjustments)) { state.resultsBySession[sessionId].adjustments = []; } return state.resultsBySession[sessionId]; } export function buildLeaderboard(session, { ensureSessionResult, getSessionTargetMs, getCompetitorPassings, isCountedPassing, getCompetitorElapsedMs, getCompetitorSeedMetric, getSessionLapWindow, formatSeedMetric, formatRaceClock, formatLeaderboardGap, formatLapDelta, formatLap, t }) { const result = ensureSessionResult(session.id); const sessionType = String(session.type || "").toLowerCase(); const targetMs = getSessionTargetMs(session); const useTargetTieBreak = session.status === "finished"; const useSeedRanking = ["practice", "qualification"].includes(sessionType) && Number(session.seedBestLapCount || 0) > 0; const isFreePractice = sessionType === "free_practice"; const isOpenPractice = sessionType === "open_practice"; const isRollingPractice = isFreePractice || isOpenPractice; const { maxLapMs } = getSessionLapWindow(session); const nowTs = Date.now(); const rows = Object.values(result.competitors).map((row) => { const allPassings = getCompetitorPassings(session, row, { includeInvalid: true }); const passings = allPassings.filter((passing) => isCountedPassing(passing)); const latestAnyPassing = allPassings.length ? allPassings[allPassings.length - 1] : null; const latestPassing = passings.length ? passings[passings.length - 1] : null; const lastPassingTs = latestPassing ? Number(latestPassing.timestamp || 0) : Number(row.lastTimestamp || 0) || 0; const rawElapsedMs = lastPassingTs ? Math.max(0, lastPassingTs - Number(row.startTimestamp || session.startedAt || lastPassingTs)) : getCompetitorElapsedMs(session, row); const manualLapAdjustment = Number(row.manualLapAdjustment || 0) || 0; const manualTimeAdjustmentMs = Number(row.manualTimeAdjustmentMs || 0) || 0; const totalElapsedMs = Math.max(0, rawElapsedMs + manualTimeAdjustmentMs); const distanceToTargetMs = Math.abs(targetMs - totalElapsedMs); const seedMetric = getCompetitorSeedMetric(session, row); const previousLapMs = passings.length >= 2 ? Number(passings[passings.length - 2].lapMs || 0) : null; const lastLapMs = latestPassing ? Number(latestPassing.lapMs || 0) : Number(row.lastLapMs || 0) || 0; const bestLapMs = Number(row.bestLapMs || 0) || 0; const lapDeltaMs = lastLapMs && previousLapMs && lastLapMs > 0 && previousLapMs > 0 ? lastLapMs - previousLapMs : null; const predictionBaseMs = lastLapMs > 0 ? lastLapMs : bestLapMs > 0 ? bestLapMs : null; const currentLapElapsedMs = lastPassingTs ? Math.max(0, nowTs - lastPassingTs) : 0; const predictionStaleMs = predictionBaseMs ? Math.max( predictionBaseMs * 2, Number.isFinite(maxLapMs) && maxLapMs > 0 ? maxLapMs : 0, 15000 ) : 0; const predictionActive = Boolean(predictionBaseMs && lastPassingTs && currentLapElapsedMs <= predictionStaleMs); const predictedRemainingMs = predictionActive ? predictionBaseMs - currentLapElapsedMs : null; const predictedProgress = predictionActive ? currentLapElapsedMs / predictionBaseMs : 0; const predictionTone = !predictionActive || predictedProgress <= 0.85 ? "good" : predictedProgress <= 1 ? "warn" : "late"; const invalidPending = latestAnyPassing?.validLap === false; return { ...row, laps: Math.max(0, Number(row.laps || 0) + manualLapAdjustment), lastLapMs, bestLapMs, lastTimestamp: lastPassingTs || row.lastTimestamp, totalElapsedMs, manualLapAdjustment, manualTimeAdjustmentMs, distanceToTargetMs, seedMetric, previousLapMs, lapDeltaMs, predictedRemainingMs, predictedProgress, predictionTone, invalidPending, invalidLabel: invalidPending ? getPassingValidationLabel(latestAnyPassing) : "", invalidLapMs: invalidPending ? Number(latestAnyPassing?.lapMs || 0) || 0 : 0, comparisonMs: isRollingPractice ? bestLapMs || lastLapMs || Number.MAX_SAFE_INTEGER : useSeedRanking && seedMetric ? seedMetric.comparableMs : totalElapsedMs, resultDisplay: isRollingPractice ? formatLap(bestLapMs || lastLapMs) : useSeedRanking && seedMetric ? formatSeedMetric(seedMetric) : `${Math.max(0, Number(row.laps || 0) + manualLapAdjustment)}/${formatRaceClock(totalElapsedMs)}`, }; }); rows.sort((a, b) => { if (isRollingPractice) { if (a.comparisonMs !== b.comparisonMs) { return a.comparisonMs - b.comparisonMs; } if (b.laps !== a.laps) { return b.laps - a.laps; } return (b.lastTimestamp || 0) - (a.lastTimestamp || 0); } if (useSeedRanking) { if (a.seedMetric && b.seedMetric && a.seedMetric.comparableMs !== b.seedMetric.comparableMs) { return a.seedMetric.comparableMs - b.seedMetric.comparableMs; } if (a.seedMetric && !b.seedMetric) { return -1; } if (!a.seedMetric && b.seedMetric) { return 1; } if (b.laps !== a.laps) { return b.laps - a.laps; } return a.totalElapsedMs - b.totalElapsedMs; } if (b.laps !== a.laps) { return b.laps - a.laps; } if (useTargetTieBreak && a.distanceToTargetMs !== b.distanceToTargetMs) { return a.distanceToTargetMs - b.distanceToTargetMs; } return a.totalElapsedMs - b.totalElapsedMs; }); const leader = rows[0]; return rows.map((row, index) => { if (!leader) { return { ...row, gap: "-", leaderGap: "-", gapAhead: "-", lapDelta: "-" }; } return { ...row, gap: formatLeaderboardGap(row, leader, { useSeedRanking, useTargetTieBreak, isFreePractice: isRollingPractice }), leaderGap: formatLeaderboardGap(row, leader, { useSeedRanking, useTargetTieBreak, isFreePractice: isRollingPractice }), gapAhead: formatLeaderboardGap(row, rows[index - 1], { useSeedRanking, useTargetTieBreak, isFreePractice: isRollingPractice, selfLabel: t("status.leader"), }), lapDelta: formatLapDelta(row.lapDeltaMs), }; }); } export function formatLapDelta(deltaMs, { formatLap }) { if (!deltaMs && deltaMs !== 0) { return "-"; } const sign = deltaMs <= 0 ? "-" : "+"; return `${sign}${(Math.abs(deltaMs) / 1000).toFixed(3)}s`; } export function formatLeaderboardGap(row, referenceRow, options = {}, { t }) { if (!referenceRow) { return "-"; } if (row.key === referenceRow.key) { if (options.isFreePractice) { return t("status.free_practice"); } return options.selfLabel || (options.useSeedRanking ? t("status.seeded") : t("status.leader")); } if (options.isFreePractice) { if (Number.isFinite(row.comparisonMs) && Number.isFinite(referenceRow.comparisonMs)) { return `+${((row.comparisonMs - referenceRow.comparisonMs) / 1000).toFixed(3)}s`; } return "-"; } if (options.useSeedRanking) { if (referenceRow.seedMetric && row.seedMetric) { const seedGap = Math.max(0, row.seedMetric.comparableMs - referenceRow.seedMetric.comparableMs); return `+${(seedGap / 1000).toFixed(3)}s`; } return `+${Math.max(0, (referenceRow.laps || 0) - (row.laps || 0))}L`; } const lapDiff = (referenceRow.laps || 0) - (row.laps || 0); if (lapDiff > 0) { return `+${lapDiff}L`; } const referenceValue = options.useTargetTieBreak ? referenceRow.distanceToTargetMs : referenceRow.totalElapsedMs; const rowValue = options.useTargetTieBreak ? row.distanceToTargetMs : row.totalElapsedMs; const timeGap = Math.max(0, rowValue - referenceValue); return `+${(timeGap / 1000).toFixed(3)}s`; } export function getCompetitorElapsedMs(session, row) { const startTs = Number(row?.startTimestamp || session?.startedAt || 0); if (!startTs || !row?.lastTimestamp) { return 0; } return Math.max(0, row.lastTimestamp - startTs); } export function getCompetitorPassings(session, row, options = {}, { ensureSessionResult, isCountedPassing }) { const result = ensureSessionResult(session.id); return result.passings .filter((passing) => { if (!options.includeInvalid && !isCountedPassing(passing)) { return false; } if (passing.competitorKey) { return passing.competitorKey === row.key; } return ( String(passing.transponder || "") === String(row.transponder || "") && String(passing.driverId || "") === String(row.driverId || "") && String(passing.carId || "") === String(row.carId || "") ); }) .sort((a, b) => a.timestamp - b.timestamp); } export function getCompetitorSeedMetric(session, row, { getCompetitorPassings }) { const lapCount = Math.max(0, Number(session?.seedBestLapCount || 0) || 0); if (lapCount <= 0) { return null; } const method = ["best_sum", "average", "consecutive"].includes(String(session?.seedMethod || "").toLowerCase()) ? String(session.seedMethod).toLowerCase() : "best_sum"; const laps = getCompetitorPassings(session, row) .map((passing) => Number(passing.lapMs || 0)) .filter((lapMs) => lapMs > 500); if (laps.length < lapCount) { return null; } let selected = []; if (method === "consecutive") { let bestWindow = null; for (let index = 0; index <= laps.length - lapCount; index += 1) { const window = laps.slice(index, index + lapCount); const totalMs = window.reduce((sum, lapMs) => sum + lapMs, 0); if (!bestWindow || totalMs < bestWindow.totalMs) { bestWindow = { laps: window, totalMs }; } } if (!bestWindow) { return null; } selected = bestWindow.laps; } else { selected = [...laps].sort((a, b) => a - b).slice(0, lapCount); } const totalMs = selected.reduce((sum, lapMs) => sum + lapMs, 0); const averageMs = totalMs / lapCount; return { lapCount, method, totalMs, averageMs, comparableMs: method === "average" ? averageMs : totalMs, laps: selected, }; } export function getSessionEntrants(session, { state, getEventDrivers }) { const event = state.events.find((item) => item.id === session.eventId); const eventDrivers = event ? getEventDrivers(event) : state.drivers; if (!Array.isArray(session.driverIds) || !session.driverIds.length) { return eventDrivers; } return eventDrivers.filter((driver) => session.driverIds.includes(driver.id)); } export function buildPracticeStandings(event, { getSessionsForEvent, buildLeaderboard, getCompetitorSeedMetric, formatSeedMetric, formatRaceClock }) { const sessions = getSessionsForEvent(event.id).filter((session) => session.type === "practice"); const competitorMap = new Map(); sessions.forEach((session) => { buildLeaderboard(session).forEach((row) => { const key = row.driverId || row.key; const seedMetric = getCompetitorSeedMetric(session, row); const comparableMs = seedMetric?.comparableMs ?? row.bestLapMs ?? row.totalElapsedMs; const current = competitorMap.get(key); if (!current || comparableMs < current.comparableMs) { competitorMap.set(key, { key, driverId: row.driverId, driverName: row.driverName, comparableMs, resultDisplay: seedMetric ? formatSeedMetric(seedMetric) : `${row.laps}/${formatRaceClock(row.totalElapsedMs)}`, sourceSessionName: session.name, }); } }); }); return [...competitorMap.values()].sort((a, b) => a.comparableMs - b.comparableMs).map((row, index) => ({ ...row, rank: index + 1, score: row.resultDisplay, })); } export function getQualifyingPointsValue(place, fieldSize, tableType) { const normalized = ["rank_low", "field_desc", "ifmar"].includes(String(tableType || "").toLowerCase()) ? String(tableType).toLowerCase() : "rank_low"; if (normalized === "field_desc") { return Math.max(1, Number(fieldSize || 0) - place + 1); } if (normalized === "ifmar") { const scale = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; return scale[place - 1] ?? 0; } return place; } export function isHighPointsTable(tableType) { return ["field_desc", "ifmar"].includes(String(tableType || "").toLowerCase()); } export function compareNumberSet(left, right, highWins = false) { for (let i = 0; i < Math.max(left.length, right.length); i += 1) { const leftValue = left[i] ?? (highWins ? -Infinity : Infinity); const rightValue = right[i] ?? (highWins ? -Infinity : Infinity); if (leftValue !== rightValue) { return highWins ? rightValue - leftValue : leftValue - rightValue; } } return 0; } export function buildQualifyingTieBreakNote(row, tieBreak, { t, formatLap }) { if (tieBreak === "best_lap") { return `${t("events.tie_break_note")}: ${t("events.qual_tie_break_best_lap")} • ${formatLap(row.bestSingleLapMs)}`; } if (tieBreak === "best_round") { return `${t("events.tie_break_note")}: ${t("events.qual_tie_break_best_round")} • ${row.bestRoundDisplay || formatLap(row.bestRoundMetric)}`; } return `${t("events.tie_break_note")}: ${t("events.counted_rounds_label")} • ${(row.ranks || []).join(" / ") || "-"}`; } export function hasQualifyingPrimaryTie(left, right, scoringMode) { if (!left || !right) { return false; } if (scoringMode === "points") { return left.totalScore === right.totalScore; } return left.bestRank === right.bestRank; } export function buildQualifyingStandings(event, { getSessionsForEvent, buildLeaderboard, getSessionEntrants, isHighPointsTable, getQualifyingPointsValue, compareNumberSet, buildQualifyingTieBreakNote, formatLap, formatRaceClock, t, hasQualifyingPrimaryTie }) { const sessions = getSessionsForEvent(event.id).filter((session) => session.type === "qualification"); const scoringMode = event.raceConfig?.qualifyingScoring || "points"; const countedRounds = Math.max(1, Number(event.raceConfig?.countedQualRounds || 1) || 1); const pointsTable = event.raceConfig?.qualifyingPointsTable || "rank_low"; const highPointsWin = isHighPointsTable(pointsTable); const tieBreak = event.raceConfig?.qualifyingTieBreak || "rounds"; const competitorMap = new Map(); sessions.forEach((session) => { const rows = buildLeaderboard(session); const entrantCount = Math.max(rows.length, getSessionEntrants(session).length || 0); rows.forEach((row, index) => { const key = row.driverId || row.key; if (!competitorMap.has(key)) { competitorMap.set(key, { key, driverId: row.driverId, driverName: row.driverName, points: [], ranks: [], roundMetrics: [], bestLaps: [], }); } const entry = competitorMap.get(key); entry.points.push(getQualifyingPointsValue(index + 1, entrantCount, pointsTable)); entry.ranks.push(index + 1); entry.roundMetrics.push(row.comparisonMs); if (Number.isFinite(row.bestLapMs)) { entry.bestLaps.push(row.bestLapMs); } if (!entry.bestRoundDisplay || row.comparisonMs < (entry.bestRoundMetricValue ?? Number.MAX_SAFE_INTEGER)) { entry.bestRoundMetricValue = row.comparisonMs; entry.bestRoundDisplay = row.resultDisplay; } entry.bestResultDisplay = row.resultDisplay; entry.lastSessionName = session.name; entry.sessionCount = (entry.sessionCount || 0) + 1; entry.maxFieldSize = entrantCount; }); }); const rows = [...competitorMap.values()].map((entry) => { const sortedPoints = [...entry.points].sort((a, b) => (highPointsWin ? b - a : a - b)); const counted = sortedPoints.slice(0, countedRounds); const totalScore = counted.reduce((sum, value) => sum + value, 0); const bestRank = Math.min(...entry.ranks); const bestRoundMetric = Math.min(...entry.roundMetrics); const bestSingleLapMs = entry.bestLaps.length ? Math.min(...entry.bestLaps) : Number.MAX_SAFE_INTEGER; return { key: entry.key, driverId: entry.driverId, driverName: entry.driverName, ranks: [...entry.ranks].sort((a, b) => a - b), countedPoints: counted, totalScore, bestRank, bestRoundMetric, bestRoundDisplay: entry.bestRoundDisplay || formatLap(bestRoundMetric), bestSingleLapMs, score: scoringMode === "points" ? `${totalScore} (${counted.join("+")})` : `${bestRank} / ${formatRaceClock(bestRoundMetric)}`, }; }); rows.sort((a, b) => { if (scoringMode === "points") { if (a.totalScore !== b.totalScore) { return highPointsWin ? b.totalScore - a.totalScore : a.totalScore - b.totalScore; } if (tieBreak === "best_lap" && a.bestSingleLapMs !== b.bestSingleLapMs) { return a.bestSingleLapMs - b.bestSingleLapMs; } if (tieBreak === "best_round" && a.bestRoundMetric !== b.bestRoundMetric) { return a.bestRoundMetric - b.bestRoundMetric; } const pointDiff = compareNumberSet(a.countedPoints, b.countedPoints, highPointsWin); if (pointDiff !== 0) { return pointDiff; } return a.bestRoundMetric - b.bestRoundMetric; } if (a.bestRank !== b.bestRank) { return a.bestRank - b.bestRank; } if (tieBreak === "rounds") { const rankDiff = compareNumberSet(a.ranks, b.ranks, false); if (rankDiff !== 0) { return rankDiff; } } if (tieBreak === "best_lap" && a.bestSingleLapMs !== b.bestSingleLapMs) { return a.bestSingleLapMs - b.bestSingleLapMs; } return a.bestRoundMetric - b.bestRoundMetric; }); rows.forEach((row, index) => { row.tieBreakWonAgainst = ""; row.tieBreakLostAgainst = ""; if (index === 0) { return; } const previous = rows[index - 1]; if (!hasQualifyingPrimaryTie(previous, row, scoringMode)) { return; } previous.tieBreakWonAgainst = previous.tieBreakWonAgainst || row.driverName || t("common.unknown_driver"); row.tieBreakLostAgainst = row.tieBreakLostAgainst || previous.driverName || t("common.unknown_driver"); }); return rows.map((row, index) => ({ ...row, rank: index + 1, scoreNote: [ buildQualifyingTieBreakNote(row, tieBreak), row.tieBreakWonAgainst ? `${t("events.tie_break_won")}: ${row.tieBreakWonAgainst}` : "", row.tieBreakLostAgainst ? `${t("events.tie_break_lost")}: ${row.tieBreakLostAgainst}` : "", ] .filter(Boolean) .join(" • "), })); } export function formatTeamActiveMemberLabel(rowOrPassing) { if (!rowOrPassing) { return "-"; } const parts = [rowOrPassing.driverName || "", rowOrPassing.carName || ""].filter(Boolean); return parts.join(" • ") || rowOrPassing.subLabel || "-"; } export function buildTeamRaceStandings(event, { getSessionsForEvent, buildLeaderboard, getSessionSortWeight }) { return getSessionsForEvent(event.id) .filter((session) => session.type === "team_race") .sort((left, right) => getSessionSortWeight(left) - getSessionSortWeight(right) || String(left.name).localeCompare(String(right.name))) .map((session) => ({ session, rows: buildLeaderboard(session), })); } export function buildTeamStintLog(session, row, { getCompetitorPassings, getSessionLapWindow, formatTeamActiveMemberLabel }) { const passings = getCompetitorPassings(session, row); if (!passings.length) { return []; } const { maxLapMs } = getSessionLapWindow(session); const stintGapMs = Number.isFinite(maxLapMs) ? maxLapMs : Number.POSITIVE_INFINITY; const stints = []; let current = null; passings.forEach((passing) => { const memberLabel = formatTeamActiveMemberLabel(passing); const memberKey = `${passing.driverId || passing.driverName || "-"}|${passing.carId || passing.carName || "-"}`; const gapBreak = current && Number.isFinite(stintGapMs) && Math.max(0, passing.timestamp - current.endTs) > stintGapMs; if (!current || current.memberKey !== memberKey || gapBreak) { current = { memberKey, memberLabel, driverName: passing.driverName || "-", carName: passing.carName || "-", startTs: passing.timestamp, endTs: passing.timestamp, laps: 1, }; stints.push(current); return; } current.endTs = passing.timestamp; current.laps += 1; }); return stints.map((stint, index) => ({ ...stint, index: index + 1, durationMs: Math.max(0, stint.endTs - stint.startTs), })); } export function getSessionGridEntries(session, { state, t, getEventTeams, getDriverDisplayById, getSessionGridOrder }) { if (!session) { return []; } if (session.mode === "track") { return (session.assignments || []).map((assignment, index) => { const driver = state.drivers.find((item) => item.id === assignment.driverId); const car = state.cars.find((item) => item.id === assignment.carId); return { slot: index + 1, name: driver?.name || t("common.unknown_driver"), meta: car ? `${car.name} (${car.transponder || "-"})` : t("common.unknown_car"), }; }); } if (session.type === "team_race") { const event = state.events.find((item) => item.id === session.eventId); return getEventTeams(event).map((team, index) => ({ slot: index + 1, name: team.name, meta: team.driverIds.map((driverId) => getDriverDisplayById(driverId)).join(", ") || team.carIds .map((carId) => { const car = state.cars.find((item) => item.id === carId); return car ? `${car.name} (${car.transponder || "-"})` : ""; }) .filter(Boolean) .join(", ") || "-", })); } return getSessionGridOrder(session).map((driverId, index) => { const driver = state.drivers.find((item) => item.id === driverId); return { slot: index + 1, name: driver?.name || t("common.unknown_driver"), meta: driver?.transponder || "-", }; }); } export function getSessionGridOrder(session) { if (!session) { return []; } if (Array.isArray(session.manualGridIds) && session.manualGridIds.length) { return session.manualGridIds; } return Array.isArray(session.driverIds) ? session.driverIds : []; } export function ensureSessionDriverOrder(session, { getSessionEntrants }) { if (!session || session.mode !== "race") { return []; } if (!Array.isArray(session.driverIds) || !session.driverIds.length) { session.driverIds = getSessionEntrants(session) .map((driver) => driver.id) .filter(Boolean); } if (!Array.isArray(session.manualGridIds) || !session.manualGridIds.length) { session.manualGridIds = [...session.driverIds]; } return session.manualGridIds; } export function buildFinalStandings(event, { getSessionsForEvent, buildLeaderboard }) { const sessions = getSessionsForEvent(event.id).filter((session) => session.type === "final"); const groupedByMain = new Map(); const countedFinalLegs = Math.max(1, Number(event.raceConfig?.countedFinalLegs || 1) || 1); sessions.forEach((session) => { const rows = buildLeaderboard(session); const mainMatch = String(session.name || "").match(/^([A-Z])/i); const mainKey = mainMatch ? mainMatch[1].toUpperCase() : "A"; if (!groupedByMain.has(mainKey)) { groupedByMain.set(mainKey, new Map()); } const competitorMap = groupedByMain.get(mainKey); rows.forEach((row, index) => { const key = row.driverId || row.key; if (!competitorMap.has(key)) { competitorMap.set(key, { key, mainKey, driverId: row.driverId, driverName: row.driverName, legRanks: [], bestElapsedMs: [], }); } const entry = competitorMap.get(key); entry.legRanks.push(index + 1); entry.bestElapsedMs.push(row.totalElapsedMs); }); }); const mains = [...groupedByMain.entries()].sort((a, b) => a[0].localeCompare(b[0])); const rows = []; mains.forEach(([mainKey, competitorMap], mainIndex) => { const mainRows = [...competitorMap.values()].map((entry) => { const sortedRanks = [...entry.legRanks].sort((a, b) => a - b); const countedRanks = sortedRanks.slice(0, countedFinalLegs); return { mainKey, driverId: entry.driverId, driverName: entry.driverName, finalScore: countedRanks.reduce((sum, value) => sum + value, 0), legRanks: sortedRanks, bestElapsedMs: Math.min(...entry.bestElapsedMs), }; }); mainRows.sort((a, b) => { if (a.finalScore !== b.finalScore) { return a.finalScore - b.finalScore; } for (let i = 0; i < Math.max(a.legRanks.length, b.legRanks.length); i += 1) { const left = a.legRanks[i] ?? 999; const right = b.legRanks[i] ?? 999; if (left !== right) { return left - right; } } return a.bestElapsedMs - b.bestElapsedMs; }); mainRows.forEach((row, rowIndex) => { rows.push({ rank: `${mainKey}${rowIndex + 1}`, driverId: row.driverId, driverName: row.driverName, score: `${row.finalScore} (${row.legRanks.join("+")})`, orderingGroup: mainIndex, orderingIndex: rowIndex, }); }); }); return rows.sort((a, b) => { if (a.orderingGroup !== b.orderingGroup) { return a.orderingGroup - b.orderingGroup; } return a.orderingIndex - b.orderingIndex; }); }