import { Score, Team, TeamResult, Criterion, CriterionStats } from '../shared/types.js'; /** * Calculate the aggregated result for a single team. * * - total = sum of all score.value where score.teamId matches * - jurorCount = count of distinct jurorIds that have submitted ALL criteria (criteriaCount scores) * - average = total / jurorCount (or 0 if jurorCount is 0) * - stddev = standard deviation of per-juror totals * - complete = jurorCount equals total number of jurors who have at least one score for this team * AND all have all criteria */ export function calculateTeamResult( teamId: string, teamName: string, scores: Score[], criteriaCount: number, criteria?: Criterion[] ): TeamResult { const teamScores = scores.filter((s) => s.teamId === teamId); const total = teamScores.reduce((sum, s) => sum + s.value, 0); // Group scores by jurorId const scoresByJuror = new Map(); for (const score of teamScores) { const existing = scoresByJuror.get(score.jurorId) || []; existing.push(score); scoresByJuror.set(score.jurorId, existing); } // Count jurors who have submitted ALL criteria const jurorCount = Array.from(scoresByJuror.values()).filter( (jurorScores) => jurorScores.length >= criteriaCount ).length; const average = jurorCount > 0 ? total / jurorCount : 0; // Calculate stddev of per-juror totals const jurorTotals = Array.from(scoresByJuror.values()) .filter((jurorScores) => jurorScores.length >= criteriaCount) .map((jurorScores) => jurorScores.reduce((sum, s) => sum + s.value, 0)); let stddev = 0; if (jurorTotals.length > 1) { const mean = jurorTotals.reduce((a, b) => a + b, 0) / jurorTotals.length; const variance = jurorTotals.reduce((sum, v) => sum + (v - mean) ** 2, 0) / jurorTotals.length; stddev = Math.sqrt(variance); } // Complete = all jurors who have at least one score have submitted all criteria const totalJurorsWithScores = scoresByJuror.size; const complete = totalJurorsWithScores > 0 && jurorCount === totalJurorsWithScores; // Per-criterion stats let criteriaStats: CriterionStats[] | undefined; if (criteria) { criteriaStats = criteria.map((criterion) => { const criterionScores = teamScores .filter((s) => s.criterionId === criterion.id) .map((s) => s.value); const cMean = criterionScores.length > 0 ? criterionScores.reduce((a, b) => a + b, 0) / criterionScores.length : 0; let cStddev = 0; if (criterionScores.length > 1) { const cVariance = criterionScores.reduce((sum, v) => sum + (v - cMean) ** 2, 0) / criterionScores.length; cStddev = Math.sqrt(cVariance); } return { criterionId: criterion.id, criterionName: criterion.nameDE, mean: cMean, stddev: cStddev, scores: criterionScores, }; }); } return { teamId, teamName, total, average, stddev, rank: 0, // Will be assigned by calculateRankings jurorCount, complete, criteriaStats, }; } /** * Sort team results by total descending and assign dense ranking. * Ties get the same rank. * Example: totals [250, 240, 240, 230] → ranks [1, 2, 2, 4] */ export function calculateRankings(teamResults: TeamResult[]): TeamResult[] { // Sort by total descending const sorted = [...teamResults].sort((a, b) => b.total - a.total); // Assign dense ranking with ties let currentRank = 1; for (let i = 0; i < sorted.length; i++) { if (i > 0 && sorted[i].total < sorted[i - 1].total) { currentRank = i + 1; } sorted[i] = { ...sorted[i], rank: currentRank }; } return sorted; } /** * Get the completion status for a specific juror-team pair. * * - 0 scores → 'pending' * - 1 to criteriaCount-1 → 'partial' * - criteriaCount → 'complete' */ export function getCompletionStatus( jurorId: string, teamId: string, scores: Score[], criteriaCount: number ): 'complete' | 'partial' | 'pending' { const count = scores.filter( (s) => s.jurorId === jurorId && s.teamId === teamId ).length; if (count === 0) return 'pending'; if (count >= criteriaCount) return 'complete'; return 'partial'; } /** * Calculate results for all teams and apply rankings. * Convenience function combining calculateTeamResult + calculateRankings. */ export function calculateAllResults( scores: Score[], teams: Team[], criteriaCount: number, criteria?: Criterion[] ): TeamResult[] { const teamResults = teams.map((team) => calculateTeamResult(team.id, team.name, scores, criteriaCount, criteria) ); return calculateRankings(teamResults); }