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);
+ });
+});