Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions frontend/src/ts/controllers/input-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1182,11 +1182,9 @@ $("#wordsInput").on("keydown", (event) => {
}

const now = performance.now();
setTimeout(() => {
const eventCode =
event.code === "" || event.key === "Unidentified" ? "NoCode" : event.code;
TestInput.recordKeydownTime(now, eventCode);
}, 0);
const eventCode =
event.code === "" || event.key === "Unidentified" ? "NoCode" : event.code;
TestInput.recordKeydownTime(now, eventCode);
});

$("#wordsInput").on("keyup", (event) => {
Expand All @@ -1213,11 +1211,9 @@ $("#wordsInput").on("keyup", (event) => {
}

const now = performance.now();
setTimeout(() => {
const eventCode =
event.code === "" || event.key === "Unidentified" ? "NoCode" : event.code;
TestInput.recordKeyupTime(now, eventCode);
}, 0);
const eventCode =
event.code === "" || event.key === "Unidentified" ? "NoCode" : event.code;
TestInput.recordKeyupTime(now, eventCode);
});

$("#wordsInput").on("keyup", (event) => {
Expand Down
82 changes: 64 additions & 18 deletions frontend/src/ts/test/test-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,16 +262,37 @@ export function forceKeyup(now: number): void {
//using mean here because for words mode, the last keypress ends the test.
//if we then force keyup on that last keypress, it will record a duration of 0
//skewing the average and standard deviation
const avg = roundTo2(mean(keypressTimings.duration.array));
const keysOrder = Object.entries(keyDownData);
keysOrder.sort((a, b) => a[1].timestamp - b[1].timestamp);
for (const keyOrder of keysOrder) {
recordKeyupTime(now, keyOrder[0]);

const indexesToRemove = new Set(
Object.values(keyDownData).map((data) => data.index)
);

const keypressDurations = keypressTimings.duration.array.filter(
(_, index) => !indexesToRemove.has(index)
);
if (keypressDurations.length === 0) {
// this means the test ended while all keys were still held - probably safe to ignore
// since this will result in a "too short" test anyway
return;
}
const last = lastElementFromArray(keysOrder)?.[0] as string;
const index = keyDownData[last]?.index;
if (last !== undefined && index !== undefined) {

const avg = roundTo2(mean(keypressDurations));

const orderedKeys = Object.entries(keyDownData).sort(
(a, b) => a[1].timestamp - b[1].timestamp
);

for (const [key, { index }] of orderedKeys) {
keypressTimings.duration.array[index] = avg;

if (key === "NoCode") {
noCodeIndex--;
}

// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete keyDownData[key];

updateOverlap(now);
}
}

Expand Down Expand Up @@ -350,6 +371,21 @@ function updateOverlap(now: number): void {
}

export function resetKeypressTimings(): void {
//because keydown triggers before input, we need to grab the first keypress data here and carry it over

//take the key with the largest index
const lastKey = Object.keys(keyDownData).reduce((a, b) => {
const aIndex = keyDownData[a]?.index;
const bIndex = keyDownData[b]?.index;
if (aIndex === undefined) return b;
if (bIndex === undefined) return a;
return aIndex > bIndex ? a : b;
});

//get the data
const lastKeyData = keyDownData[lastKey];

//reset
keypressTimings = {
spacing: {
first: -1,
Expand All @@ -366,6 +402,26 @@ export function resetKeypressTimings(): void {
};
keyDownData = {};
noCodeIndex = 0;

//carry over
if (lastKeyData !== undefined) {
keypressTimings = {
spacing: {
first: lastKeyData.timestamp,
last: lastKeyData.timestamp,
array: [],
},
duration: {
array: [0],
},
};
keyDownData[lastKey] = {
timestamp: lastKeyData.timestamp,
// make sure to set it to the first index
index: 0,
};
}

console.debug("Keypress timings reset");
}

Expand Down Expand Up @@ -413,14 +469,4 @@ export function restart(): void {
correct: 0,
incorrect: 0,
};
keypressTimings = {
spacing: {
first: -1,
last: -1,
array: [],
},
duration: {
array: [],
},
};
}
2 changes: 1 addition & 1 deletion frontend/src/ts/test/test-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,7 @@ export async function finish(difficultyFailed = false): Promise<void> {
}

// stats
const stats = TestStats.calculateStats();
const stats = TestStats.calculateFinalStats();
if (stats.time % 1 !== 0 && Config.mode !== "time") {
TestStats.setLastSecondNotRound();
}
Expand Down
26 changes: 19 additions & 7 deletions frontend/src/ts/test/test-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as TestState from "./test-state";
import * as Numbers from "@monkeytype/util/numbers";
import { CompletedEvent, IncompleteTest } from "@monkeytype/schemas/results";
import { isFunboxActiveWithProperty } from "./funbox/list";
import * as CustomText from "./custom-text";

type CharCount = {
spaces: number;
Expand Down Expand Up @@ -144,14 +145,17 @@ export function calculateTestSeconds(now?: number): number {
}
}

export function calculateWpmAndRaw(withDecimalPoints?: true): {
export function calculateWpmAndRaw(
withDecimalPoints?: true,
final = false
): {
wpm: number;
raw: number;
} {
const testSeconds = calculateTestSeconds(
TestState.isActive ? performance.now() : end
);
const chars = countChars();
const chars = countChars(final);
const wpm = Numbers.roundTo2(
((chars.correctWordChars + chars.correctSpaces) * (60 / testSeconds)) / 5
);
Expand Down Expand Up @@ -280,7 +284,7 @@ function getTargetWords(): string[] {
return targetWords;
}

function countChars(): CharCount {
function countChars(final = false): CharCount {
let correctWordChars = 0;
let correctChars = 0;
let incorrectChars = 0;
Expand Down Expand Up @@ -343,7 +347,13 @@ function countChars(): CharCount {
}
correctChars += toAdd.correct;
incorrectChars += toAdd.incorrect;
if (i === inputWords.length - 1) {

const isTimedTest =
Config.mode === "time" ||
(Config.mode === "custom" && CustomText.getLimit().mode === "time");
const shouldCountPartialLastWord = !final || (final && isTimedTest);

if (i === inputWords.length - 1 && shouldCountPartialLastWord) {
//last word - check if it was all correct - add to correct word chars
if (toAdd.incorrect === 0) correctWordChars += toAdd.correct;
} else {
Expand All @@ -370,7 +380,7 @@ function countChars(): CharCount {
};
}

export function calculateStats(): Stats {
export function calculateFinalStats(): Stats {
console.debug("Calculating result stats");
let testSeconds = calculateTestSeconds();
console.debug(
Expand Down Expand Up @@ -398,8 +408,10 @@ export function calculateStats(): Stats {
testSeconds
);
}
const chars = countChars();
const { wpm, raw } = calculateWpmAndRaw(true);

//todo: this counts chars twice - once here and once in calculateWpmAndRaw
const chars = countChars(true);
const { wpm, raw } = calculateWpmAndRaw(true, true);
const acc = Numbers.roundTo2(calculateAccuracy());
const ret = {
wpm: isNaN(wpm) ? 0 : wpm,
Expand Down
Loading