diff --git a/client/src/components/ResultStat/ResultStat.jsx b/client/src/components/ResultStat/ResultStat.jsx new file mode 100644 index 00000000..9ce6f4d2 --- /dev/null +++ b/client/src/components/ResultStat/ResultStat.jsx @@ -0,0 +1,61 @@ +import { Box, Tooltip } from "@mui/material"; +import { + bestPossibleAverage, + worstPossibleAverage, + formatAttemptResult, + incompleteMean, +} from "../../lib/attempt-result"; +import { shouldComputeAverage } from "../../lib/result"; + +function ResultStat({ result, field, eventId, format }) { + if ( + field === "average" && + result.average === 0 && + shouldComputeAverage(eventId, format.numberOfAttempts) + ) { + const attemptResults = result.attempts.map((attempt) => attempt.result); + + if (format.numberOfAttempts === 5 && result.attempts.length === 4) { + return ( + + + + {formatAttemptResult( + bestPossibleAverage(attemptResults), + eventId + )} + + + {" / "} + + + {formatAttemptResult( + worstPossibleAverage(attemptResults), + eventId + )} + + + + ); + } + + if (format.numberOfAttempts === 3 && result.attempts.length === 2) { + return ( + + + + {formatAttemptResult( + incompleteMean(attemptResults, eventId), + eventId + )} + + + + ); + } + } + + return formatAttemptResult(result[field], eventId); +} + +export default ResultStat; diff --git a/client/src/components/ResultsProjector/ResultsProjector.jsx b/client/src/components/ResultsProjector/ResultsProjector.jsx index 7e48cefc..5a18c09a 100644 --- a/client/src/components/ResultsProjector/ResultsProjector.jsx +++ b/client/src/components/ResultsProjector/ResultsProjector.jsx @@ -22,6 +22,7 @@ import { times } from "../../lib/utils"; import { formatAttemptResult } from "../../lib/attempt-result"; import { orderedResultStats, paddedAttemptResults } from "../../lib/result"; import RecordTagBadge from "../RecordTagBadge/RecordTagBadge"; +import ResultStat from "../ResultStat/ResultStat"; const styles = { cell: { @@ -223,7 +224,12 @@ function ResultsProjector({ results, format, eventId, title, exitUrl }) { recordTag={result[recordTagField]} hidePr > - {formatAttemptResult(result[field], eventId)} + ))} diff --git a/client/src/components/RoundResults/RoundResultsTable.jsx b/client/src/components/RoundResults/RoundResultsTable.jsx index a6cddbdb..5f5c8873 100644 --- a/client/src/components/RoundResults/RoundResultsTable.jsx +++ b/client/src/components/RoundResults/RoundResultsTable.jsx @@ -15,6 +15,7 @@ import { times } from "../../lib/utils"; import { formatAttemptResult } from "../../lib/attempt-result"; import { orderedResultStats, paddedAttemptResults } from "../../lib/result"; import RecordTagBadge from "../RecordTagBadge/RecordTagBadge"; +import ResultStat from "../ResultStat/ResultStat"; const styles = { cell: { @@ -130,7 +131,12 @@ const RoundResultsTable = memo( }} > - {formatAttemptResult(result[field], eventId)} + ))} diff --git a/client/src/components/admin/AdminRound/AdminResultsTable.jsx b/client/src/components/admin/AdminRound/AdminResultsTable.jsx index b2b02813..37cb600a 100644 --- a/client/src/components/admin/AdminRound/AdminResultsTable.jsx +++ b/client/src/components/admin/AdminRound/AdminResultsTable.jsx @@ -14,6 +14,7 @@ import { times } from "../../../lib/utils"; import { formatAttemptResult } from "../../../lib/attempt-result"; import { orderedResultStats, paddedAttemptResults } from "../../../lib/result"; import RecordTagBadge from "../../RecordTagBadge/RecordTagBadge"; +import ResultStat from "../../ResultStat/ResultStat"; const styles = { ranking: { @@ -146,7 +147,12 @@ const AdminResultsTable = memo( sx={{ fontWeight: index === 0 ? 600 : 400 }} > - {formatAttemptResult(result[field], eventId)} + ))} diff --git a/client/src/lib/attempt-result.js b/client/src/lib/attempt-result.js index fd3770ac..27f68bca 100644 --- a/client/src/lib/attempt-result.js +++ b/client/src/lib/attempt-result.js @@ -85,7 +85,7 @@ export function average(attemptResults, eventId) { return averageOf5(scaled); default: throw new Error( - `Invalid number of attempt results, expected 3 or 5, given ${attemptResults.length}.` + `Invalid number of attempt results, expected 3 or 5, got ${attemptResults.length}.` ); } } @@ -97,7 +97,7 @@ export function average(attemptResults, eventId) { return roundOver10Mins(averageOf5(attemptResults)); default: throw new Error( - `Invalid number of attempt results, expected 3 or 5, given ${attemptResults.length}.` + `Invalid number of attempt results, expected 3 or 5, got ${attemptResults.length}.` ); } } @@ -116,11 +116,76 @@ function averageOf5(attemptResults) { function meanOf3(attemptResults) { if (!attemptResults.every(isComplete)) return DNF_VALUE; - return mean(...attemptResults); + return mean(attemptResults); } -function mean(x, y, z) { - return Math.round((x + y + z) / 3); +function mean(values) { + const sum = values.reduce((x, y) => x + y, 0); + return Math.round(sum / values.length); +} + +/** + * Calculates the best possible average of 5 for the given attempts. + * + * Expects exactly 4 attempt results to be given. + * + * @example + * bestPossibleAverage([3642, 3102, 3001, 2992]); // => 3032 + * bestPossibleAverage([6111, -1, -1, 6000]); // => -1 + * bestPossibleAverage([4822, 4523, 4233, -1]; // => 4526 + */ +export function bestPossibleAverage(attemptResults) { + if (attemptResults.length !== 4) { + throw new Error( + `Invalid number of attempt results, expected 4, got ${attemptResults.length}.` + ); + } + + const [x, y, z] = attemptResults.slice().sort(compareAttemptResults); + const mean = meanOf3([x, y, z]); + return roundOver10Mins(mean); +} + +/** + * Calculates the worst possible average of 5 for the given attempts. + * + * Expects exactly 4 attempt results to be given. + * + * @example + * worstPossibleAverage([3642, 3102, 3001, 2992]); // => 3248 + * worstPossibleAverage([6111, -1, -1, 6000]); // => -1 + * worstPossibleAverage([6111, -1, 6000, 5999]); // => -1 + */ +export function worstPossibleAverage(attemptResults) { + if (attemptResults.length !== 4) { + throw new Error( + `Invalid number of attempt results, expected 4, got ${attemptResults.length}.` + ); + } + + const [, x, y, z] = attemptResults.slice().sort(compareAttemptResults); + const mean = meanOf3([x, y, z]); + return roundOver10Mins(mean); +} + +/** + * Calculates mean of 2 for the given attempt results. + */ +export function incompleteMean(attemptResults, eventId) { + if (attemptResults.length !== 2) { + throw new Error( + `Invalid number of attempt results, expected 2, got ${attemptResults.length}.` + ); + } + + if (!attemptResults.every(isComplete)) return DNF_VALUE; + + if (eventId === "333fm") { + const scaled = attemptResults.map((attemptResult) => attemptResult * 100); + return mean(scaled); + } + + return roundOver10Mins(mean(attemptResults)); } /** diff --git a/client/src/lib/tests/attempt-result.test.js b/client/src/lib/tests/attempt-result.test.js index 754e87ea..21c39048 100644 --- a/client/src/lib/tests/attempt-result.test.js +++ b/client/src/lib/tests/attempt-result.test.js @@ -1,5 +1,6 @@ import { best, + bestPossibleAverage, average, formatAttemptResult, decodeMbldAttemptResult, @@ -12,6 +13,8 @@ import { applyTimeLimit, applyCutoff, isWorldRecord, + worstPossibleAverage, + incompleteMean, } from "../attempt-result"; describe("best", () => { @@ -50,7 +53,7 @@ describe("average", () => { it("throws an error if the number of attempt results is neither 3 nor 5", () => { expect(() => { average([1100, 900], "333"); - }).toThrow("Invalid number of attempt results, expected 3 or 5, given 2."); + }).toThrow("Invalid number of attempt results, expected 3 or 5, got 2."); }); it("returns 0 (skipped) for 3x3x3 Multi-Blind", () => { @@ -554,3 +557,39 @@ describe("applyCutoff", () => { expect(applyCutoff(attempts, cutoff)).toEqual([1000, 799, 1200, 1000, 900]); }); }); + +describe("worstPossibleAverage", () => { + it("returns -1 if any attempt result is DNF", () => { + const attemptResults = [1000, -1, 1200, 1300]; + expect(worstPossibleAverage(attemptResults)).toEqual(-1); + }); + + it("calculates average of 5 assuming worst attempt result", () => { + const attemptResults = [3642, 3102, 3001, 2992]; + expect(worstPossibleAverage(attemptResults)).toEqual(3248); + }); +}); + +describe("bestPossibleAverage", () => { + it("returns -1 if two attempts result are DNFs", () => { + const attemptResults = [1000, -1, 1200, -1]; + expect(bestPossibleAverage(attemptResults)).toEqual(-1); + }); + + it("calculates average of 5 assuming best attempt result", () => { + const attemptResults = [3642, 3102, 3001, 2992]; + expect(bestPossibleAverage(attemptResults)).toEqual(3032); + }); +}); + +describe("incompleteMean", () => { + it("returns -1 if any attempt result is DNF", () => { + const attemptResults = [21, -1]; + expect(incompleteMean(attemptResults, "333fm")).toEqual(-1); + }); + + it("calculates mean of 2", () => { + const attemptResults = [21, 23]; + expect(incompleteMean(attemptResults, "333fm")).toEqual(2200); + }); +});