From 070d933e385ddd9a087606f3e239a97277d61d9c Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 4 Jun 2026 22:20:09 +0200 Subject: [PATCH 1/3] fix: dont double count newline characters --- frontend/src/ts/test/test-stats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 807962f76b3e..b177a464439a 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -328,7 +328,7 @@ function countChars(final = false): CharCount { missedChars += toAdd.missed; } } - if (i < inputWords.length - 1) { + if (i < inputWords.length - 1 && Strings.getLastChar(inputWord) !== "\n") { spaces++; } } From 862177d948869cd583e5cc7ee16144cc1b64fbb9 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 4 Jun 2026 23:54:22 +0200 Subject: [PATCH 2/3] chore: more tests --- frontend/__tests__/utils/strings.spec.ts | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index 064dbe9856ee..c76d93f8f27c 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -993,6 +993,54 @@ describe("string utils", () => { missed: 0, }, }, + { + description: "correctly count incorrect newlines", + input: { + inputWord: "hello ", + targetWord: "hello\n", + lastWord: false, + shouldLastPartialWordCount: false, + }, + expected: { + allCorrect: 5, + correctWord: 0, + incorrect: 1, + extra: 0, + missed: 0, + }, + }, + { + description: "partial correct, with space", + input: { + inputWord: "helxx ", + targetWord: "hello ", + lastWord: false, + shouldLastPartialWordCount: false, + }, + expected: { + allCorrect: 3, + correctWord: 0, + incorrect: 3, + extra: 0, + missed: 0, + }, + }, + { + description: "newlines", + input: { + inputWord: "hello\n", + targetWord: "hello\n", + lastWord: false, + shouldLastPartialWordCount: false, + }, + expected: { + allCorrect: 6, + correctWord: 6, + incorrect: 0, + extra: 0, + missed: 0, + }, + }, ]; it.each(testCases)("$description", ({ input, expected }) => { From f301469eaf03b9822f17dc3102315850dff20bdb Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 4 Jun 2026 23:58:07 +0200 Subject: [PATCH 3/3] chore: use util function, --- frontend/src/ts/test/test-logic.ts | 2 +- frontend/src/ts/test/test-stats.ts | 132 +++++++++-------------------- 2 files changed, 43 insertions(+), 91 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 88e18ae8bc36..8979f2faf244 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -865,7 +865,7 @@ function buildCompletedEvent( wpm: stats.wpm, rawWpm: stats.wpmRaw, charStats: [ - stats.correctChars + stats.correctSpaces, + stats.correctChars, stats.incorrectChars, stats.extraChars, stats.missedChars, diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index b177a464439a..bc86e0073edb 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -1,22 +1,20 @@ import Hangul from "hangul-js"; import { Config } from "../config/store"; -import * as Strings from "../utils/strings"; import * as TestInput from "./test-input"; import * as TestWords from "./test-words"; import * as TestState from "./test-state"; import * as Numbers from "@monkeytype/util/numbers"; -import { isFunboxActiveWithProperty } from "./funbox/list"; import * as CustomText from "./custom-text"; import { getLastResult } from "../states/test"; +import { countChars as countCharsUtils, getLastChar } from "../utils/strings"; +import { isFunboxActiveWithProperty } from "./funbox/list"; type CharCount = { - spaces: number; correctWordChars: number; allCorrectChars: number; incorrectChars: number; extraChars: number; missedChars: number; - correctSpaces: number; }; export type Stats = { @@ -29,8 +27,6 @@ export type Stats = { extraChars: number; allChars: number; time: number; - spaces: number; - correctSpaces: number; }; export let start: number, end: number; @@ -130,13 +126,10 @@ export function calculateWpmAndRaw( const chars = countChars(final); const wpm = Numbers.roundTo2( - ((chars.correctWordChars + chars.correctSpaces) * (60 / testSeconds)) / 5, + (chars.correctWordChars * (60 / testSeconds)) / 5, ); const raw = Numbers.roundTo2( - ((chars.allCorrectChars + - chars.spaces + - chars.incorrectChars + - chars.extraChars) * + ((chars.allCorrectChars + chars.incorrectChars + chars.extraChars) * (60 / testSeconds)) / 5, ); @@ -225,6 +218,15 @@ function getInputWords(): string[] { inputWords = inputWords.map((w) => Hangul.disassemble(w).join("")); } + for (let i = 0; i < inputWords.length - 1; i++) { + if ( + getLastChar(inputWords[i] as string) !== "\n" && + !isFunboxActiveWithProperty("nospace") + ) { + inputWords[i] += " "; + } + } + return inputWords; } @@ -249,6 +251,15 @@ function getTargetWords(): string[] { targetWords = targetWords.map((w) => Hangul.disassemble(w).join("")); } + for (let i = 0; i < targetWords.length - 1; i++) { + if ( + getLastChar(targetWords[i] as string) !== "\n" && + !isFunboxActiveWithProperty("nospace") + ) { + targetWords[i] += " "; + } + } + return targetWords; } @@ -258,93 +269,40 @@ function countChars(final = false): CharCount { let incorrectChars = 0; let extraChars = 0; let missedChars = 0; - let spaces = 0; - let correctspaces = 0; const inputWords = getInputWords(); const targetWords = getTargetWords(); + const isTimedTest = + Config.mode === "time" || + (Config.mode === "custom" && CustomText.getLimit().mode === "time"); + for (let i = 0; i < inputWords.length; i++) { const inputWord = inputWords[i] as string; const targetWord = targetWords[i] as string; - if (inputWord === targetWord) { - //the word is correct - correctWordChars += targetWord.length; - correctChars += targetWord.length; - if ( - i < inputWords.length - 1 && - Strings.getLastChar(inputWord) !== "\n" - ) { - correctspaces++; - } - } else if (inputWord.length >= targetWord.length) { - //too many chars - for (let c = 0; c < inputWord.length; c++) { - if (c < targetWord.length) { - //on char that still has a word list pair - if (inputWord[c] === targetWord[c]) { - correctChars++; - } else { - incorrectChars++; - } - } else { - //on char that is extra - extraChars++; - } - } - } else { - //not enough chars - const toAdd = { - correct: 0, - incorrect: 0, - missed: 0, - }; - for (let c = 0; c < targetWord.length; c++) { - if (c < inputWord.length) { - //on char that still has a word list pair - if (inputWord[c] === targetWord[c]) { - toAdd.correct++; - } else { - toAdd.incorrect++; - } - } else { - //on char that is extra - toAdd.missed++; - } - } - correctChars += toAdd.correct; - incorrectChars += toAdd.incorrect; - - 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 { - missedChars += toAdd.missed; - } - } - if (i < inputWords.length - 1 && Strings.getLastChar(inputWord) !== "\n") { - spaces++; - } - } - if (isFunboxActiveWithProperty("nospace")) { - spaces = 0; - correctspaces = 0; + const { correctWord, allCorrect, incorrect, missed, extra } = + countCharsUtils( + inputWord, + targetWord, + !final || (i === inputWords.length - 1 && final), + isTimedTest, + ); + + correctWordChars += correctWord; + correctChars += allCorrect; + incorrectChars += incorrect; + extraChars += extra; + missedChars += missed; } + return { - spaces: spaces, correctWordChars: correctWordChars, allCorrectChars: correctChars, incorrectChars: Config.mode === "zen" ? TestInput.accuracy.incorrect : incorrectChars, extraChars: extraChars, missedChars: missedChars, - correctSpaces: correctspaces, }; } @@ -386,17 +344,11 @@ export function calculateFinalStats(): Stats { wpmRaw: isNaN(raw) ? 0 : raw, acc: acc, correctChars: chars.correctWordChars, - incorrectChars: chars.incorrectChars + chars.spaces - chars.correctSpaces, + incorrectChars: chars.incorrectChars, missedChars: chars.missedChars, extraChars: chars.extraChars, - allChars: - chars.allCorrectChars + - chars.spaces + - chars.incorrectChars + - chars.extraChars, + allChars: chars.allCorrectChars + chars.incorrectChars + chars.extraChars, time: Numbers.roundTo2(testSeconds), - spaces: chars.spaces, - correctSpaces: chars.correctSpaces, }; console.debug("Result stats", ret); return ret;