From 39e611d276e96da215c922c282f2ac51a108a88b Mon Sep 17 00:00:00 2001 From: Sanidhya Singh Date: Thu, 4 Apr 2024 01:42:48 +0530 Subject: [PATCH] refactor(utils): distribute color, numbers, strings and other group of functions into separate util files from misc.ts (sanidhyas3s) (#5254) * fix: Prevent theme switch when opening theme commandline from the footer fixes #5103 * refactor(utils/misc): move functions and adjust usages also renamed some functions for clarity for #5187 * refactor(utils/misc): move color utils into separate file & add docstring for #5187 * refactor(utils/misc): separte out number-related utils and more add docstring to the functions in `utils/numbers` also, move `getDiscordAvatarUrl` back into misc because it was causing mt to not open somehow for #5187 * refactor(utils/misc): move get-text functions into separate file & add docstring for them for #5187 * refactor(utils/misc): move get-data type functions into separate file & add docstrings for #5187 * Fix cyclic dependency by moving function * Move strings utils to separate file and other minor changes to utils * Shift Date & Time util functions to a separate file and add docstrings * Shift more string functions to string util file * separate out Arrays functions from misc utils * Rename some utility files and move some functions * lowercase filename * rename module imports * move ddr stuff into its own file * temp file rename * file rename --------- Co-authored-by: Miodec --- frontend/__tests__/test/misc.spec.ts | 2 +- frontend/src/html/pages/about.html | 2 +- frontend/src/ts/account/all-time-stats.ts | 4 +- frontend/src/ts/account/mini-result-chart.ts | 3 +- frontend/src/ts/account/result-filters.ts | 8 +- frontend/src/ts/commandline/lists.ts | 13 +- .../ts/commandline/lists/keymap-layouts.ts | 2 +- .../src/ts/commandline/lists/languages.ts | 2 +- frontend/src/ts/commandline/lists/layouts.ts | 2 +- .../ts/commandline/lists/load-challenge.ts | 2 +- .../src/ts/commandline/lists/result-screen.ts | 4 +- frontend/src/ts/commandline/lists/themes.ts | 2 +- frontend/src/ts/config-validation.ts | 5 +- .../src/ts/controllers/account-controller.ts | 3 +- frontend/src/ts/controllers/ad-controller.ts | 4 +- .../ts/controllers/challenge-controller.ts | 3 +- .../src/ts/controllers/chart-controller.ts | 32 +- .../src/ts/controllers/input-controller.ts | 6 +- .../src/ts/controllers/page-controller.ts | 3 +- .../src/ts/controllers/quotes-controller.ts | 9 +- .../src/ts/controllers/sound-controller.ts | 8 +- .../src/ts/controllers/theme-controller.ts | 13 +- frontend/src/ts/db.ts | 3 +- frontend/src/ts/elements/account-button.ts | 13 +- frontend/src/ts/elements/fps-counter.ts | 2 +- frontend/src/ts/elements/keymap.ts | 3 +- frontend/src/ts/elements/last-10-average.ts | 3 +- frontend/src/ts/elements/leaderboards.ts | 11 +- frontend/src/ts/elements/modes-notice.ts | 2 +- frontend/src/ts/elements/notifications.ts | 3 +- frontend/src/ts/elements/profile.ts | 38 +- frontend/src/ts/elements/psa.ts | 3 +- frontend/src/ts/elements/result-batches.ts | 3 +- .../src/ts/elements/result-word-highlight.ts | 3 +- frontend/src/ts/modals/edit-result-tags.ts | 2 +- frontend/src/ts/modals/pb-tables.ts | 2 +- frontend/src/ts/modals/quote-report.ts | 2 +- frontend/src/ts/modals/quote-submit.ts | 7 +- frontend/src/ts/modals/version-history.ts | 2 +- frontend/src/ts/modals/word-filter.ts | 9 +- frontend/src/ts/pages/about.ts | 17 +- frontend/src/ts/pages/account.ts | 17 +- frontend/src/ts/pages/settings.ts | 16 +- frontend/src/ts/ready.ts | 3 +- frontend/src/ts/settings/theme-picker.ts | 6 +- frontend/src/ts/test/british-english.ts | 6 +- frontend/src/ts/test/caret.ts | 7 +- frontend/src/ts/test/english-punctuation.ts | 6 +- .../src/ts/test/funbox/funbox-validation.ts | 7 +- frontend/src/ts/test/funbox/funbox.ts | 38 +- .../test/funbox/layoutfluid-funbox-timer.ts | 2 +- frontend/src/ts/test/ip-addresses.ts | 2 +- frontend/src/ts/test/layout-emulator.ts | 3 +- frontend/src/ts/test/pace-caret.ts | 10 +- frontend/src/ts/test/replay.ts | 5 +- frontend/src/ts/test/result.ts | 37 +- frontend/src/ts/test/shift-tracker.ts | 4 +- frontend/src/ts/test/test-input.ts | 7 +- frontend/src/ts/test/test-logic.ts | 55 +- frontend/src/ts/test/test-stats.ts | 21 +- frontend/src/ts/test/test-timer.ts | 4 +- frontend/src/ts/test/test-ui.ts | 38 +- frontend/src/ts/test/timer-progress.ts | 10 +- frontend/src/ts/test/today-tracker.ts | 4 +- frontend/src/ts/test/tts.ts | 4 +- frontend/src/ts/test/wikipedia.ts | 3 +- frontend/src/ts/test/words-generator.ts | 29 +- frontend/src/ts/test/wordset.ts | 3 +- frontend/src/ts/utils/arrays.ts | 101 ++ frontend/src/ts/utils/colors.ts | 189 +++ frontend/src/ts/utils/date-and-time.ts | 164 +++ frontend/src/ts/utils/ddr.ts | 61 + frontend/src/ts/utils/format.ts | 4 +- frontend/src/ts/utils/generate.ts | 193 +++ frontend/src/ts/utils/json-data.ts | 369 ++++++ frontend/src/ts/utils/levels.ts | 17 + frontend/src/ts/utils/misc.ts | 1157 +---------------- frontend/src/ts/utils/numbers.ts | 203 +++ frontend/src/ts/utils/strings.ts | 97 +- 79 files changed, 1765 insertions(+), 1397 deletions(-) create mode 100644 frontend/src/ts/utils/arrays.ts create mode 100644 frontend/src/ts/utils/colors.ts create mode 100644 frontend/src/ts/utils/date-and-time.ts create mode 100644 frontend/src/ts/utils/ddr.ts create mode 100644 frontend/src/ts/utils/generate.ts create mode 100644 frontend/src/ts/utils/json-data.ts create mode 100644 frontend/src/ts/utils/levels.ts create mode 100644 frontend/src/ts/utils/numbers.ts diff --git a/frontend/__tests__/test/misc.spec.ts b/frontend/__tests__/test/misc.spec.ts index 4ecd07f2334e..3f86c02a08f9 100644 --- a/frontend/__tests__/test/misc.spec.ts +++ b/frontend/__tests__/test/misc.spec.ts @@ -1,7 +1,7 @@ import { getLanguageDisplayString, removeLanguageSize, -} from "../../src/ts/utils/misc"; +} from "../../src/ts/utils/strings"; describe("misc.ts", () => { describe("getLanguageDisplayString", () => { diff --git a/frontend/src/html/pages/about.html b/frontend/src/html/pages/about.html index 56ea06372b1e..f55227710549 100644 --- a/frontend/src/html/pages/about.html +++ b/frontend/src/html/pages/about.html @@ -80,7 +80,7 @@

You can use tab and - enter + enter (or just tab if you have quick tab mode enabled) to restart the typing test. Open the diff --git a/frontend/src/ts/account/all-time-stats.ts b/frontend/src/ts/account/all-time-stats.ts index c01d7c9b2f37..b8adecceb333 100644 --- a/frontend/src/ts/account/all-time-stats.ts +++ b/frontend/src/ts/account/all-time-stats.ts @@ -1,5 +1,5 @@ import * as DB from "../db"; -import * as Misc from "../utils/misc"; +import * as DateTime from "../utils/date-and-time"; export function clear(): void { $(".pageAccount .globalTimeTyping .val").text(`-`); @@ -19,7 +19,7 @@ export function update(): void { if (seconds === 0) { string = "-"; } else { - string = Misc.secondsToString(Math.round(seconds), true, true); + string = DateTime.secondsToString(Math.round(seconds), true, true); } $(".pageAccount .globalTimeTyping .val").text(string); } diff --git a/frontend/src/ts/account/mini-result-chart.ts b/frontend/src/ts/account/mini-result-chart.ts index 740b0b18c890..572659975431 100644 --- a/frontend/src/ts/account/mini-result-chart.ts +++ b/frontend/src/ts/account/mini-result-chart.ts @@ -1,6 +1,7 @@ import * as ChartController from "../controllers/chart-controller"; import Config from "../config"; import * as Misc from "../utils/misc"; +import * as Arrays from "../utils/arrays"; export function updatePosition(x: number, y: number): void { $(".pageAccount .miniResultChartWrapper").css({ top: y, left: x }); @@ -28,7 +29,7 @@ export function updateData(data: SharedTypes.ChartData): void { data.err = data.err.slice(0, data.raw.length); labels = labels.slice(0, data.raw.length); - const smoothedRawData = Misc.smooth(data.raw, 1); + const smoothedRawData = Arrays.smooth(data.raw, 1); ChartController.miniResult.data.labels = labels; diff --git a/frontend/src/ts/account/result-filters.ts b/frontend/src/ts/account/result-filters.ts index b36fdeaaffb0..4d86bb12c3cb 100644 --- a/frontend/src/ts/account/result-filters.ts +++ b/frontend/src/ts/account/result-filters.ts @@ -1,4 +1,6 @@ import * as Misc from "../utils/misc"; +import * as Strings from "../utils/strings"; +import * as JSONData from "../utils/json-data"; import * as DB from "../db"; import Config from "../config"; import * as Notifications from "../elements/notifications"; @@ -637,7 +639,7 @@ $(".pageAccount .topFilters button.toggleAdvancedFilters").on("click", () => { export async function appendButtons(): Promise { let languageList; try { - languageList = await Misc.getLanguageList(); + languageList = await JSONData.getLanguageList(); } catch (e) { console.error( Misc.createErrorMessage(e, "Failed to append language buttons") @@ -646,7 +648,7 @@ export async function appendButtons(): Promise { if (languageList) { let html = ""; for (const language of languageList) { - html += ``; } @@ -658,7 +660,7 @@ export async function appendButtons(): Promise { let funboxList; try { - funboxList = await Misc.getFunboxList(); + funboxList = await JSONData.getFunboxList(); } catch (e) { console.error( Misc.createErrorMessage(e, "Failed to append funbox buttons") diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index f766105f61a4..b83673db8509 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -92,6 +92,7 @@ import KeymapLayoutsCommands, { import Config, * as UpdateConfig from "../config"; import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import { randomizeTheme } from "../controllers/theme-controller"; import * as CustomTextPopup from "../popups/custom-text-popup"; import * as Settings from "../pages/settings"; @@ -102,7 +103,7 @@ import * as TestStats from "../test/test-stats"; import * as QuoteSearchModal from "../modals/quote-search"; import * as FPSCounter from "../elements/fps-counter"; -const layoutsPromise = Misc.getLayoutsList(); +const layoutsPromise = JSONData.getLayoutsList(); layoutsPromise .then((layouts) => { updateLayoutsCommands(layouts); @@ -114,7 +115,7 @@ layoutsPromise ); }); -const languagesPromise = Misc.getLanguageList(); +const languagesPromise = JSONData.getLanguageList(); languagesPromise .then((languages) => { updateLanguagesCommands(languages); @@ -125,7 +126,7 @@ languagesPromise ); }); -const funboxPromise = Misc.getFunboxList(); +const funboxPromise = JSONData.getFunboxList(); funboxPromise .then((funboxes) => { updateFunboxCommands(funboxes); @@ -141,7 +142,7 @@ funboxPromise ); }); -const fontsPromise = Misc.getFontsList(); +const fontsPromise = JSONData.getFontsList(); fontsPromise .then((fonts) => { updateFontFamilyCommands(fonts); @@ -152,7 +153,7 @@ fontsPromise ); }); -const themesPromise = Misc.getThemesList(); +const themesPromise = JSONData.getThemesList(); themesPromise .then((themes) => { updateThemesCommands(themes); @@ -163,7 +164,7 @@ themesPromise ); }); -const challengesPromise = Misc.getChallengeList(); +const challengesPromise = JSONData.getChallengeList(); challengesPromise .then((challenges) => { updateLoadChallengeCommands(challenges); diff --git a/frontend/src/ts/commandline/lists/keymap-layouts.ts b/frontend/src/ts/commandline/lists/keymap-layouts.ts index 4733ca28dd9b..17118501e728 100644 --- a/frontend/src/ts/commandline/lists/keymap-layouts.ts +++ b/frontend/src/ts/commandline/lists/keymap-layouts.ts @@ -1,6 +1,6 @@ import * as UpdateConfig from "../../config"; import * as TestLogic from "../../test/test-logic"; -import { capitalizeFirstLetterOfEachWord } from "../../utils/misc"; +import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; const subgroup: MonkeyTypes.CommandsSubgroup = { title: "Change keymap layout...", diff --git a/frontend/src/ts/commandline/lists/languages.ts b/frontend/src/ts/commandline/lists/languages.ts index 52c32db489f6..01b95dab2d6f 100644 --- a/frontend/src/ts/commandline/lists/languages.ts +++ b/frontend/src/ts/commandline/lists/languages.ts @@ -2,7 +2,7 @@ import * as UpdateConfig from "../../config"; import { capitalizeFirstLetterOfEachWord, getLanguageDisplayString, -} from "../../utils/misc"; +} from "../../utils/strings"; const subgroup: MonkeyTypes.CommandsSubgroup = { title: "Language...", diff --git a/frontend/src/ts/commandline/lists/layouts.ts b/frontend/src/ts/commandline/lists/layouts.ts index ec53a7ca05fc..df1c83dc0e55 100644 --- a/frontend/src/ts/commandline/lists/layouts.ts +++ b/frontend/src/ts/commandline/lists/layouts.ts @@ -1,6 +1,6 @@ import * as UpdateConfig from "../../config"; import * as TestLogic from "../../test/test-logic"; -import { capitalizeFirstLetterOfEachWord } from "../../utils/misc"; +import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; const subgroup: MonkeyTypes.CommandsSubgroup = { title: "Layout emulator...", diff --git a/frontend/src/ts/commandline/lists/load-challenge.ts b/frontend/src/ts/commandline/lists/load-challenge.ts index 5281e236585d..e260ff2db509 100644 --- a/frontend/src/ts/commandline/lists/load-challenge.ts +++ b/frontend/src/ts/commandline/lists/load-challenge.ts @@ -1,7 +1,7 @@ import { navigate } from "../../controllers/route-controller"; import * as ChallengeController from "../../controllers/challenge-controller"; import * as TestLogic from "../../test/test-logic"; -import { capitalizeFirstLetterOfEachWord } from "../../utils/misc"; +import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; const subgroup: MonkeyTypes.CommandsSubgroup = { title: "Load challenge...", diff --git a/frontend/src/ts/commandline/lists/result-screen.ts b/frontend/src/ts/commandline/lists/result-screen.ts index 82207703e2dc..1a9b1cb9a93c 100644 --- a/frontend/src/ts/commandline/lists/result-screen.ts +++ b/frontend/src/ts/commandline/lists/result-screen.ts @@ -1,7 +1,7 @@ import * as TestLogic from "../../test/test-logic"; import * as TestUI from "../../test/test-ui"; import * as PractiseWords from "../../test/practise-words"; -import * as Misc from "../../utils/misc"; +import * as GetText from "../../utils/generate"; import * as Notifications from "../../elements/notifications"; const copyWords: MonkeyTypes.CommandsSubgroup = { @@ -15,7 +15,7 @@ const copyWords: MonkeyTypes.CommandsSubgroup = { id: "copyYes", display: "Yes, I am sure", exec: (): void => { - const words = Misc.getWords(); + const words = GetText.getWords(); navigator.clipboard.writeText(words).then( () => { diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index 998eed78a79f..a1c112b59c5c 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -1,5 +1,5 @@ import Config, * as UpdateConfig from "../../config"; -import { capitalizeFirstLetterOfEachWord } from "../../utils/misc"; +import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; import * as ThemeController from "../../controllers/theme-controller"; const subgroup: MonkeyTypes.CommandsSubgroup = { diff --git a/frontend/src/ts/config-validation.ts b/frontend/src/ts/config-validation.ts index 20ef6794fc45..9d9777422d6e 100644 --- a/frontend/src/ts/config-validation.ts +++ b/frontend/src/ts/config-validation.ts @@ -1,4 +1,5 @@ import * as Misc from "./utils/misc"; +import * as JSONData from "./utils/json-data"; import * as Notifications from "./elements/notifications"; type PossibleType = @@ -121,7 +122,7 @@ export async function isConfigValueValidAsync( if (layoutNames.length < 2 || layoutNames.length > 5) break; try { - await Misc.getLayoutsList(); + await JSONData.getLayoutsList(); } catch (e) { customMessage = Misc.createErrorMessage( e, @@ -132,7 +133,7 @@ export async function isConfigValueValidAsync( // convert the layout names to layouts const layouts = await Promise.all( - layoutNames.map(async (layoutName) => Misc.getLayout(layoutName)) + layoutNames.map(async (layoutName) => JSONData.getLayout(layoutName)) ); // check if all layouts exist diff --git a/frontend/src/ts/controllers/account-controller.ts b/frontend/src/ts/controllers/account-controller.ts index b81e3d751ee5..50ff47ac9384 100644 --- a/frontend/src/ts/controllers/account-controller.ts +++ b/frontend/src/ts/controllers/account-controller.ts @@ -3,6 +3,7 @@ import * as Notifications from "../elements/notifications"; import Config, * as UpdateConfig from "../config"; import * as AccountButton from "../elements/account-button"; import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import * as Settings from "../pages/settings"; import * as AllTimeStats from "../account/all-time-stats"; import * as DB from "../db"; @@ -125,7 +126,7 @@ async function getDataAndInit(): Promise { ResultFilters.loadTags(snapshot.tags); - Promise.all([Misc.getLanguageList(), Misc.getFunboxList()]) + Promise.all([JSONData.getLanguageList(), JSONData.getFunboxList()]) .then((values) => { const [languages, funboxes] = values; languages.forEach((language) => { diff --git a/frontend/src/ts/controllers/ad-controller.ts b/frontend/src/ts/controllers/ad-controller.ts index 4aa8271bb026..1125f9c6d9a8 100644 --- a/frontend/src/ts/controllers/ad-controller.ts +++ b/frontend/src/ts/controllers/ad-controller.ts @@ -1,5 +1,5 @@ import { debounce } from "throttle-debounce"; -import * as Misc from "../utils/misc"; +import * as Numbers from "../utils/numbers"; import * as ConfigEvent from "../observables/config-event"; import * as BannerEvent from "../observables/banner-event"; import Config from "../config"; @@ -90,7 +90,7 @@ function removeResult(): void { function updateVerticalMargin(): void { const height = $("#bannerCenter").height() as number; - const margin = height + Misc.convertRemToPixels(2) + "px"; + const margin = height + Numbers.convertRemToPixels(2) + "px"; $("#ad-vertical-left-wrapper").css("margin-top", margin); $("#ad-vertical-right-wrapper").css("margin-top", margin); } diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 9bd4a833eb18..360f0c033f67 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -1,4 +1,5 @@ import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import * as Notifications from "../elements/notifications"; import * as ManualRestart from "../test/manual-restart-tracker"; import * as CustomText from "../test/custom-text"; @@ -209,7 +210,7 @@ export async function setup(challengeName: string): Promise { let list; try { - list = await Misc.getChallengeList(); + list = await JSONData.getChallengeList(); } catch (e) { const message = Misc.createErrorMessage(e, "Failed to setup challenge"); Notifications.add(message, -1); diff --git a/frontend/src/ts/controllers/chart-controller.ts b/frontend/src/ts/controllers/chart-controller.ts index b6e695a2be6c..ada21cfd8917 100644 --- a/frontend/src/ts/controllers/chart-controller.ts +++ b/frontend/src/ts/controllers/chart-controller.ts @@ -63,7 +63,10 @@ import Config from "../config"; import * as ThemeColors from "../elements/theme-colors"; import * as ConfigEvent from "../observables/config-event"; import * as TestInput from "../test/test-input"; -import * as Misc from "../utils/misc"; +import * as DateTime from "../utils/date-and-time"; +import * as Arrays from "../utils/arrays"; +import * as Numbers from "../utils/numbers"; +import { blendTwoHexColors } from "../utils/colors"; class ChartWithUpdateColors< TType extends ChartType = ChartType, @@ -242,7 +245,8 @@ export const result = new ChartWithUpdateColors< const unique = [...new Set(wordsToHighlight)]; const firstHighlightWordIndex = unique[0]; - const lastHighlightWordIndex = unique[unique.length - 1]; + const lastHighlightWordIndex = + Arrays.lastElementFromArray(unique); if ( firstHighlightWordIndex === undefined || lastHighlightWordIndex === undefined @@ -496,9 +500,9 @@ export const accountHistory = new ChartWithUpdateColors< const resultData = tooltipItem.dataset.data[ tooltipItem.dataIndex ] as MonkeyTypes.AccChartData; - return `error rate: ${Misc.roundTo2( + return `error rate: ${Numbers.roundTo2( resultData.errorRate - )}%\nacc: ${Misc.roundTo2(100 - resultData.errorRate)}%`; + )}%\nacc: ${Numbers.roundTo2(100 - resultData.errorRate)}%`; } const resultData = tooltipItem.dataset.data[ tooltipItem.dataIndex @@ -674,13 +678,13 @@ export const accountActivity = new ChartWithUpdateColors< ] as MonkeyTypes.ActivityChartDataPoint; switch (tooltipItem.datasetIndex) { case 0: - return `Time Typing: ${Misc.secondsToString( + return `Time Typing: ${DateTime.secondsToString( Math.round(resultData.y * 60), true, true )}\nTests Completed: ${resultData.amount}`; case 1: - return `Average ${Config.typingSpeedUnit}: ${Misc.roundTo2( + return `Average ${Config.typingSpeedUnit}: ${Numbers.roundTo2( resultData.y )}`; default: @@ -777,7 +781,7 @@ export const accountHistogram = new ChartWithUpdateColors< // ] as MonkeyTypes.ActivityChartDataPoint; // switch (tooltipItem.datasetIndex) { // case 0: - // return `Time Typing: ${Misc.secondsToString( + // return `Time Typing: ${DateTime.secondsToString( // Math.round(resultData.y), // true, // true @@ -785,7 +789,7 @@ export const accountHistogram = new ChartWithUpdateColors< // case 1: // return `Average ${ // Config.typingSpeedUnit - // }: ${Misc.roundTo2(resultData.y)}`; + // }: ${Numbers.roundTo2(resultData.y)}`; // default: // return ""; // } @@ -1113,7 +1117,7 @@ async function updateColors< const errorcolor = await ThemeColors.get("error"); const textcolor = await ThemeColors.get("text"); - const gridcolor = Misc.blendTwoHexColors(bgcolor, subaltcolor, 0.75); + const gridcolor = blendTwoHexColors(bgcolor, subaltcolor, 0.75); //@ts-expect-error chart.data.datasets[0].borderColor = (ctx): string => { @@ -1182,12 +1186,12 @@ async function updateColors< const avg10On = Config.accountChart[2] === "on"; const avg100On = Config.accountChart[3] === "on"; - const text02 = Misc.blendTwoHexColors(bgcolor, textcolor, 0.2); - const main02 = Misc.blendTwoHexColors(bgcolor, maincolor, 0.2); - const main04 = Misc.blendTwoHexColors(bgcolor, maincolor, 0.4); + const text02 = blendTwoHexColors(bgcolor, textcolor, 0.2); + const main02 = blendTwoHexColors(bgcolor, maincolor, 0.2); + const main04 = blendTwoHexColors(bgcolor, maincolor, 0.4); - const sub02 = Misc.blendTwoHexColors(bgcolor, subcolor, 0.2); - const sub04 = Misc.blendTwoHexColors(bgcolor, subcolor, 0.4); + const sub02 = blendTwoHexColors(bgcolor, subcolor, 0.2); + const sub04 = blendTwoHexColors(bgcolor, subcolor, 0.4); const [ wpmDataset, diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts index 3d4f44d161db..a7661bcf8496 100644 --- a/frontend/src/ts/controllers/input-controller.ts +++ b/frontend/src/ts/controllers/input-controller.ts @@ -4,6 +4,8 @@ import * as TestStats from "../test/test-stats"; import * as Monkey from "../test/monkey"; import Config from "../config"; import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; +import * as Numbers from "../utils/numbers"; import * as LiveAcc from "../test/live-acc"; import * as LiveBurst from "../test/live-burst"; import * as Funbox from "../test/funbox/funbox"; @@ -53,7 +55,7 @@ function setWordsInput(value: string): void { } function updateUI(): void { - const acc: number = Misc.roundTo2(TestStats.calculateAccuracy()); + const acc: number = Numbers.roundTo2(TestStats.calculateAccuracy()); if (!isNaN(acc)) LiveAcc.update(acc); if (Config.keymapMode === "next" && Config.mode !== "zen") { @@ -1115,7 +1117,7 @@ $(document).on("keydown", async (event) => { Config.oppositeShiftMode === "keymap" && Config.keymapLayout !== "overrideSync" ) { - const keymapLayout = await Misc.getLayout(Config.keymapLayout).catch( + const keymapLayout = await JSONData.getLayout(Config.keymapLayout).catch( () => undefined ); if (keymapLayout === undefined) { diff --git a/frontend/src/ts/controllers/page-controller.ts b/frontend/src/ts/controllers/page-controller.ts index 547b131a49fb..db04231137c8 100644 --- a/frontend/src/ts/controllers/page-controller.ts +++ b/frontend/src/ts/controllers/page-controller.ts @@ -1,4 +1,5 @@ import * as Misc from "../utils/misc"; +import * as Strings from "../utils/strings"; import * as ActivePage from "../states/active-page"; import * as Settings from "../pages/settings"; import * as Account from "../pages/account"; @@ -78,7 +79,7 @@ export async function change( Misc.updateTitle(); } else { Misc.updateTitle( - Misc.capitalizeFirstLetterOfEachWord(nextPage.name) + + Strings.capitalizeFirstLetterOfEachWord(nextPage.name) + " | Monkeytype" ); } diff --git a/frontend/src/ts/controllers/quotes-controller.ts b/frontend/src/ts/controllers/quotes-controller.ts index 3f3778727761..b6e175e9bea7 100644 --- a/frontend/src/ts/controllers/quotes-controller.ts +++ b/frontend/src/ts/controllers/quotes-controller.ts @@ -1,9 +1,6 @@ -import { - cachedFetchJson, - randomElementFromArray, - removeLanguageSize, - shuffle, -} from "../utils/misc"; +import { removeLanguageSize } from "../utils/strings"; +import { randomElementFromArray, shuffle } from "../utils/arrays"; +import { cachedFetchJson } from "../utils/json-data"; import { subscribe } from "../observables/config-event"; import * as DB from "../db"; diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 4443a7aa2e9e..7bcf6f9a8f82 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -1,10 +1,8 @@ import Config from "../config"; import * as ConfigEvent from "../observables/config-event"; -import { - createErrorMessage, - randomElementFromArray, - randomIntFromRange, -} from "../utils/misc"; +import { createErrorMessage } from "../utils/misc"; +import { randomElementFromArray } from "../utils/arrays"; +import { randomIntFromRange } from "../utils/numbers"; import { leftState, rightState } from "../test/shift-tracker"; import { capsState } from "../test/caps-warning"; import * as Notifications from "../elements/notifications"; diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index 680301b7e237..d95b155272a9 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -1,6 +1,9 @@ import * as ThemeColors from "../elements/theme-colors"; import * as ChartController from "./chart-controller"; import * as Misc from "../utils/misc"; +import * as Arrays from "../utils/arrays"; +import * as JSONData from "../utils/json-data"; +import { isColorDark, isColorLight } from "../utils/colors"; import Config, { setAutoSwitchTheme } from "../config"; import * as BackgroundFilter from "../elements/custom-background-filter"; import * as ConfigEvent from "../observables/config-event"; @@ -239,7 +242,7 @@ let themesList: string[] = []; async function changeThemeList(): Promise { let themes; try { - themes = await Misc.getThemesList(); + themes = await JSONData.getThemesList(); } catch (e) { console.error( Misc.createErrorMessage(e, "Failed to update random theme list") @@ -251,11 +254,11 @@ async function changeThemeList(): Promise { themesList = Config.favThemes; } else if (Config.randomTheme === "light") { themesList = themes - .filter((t) => Misc.isColorLight(t.bgColor)) + .filter((t) => isColorLight(t.bgColor)) .map((t) => t.name); } else if (Config.randomTheme === "dark") { themesList = themes - .filter((t) => Misc.isColorDark(t.bgColor)) + .filter((t) => isColorDark(t.bgColor)) .map((t) => t.name); } else if (Config.randomTheme === "on") { themesList = themes.map((t) => { @@ -264,7 +267,7 @@ async function changeThemeList(): Promise { } else if (Config.randomTheme === "custom" && DB.getSnapshot()) { themesList = DB.getSnapshot()?.customThemes?.map((ct) => ct._id) ?? []; } - Misc.shuffle(themesList); + Arrays.shuffle(themesList); randomThemeIndex = 0; } @@ -277,7 +280,7 @@ export async function randomizeTheme(): Promise { randomThemeIndex++; if (randomThemeIndex >= themesList.length) { - Misc.shuffle(themesList); + Arrays.shuffle(themesList); randomThemeIndex = 0; } diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 6139b6316991..6f818e4f7e5e 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -5,7 +5,8 @@ import DefaultConfig from "./constants/default-config"; import { isAuthenticated } from "./firebase"; import { defaultSnap } from "./constants/default-snapshot"; import * as ConnectionState from "./states/connection"; -import { getFunboxList, lastElementFromArray } from "./utils/misc"; +import { lastElementFromArray } from "./utils/arrays"; +import { getFunboxList } from "./utils/json-data"; import { mergeWithDefaultConfig } from "./utils/config"; let dbSnapshot: MonkeyTypes.Snapshot | undefined; diff --git a/frontend/src/ts/elements/account-button.ts b/frontend/src/ts/elements/account-button.ts index 2364e1222adf..3783cff9245e 100644 --- a/frontend/src/ts/elements/account-button.ts +++ b/frontend/src/ts/elements/account-button.ts @@ -1,6 +1,7 @@ import { getSnapshot } from "../db"; import { isAuthenticated } from "../firebase"; import * as Misc from "../utils/misc"; +import * as Levels from "../utils/levels"; import { getAll } from "./theme-colors"; import * as SlowTimer from "../states/slow-timer"; @@ -102,9 +103,9 @@ export async function update( ): Promise { if (isAuthenticated()) { if (xp !== undefined) { - $("header nav .level").text(Math.floor(Misc.getLevel(xp))); + $("header nav .level").text(Math.floor(Levels.getLevel(xp))); $("header nav .bar").css({ - width: (Misc.getLevel(xp) % 1) * 100 + "%", + width: (Levels.getLevel(xp) % 1) * 100 + "%", }); } if ((discordAvatar ?? "") && (discordId ?? "")) { @@ -156,14 +157,14 @@ export async function updateXpBar( breakdown?: Record ): Promise { skipBreakdown = false; - const startingLevel = Misc.getLevel(currentXp); - const endingLevel = Misc.getLevel(currentXp + addedXp); + const startingLevel = Levels.getLevel(currentXp); + const endingLevel = Levels.getLevel(currentXp + addedXp); const snapshot = getSnapshot(); if (!snapshot) return; if (skipBreakdown) { - $("nav .level").text(Math.floor(Misc.getLevel(snapshot.xp))); + $("nav .level").text(Math.floor(Levels.getLevel(snapshot.xp))); $("nav .xpBar") .stop(true, true) .css("opacity", 1) @@ -178,7 +179,7 @@ export async function updateXpBar( await Promise.all([xpBarPromise, xpBreakdownPromise]); await Misc.sleep(2000); - $("nav .level").text(Math.floor(Misc.getLevel(snapshot.xp))); + $("nav .level").text(Math.floor(Levels.getLevel(snapshot.xp))); $("nav .xpBar") .stop(true, true) .css("opacity", 1) diff --git a/frontend/src/ts/elements/fps-counter.ts b/frontend/src/ts/elements/fps-counter.ts index 68109ba58976..994fce9c51eb 100644 --- a/frontend/src/ts/elements/fps-counter.ts +++ b/frontend/src/ts/elements/fps-counter.ts @@ -1,4 +1,4 @@ -import { roundTo2 } from "../utils/misc"; +import { roundTo2 } from "../utils/numbers"; let frameCount = 0; let fpsInterval: number; diff --git a/frontend/src/ts/elements/keymap.ts b/frontend/src/ts/elements/keymap.ts index 88c4a2cea5d8..d288b3dc7cc3 100644 --- a/frontend/src/ts/elements/keymap.ts +++ b/frontend/src/ts/elements/keymap.ts @@ -4,6 +4,7 @@ import * as SlowTimer from "../states/slow-timer"; import * as ConfigEvent from "../observables/config-event"; import * as KeymapEvent from "../observables/keymap-event"; import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import * as Hangul from "hangul-js"; import * as Notifications from "../elements/notifications"; import * as ActivePage from "../states/active-page"; @@ -115,7 +116,7 @@ export async function refresh( try { let layouts; try { - layouts = await Misc.getLayoutsList(); + layouts = await JSONData.getLayoutsList(); } catch (e) { Notifications.add( Misc.createErrorMessage(e, "Failed to refresh keymap"), diff --git a/frontend/src/ts/elements/last-10-average.ts b/frontend/src/ts/elements/last-10-average.ts index 658b61f53fdb..aca6fa1d00d5 100644 --- a/frontend/src/ts/elements/last-10-average.ts +++ b/frontend/src/ts/elements/last-10-average.ts @@ -1,5 +1,6 @@ import * as DB from "../db"; import * as Misc from "../utils/misc"; +import * as Numbers from "../utils/numbers"; import Config from "../config"; import * as TestWords from "../test/test-words"; @@ -19,7 +20,7 @@ export async function update(): Promise { Config.difficulty, Config.lazyMode ) - ).map(Misc.roundTo2) as [number, number]; + ).map(Numbers.roundTo2) as [number, number]; averageWPM = Config.alwaysShowDecimalPlaces ? wpm : Math.round(wpm); averageAcc = Config.alwaysShowDecimalPlaces ? acc : Math.floor(acc); diff --git a/frontend/src/ts/elements/leaderboards.ts b/frontend/src/ts/elements/leaderboards.ts index 4d04070b6d58..457c43e063f0 100644 --- a/frontend/src/ts/elements/leaderboards.ts +++ b/frontend/src/ts/elements/leaderboards.ts @@ -1,7 +1,10 @@ import Ape from "../ape"; import * as DB from "../db"; import Config from "../config"; +import * as DateTime from "../utils/date-and-time"; import * as Misc from "../utils/misc"; +import * as Arrays from "../utils/arrays"; +import * as Numbers from "../utils/numbers"; import * as Notifications from "./notifications"; import format from "date-fns/format"; import { isAuthenticated } from "../firebase"; @@ -101,7 +104,7 @@ function updateTimerElement(): void { const diff = differenceInSeconds(date, dateNow); $("#leaderboards .subTitle").text( - "Next reset in: " + Misc.secondsToString(diff, true) + "Next reset in: " + DateTime.secondsToString(diff, true) ); } else { const date = new Date(); @@ -109,7 +112,7 @@ function updateTimerElement(): void { const secondsToNextUpdate = 60 - date.getSeconds(); const totalSeconds = minutesToNextUpdate * 60 + secondsToNextUpdate; $("#leaderboards .subTitle").text( - "Next update in: " + Misc.secondsToString(totalSeconds, true) + "Next update in: " + DateTime.secondsToString(totalSeconds, true) ); } } @@ -193,7 +196,7 @@ function updateFooter(lb: LbKey): void { let toppercent = ""; if (currentTimeRange === "allTime" && lbRank !== undefined && lbRank?.rank) { - const num = Misc.roundTo2( + const num = Numbers.roundTo2( (lbRank.rank / (currentRank[lb].count as number)) * 100 ); if (currentRank[lb].rank === 1) { @@ -525,7 +528,7 @@ async function requestMore(lb: LbKey, prepend = false): Promise { if (requesting[lb]) return; requesting[lb] = true; showLoader(lb); - let skipVal = currentData[lb][currentData[lb].length - 1]?.rank as number; + let skipVal = Arrays.lastElementFromArray(currentData[lb])?.rank as number; if (prepend) { skipVal = (currentData[lb][0]?.rank ?? 0) - leaderboardSingleLimit; } diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index 46d811be91a1..1e5a56f4bf1c 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -7,7 +7,7 @@ import * as TestWords from "../test/test-words"; import * as ConfigEvent from "../observables/config-event"; import { isAuthenticated } from "../firebase"; import * as CustomTextState from "../states/custom-text-name"; -import { getLanguageDisplayString } from "../utils/misc"; +import { getLanguageDisplayString } from "../utils/strings"; import Format from "../utils/format"; ConfigEvent.subscribe((eventKey) => { diff --git a/frontend/src/ts/elements/notifications.ts b/frontend/src/ts/elements/notifications.ts index 4845e4b3fedc..a47a3b213852 100644 --- a/frontend/src/ts/elements/notifications.ts +++ b/frontend/src/ts/elements/notifications.ts @@ -1,5 +1,6 @@ import { debounce } from "throttle-debounce"; import * as Misc from "../utils/misc"; +import * as Numbers from "../utils/numbers"; import * as BannerEvent from "../observables/banner-event"; // import * as Alerts from "./alerts"; import * as NotificationEvent from "../observables/notification-event"; @@ -8,7 +9,7 @@ function updateMargin(): void { const height = $("#bannerCenter").height() as number; $("#contentWrapper").css( "padding-top", - height + Misc.convertRemToPixels(2) + "px" + height + Numbers.convertRemToPixels(2) + "px" ); $("#notificationCenter").css("margin-top", height + "px"); } diff --git a/frontend/src/ts/elements/profile.ts b/frontend/src/ts/elements/profile.ts index 6c690a4a1228..67e38a2fbab0 100644 --- a/frontend/src/ts/elements/profile.ts +++ b/frontend/src/ts/elements/profile.ts @@ -2,6 +2,9 @@ import * as DB from "../db"; import format from "date-fns/format"; import differenceInDays from "date-fns/differenceInDays"; import * as Misc from "../utils/misc"; +import * as Numbers from "../utils/numbers"; +import * as Levels from "../utils/levels"; +import * as DateTime from "../utils/date-and-time"; import { getHTMLById } from "../controllers/badge-controller"; import { throttle } from "throttle-debounce"; import * as ActivePage from "../states/active-page"; @@ -127,7 +130,7 @@ export async function update( const dayInMilis = 1000 * 60 * 60 * 24; - let target = Misc.getCurrentDayTimestamp(streakOffset) + dayInMilis; + let target = DateTime.getCurrentDayTimestamp(streakOffset) + dayInMilis; if (target < Date.now()) { target += dayInMilis; } @@ -138,20 +141,23 @@ export async function update( console.debug("dayInMilis", dayInMilis); console.debug( "difTarget", - new Date(Misc.getCurrentDayTimestamp(streakOffset) + dayInMilis) + new Date(DateTime.getCurrentDayTimestamp(streakOffset) + dayInMilis) ); console.debug("timeDif", timeDif); console.debug( - "Misc.getCurrentDayTimestamp()", - Misc.getCurrentDayTimestamp(), - new Date(Misc.getCurrentDayTimestamp()) + "DateTime.getCurrentDayTimestamp()", + DateTime.getCurrentDayTimestamp(), + new Date(DateTime.getCurrentDayTimestamp()) ); console.debug("profile.streakHourOffset", streakOffset); if (lastResult) { //check if the last result is from today - const isToday = Misc.isToday(lastResult.timestamp, streakOffset); - const isYesterday = Misc.isYesterday(lastResult.timestamp, streakOffset); + const isToday = DateTime.isToday(lastResult.timestamp, streakOffset); + const isYesterday = DateTime.isYesterday( + lastResult.timestamp, + streakOffset + ); console.debug( "lastResult.timestamp", @@ -217,7 +223,7 @@ export async function update( typingStatsEl .find(".timeTyping .value") .text( - Misc.secondsToString( + DateTime.secondsToString( Math.round(profile.typingStats?.timeTyping ?? 0), true, true @@ -285,18 +291,18 @@ export async function update( } const xp = profile.xp ?? 0; - const levelFraction = Misc.getLevel(xp); + const levelFraction = Levels.getLevel(xp); const level = Math.floor(levelFraction); - const xpForLevel = Misc.getXpForLevel(level); + const xpForLevel = Levels.getXpForLevel(level); const xpToDisplay = Math.round(xpForLevel * (levelFraction % 1)); details .find(".level") .text(level) - .attr("aria-label", `${Misc.abbreviateNumber(xp)} total xp`); + .attr("aria-label", `${Numbers.abbreviateNumber(xp)} total xp`); details .find(".xp") .text( - `${Misc.abbreviateNumber(xpToDisplay)}/${Misc.abbreviateNumber( + `${Numbers.abbreviateNumber(xpToDisplay)}/${Numbers.abbreviateNumber( xpForLevel )}` ); @@ -307,7 +313,9 @@ export async function update( .find(".xp") .attr( "aria-label", - `${Misc.abbreviateNumber(xpForLevel - xpToDisplay)} xp until next level` + `${Numbers.abbreviateNumber( + xpForLevel - xpToDisplay + )} xp until next level` ); //lbs @@ -406,7 +414,7 @@ export function updateNameFontSize(where: ProfileViewPaths): void { const nameFieldjQ = details.find(".user"); const nameFieldParent = nameFieldjQ.parent()[0]; const nameField = nameFieldjQ[0]; - const upperLimit = Misc.convertRemToPixels(2); + const upperLimit = Numbers.convertRemToPixels(2); if (!nameField || !nameFieldParent) return; @@ -433,5 +441,5 @@ $(window).on("resize", () => { function formatTopPercentage(lbRank: SharedTypes.RankAndCount): string { if (lbRank.rank === undefined) return "-"; if (lbRank.rank === 1) return "GOAT"; - return "Top " + Misc.roundTo2((lbRank.rank / lbRank.count) * 100) + "%"; + return "Top " + Numbers.roundTo2((lbRank.rank / lbRank.count) * 100) + "%"; } diff --git a/frontend/src/ts/elements/psa.ts b/frontend/src/ts/elements/psa.ts index dbec1a5c777e..76ebc7ed6562 100644 --- a/frontend/src/ts/elements/psa.ts +++ b/frontend/src/ts/elements/psa.ts @@ -1,5 +1,6 @@ import Ape from "../ape"; -import { isDevEnvironment, secondsToString } from "../utils/misc"; +import { isDevEnvironment } from "../utils/misc"; +import { secondsToString } from "../utils/date-and-time"; import * as Notifications from "./notifications"; import format from "date-fns/format"; import * as Alerts from "./alerts"; diff --git a/frontend/src/ts/elements/result-batches.ts b/frontend/src/ts/elements/result-batches.ts index 861ce905b364..9a2c20c66e8e 100644 --- a/frontend/src/ts/elements/result-batches.ts +++ b/frontend/src/ts/elements/result-batches.ts @@ -1,6 +1,7 @@ import * as DB from "../db"; import * as ServerConfiguration from "../ape/server-configuration"; -import { blendTwoHexColors, mapRange } from "../utils/misc"; +import { mapRange } from "../utils/misc"; +import { blendTwoHexColors } from "../utils/colors"; import * as ThemeColors from "../elements/theme-colors"; export function hide(): void { diff --git a/frontend/src/ts/elements/result-word-highlight.ts b/frontend/src/ts/elements/result-word-highlight.ts index 1865af410d48..95aedd773d21 100644 --- a/frontend/src/ts/elements/result-word-highlight.ts +++ b/frontend/src/ts/elements/result-word-highlight.ts @@ -4,6 +4,7 @@ // Constants for padding around the highlights import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import Config from "../config"; const PADDING_X = 16; @@ -198,7 +199,7 @@ async function init(): Promise { } // Set isLanguageRTL - const currentLanguage = await Misc.getCurrentLanguage(Config.language); + const currentLanguage = await JSONData.getCurrentLanguage(Config.language); isLanguageRightToLeft = currentLanguage.rightToLeft; RWH_el = $("#resultWordsHistory")[0] as HTMLElement; diff --git a/frontend/src/ts/modals/edit-result-tags.ts b/frontend/src/ts/modals/edit-result-tags.ts index bad868318e46..6c373e876e5b 100644 --- a/frontend/src/ts/modals/edit-result-tags.ts +++ b/frontend/src/ts/modals/edit-result-tags.ts @@ -4,7 +4,7 @@ import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; import * as AccountPage from "../pages/account"; import * as ConnectionState from "../states/connection"; -import { areUnsortedArraysEqual } from "../utils/misc"; +import { areUnsortedArraysEqual } from "../utils/arrays"; import * as Result from "../test/result"; import AnimatedModal from "../utils/animated-modal"; diff --git a/frontend/src/ts/modals/pb-tables.ts b/frontend/src/ts/modals/pb-tables.ts index 19e07cd1d6ce..21cf94f08aa9 100644 --- a/frontend/src/ts/modals/pb-tables.ts +++ b/frontend/src/ts/modals/pb-tables.ts @@ -1,6 +1,6 @@ import * as DB from "../db"; import format from "date-fns/format"; -import { getLanguageDisplayString } from "../utils/misc"; +import { getLanguageDisplayString } from "../utils/strings"; import Config from "../config"; import Format from "../utils/format"; import AnimatedModal from "../utils/animated-modal"; diff --git a/frontend/src/ts/modals/quote-report.ts b/frontend/src/ts/modals/quote-report.ts index 277ef857015d..d8a5f24d5fd4 100644 --- a/frontend/src/ts/modals/quote-report.ts +++ b/frontend/src/ts/modals/quote-report.ts @@ -4,7 +4,7 @@ import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; import QuotesController from "../controllers/quotes-controller"; import * as CaptchaController from "../controllers/captcha-controller"; -import { removeLanguageSize } from "../utils/misc"; +import { removeLanguageSize } from "../utils/strings"; import SlimSelect from "slim-select"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; diff --git a/frontend/src/ts/modals/quote-submit.ts b/frontend/src/ts/modals/quote-submit.ts index dcdeb2ea5926..866c63e4b364 100644 --- a/frontend/src/ts/modals/quote-submit.ts +++ b/frontend/src/ts/modals/quote-submit.ts @@ -2,7 +2,8 @@ import Ape from "../ape"; import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; import * as CaptchaController from "../controllers/captcha-controller"; -import * as Misc from "../utils/misc"; +import * as Strings from "../utils/strings"; +import * as JSONData from "../utils/json-data"; import Config from "../config"; import SlimSelect from "slim-select"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; @@ -10,7 +11,7 @@ import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; let dropdownReady = false; async function initDropdown(): Promise { if (dropdownReady) return; - const languageGroups = await Misc.getLanguageGroups(); + const languageGroups = await JSONData.getLanguageGroups(); for (const group of languageGroups) { if (group.name === "swiss_german") continue; $("#quoteSubmitModal .newQuoteLanguage").append( @@ -64,7 +65,7 @@ export async function show(showOptions: ShowOptions): Promise { }); $("#quoteSubmitModal .newQuoteLanguage").val( - Misc.removeLanguageSize(Config.language) + Strings.removeLanguageSize(Config.language) ); $("#quoteSubmitModal .newQuoteLanguage").trigger("change"); $("#quoteSubmitModal input").val(""); diff --git a/frontend/src/ts/modals/version-history.ts b/frontend/src/ts/modals/version-history.ts index 46b82756db31..f0d95cda3eec 100644 --- a/frontend/src/ts/modals/version-history.ts +++ b/frontend/src/ts/modals/version-history.ts @@ -1,5 +1,5 @@ import format from "date-fns/format"; -import { getReleasesFromGitHub } from "../utils/misc"; +import { getReleasesFromGitHub } from "../utils/json-data"; import AnimatedModal from "../utils/animated-modal"; export function show(): void { diff --git a/frontend/src/ts/modals/word-filter.ts b/frontend/src/ts/modals/word-filter.ts index 738d662ee59a..31cd73d01594 100644 --- a/frontend/src/ts/modals/word-filter.ts +++ b/frontend/src/ts/modals/word-filter.ts @@ -1,4 +1,5 @@ import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import * as CustomText from "../test/custom-text"; import * as Notifications from "../elements/notifications"; import SlimSelect from "slim-select"; @@ -102,7 +103,7 @@ async function initSelectOptions(): Promise { let LayoutList; try { - LanguageList = await Misc.getLanguageList(); + LanguageList = await JSONData.getLanguageList(); } catch (e) { console.error( Misc.createErrorMessage( @@ -114,7 +115,7 @@ async function initSelectOptions(): Promise { } try { - LayoutList = await Misc.getLayoutsList(); + LayoutList = await JSONData.getLayoutsList(); } catch (e) { console.error( Misc.createErrorMessage( @@ -205,7 +206,7 @@ async function filter(language: string): Promise { let languageWordList; try { - languageWordList = await Misc.getLanguage(language); + languageWordList = await JSONData.getLanguage(language); } catch (e) { Notifications.add( Misc.createErrorMessage(e, "Failed to filter language words"), @@ -286,7 +287,7 @@ async function setup(): Promise { return; } - const layout = await Misc.getLayout(layoutName); + const layout = await JSONData.getLayout(layoutName); $("#wordIncludeInput").val( presetToApply diff --git a/frontend/src/ts/pages/about.ts b/frontend/src/ts/pages/about.ts index a0477939a963..92a98687239a 100644 --- a/frontend/src/ts/pages/about.ts +++ b/frontend/src/ts/pages/about.ts @@ -1,4 +1,6 @@ import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; +import * as Numbers from "../utils/numbers"; import Page from "./page"; import Ape from "../ape"; import * as Notifications from "../elements/notifications"; @@ -44,10 +46,10 @@ function updateStatsAndHistogram(): void { $(".pageAbout #totalTimeTypingStat .valSmall").text("years"); $(".pageAbout #totalTimeTypingStat").attr( "aria-label", - Misc.numberWithSpaces(Math.round(secondsRounded / 3600)) + " hours" + Numbers.numberWithSpaces(Math.round(secondsRounded / 3600)) + " hours" ); - const startedWithMagnitude = Misc.getNumberWithMagnitude( + const startedWithMagnitude = Numbers.getNumberWithMagnitude( typingStatsResponseData.testsStarted ); @@ -61,10 +63,10 @@ function updateStatsAndHistogram(): void { ); $(".pageAbout #totalStartedTestsStat").attr( "aria-label", - Misc.numberWithSpaces(typingStatsResponseData.testsStarted) + " tests" + Numbers.numberWithSpaces(typingStatsResponseData.testsStarted) + " tests" ); - const completedWIthMagnitude = Misc.getNumberWithMagnitude( + const completedWIthMagnitude = Numbers.getNumberWithMagnitude( typingStatsResponseData.testsCompleted ); @@ -78,7 +80,8 @@ function updateStatsAndHistogram(): void { ); $(".pageAbout #totalCompletedTestsStat").attr( "aria-label", - Misc.numberWithSpaces(typingStatsResponseData.testsCompleted) + " tests" + Numbers.numberWithSpaces(typingStatsResponseData.testsCompleted) + + " tests" ); } } @@ -120,7 +123,7 @@ async function getStatsAndHistogramData(): Promise { async function fill(): Promise { let supporters: string[]; try { - supporters = await Misc.getSupportersList(); + supporters = await JSONData.getSupportersList(); } catch (e) { Notifications.add( Misc.createErrorMessage(e, "Failed to get supporters"), @@ -131,7 +134,7 @@ async function fill(): Promise { let contributors: string[]; try { - contributors = await Misc.getContributorsList(); + contributors = await JSONData.getContributorsList(); } catch (e) { Notifications.add( Misc.createErrorMessage(e, "Failed to get contributors"), diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index f47e253e75bd..4892abd079b5 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -11,7 +11,10 @@ import * as Focus from "../test/focus"; import * as TodayTracker from "../test/today-tracker"; import * as Notifications from "../elements/notifications"; import Page from "./page"; +import * as DateTime from "../utils/date-and-time"; import * as Misc from "../utils/misc"; +import * as Arrays from "../utils/arrays"; +import * as Numbers from "../utils/numbers"; import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; import * as Profile from "../elements/profile"; import format from "date-fns/format"; @@ -611,8 +614,8 @@ async function fillContent(): Promise { chartData.push({ x: filteredResults.length, - y: Misc.roundTo2(typingSpeedUnit.fromWpm(result.wpm)), - wpm: Misc.roundTo2(typingSpeedUnit.fromWpm(result.wpm)), + y: Numbers.roundTo2(typingSpeedUnit.fromWpm(result.wpm)), + wpm: Numbers.roundTo2(typingSpeedUnit.fromWpm(result.wpm)), acc: result.acc, mode: result.mode, mode2: result.mode2, @@ -620,7 +623,7 @@ async function fillContent(): Promise { language: result.language, timestamp: result.timestamp, difficulty: result.difficulty, - raw: Misc.roundTo2(typingSpeedUnit.fromWpm(result.rawWpm)), + raw: Numbers.roundTo2(typingSpeedUnit.fromWpm(result.rawWpm)), isPb: result.isPb ?? false, }); @@ -676,7 +679,7 @@ async function fillContent(): Promise { }); activityChartData_avgWpm.push({ x: dateInt, - y: Misc.roundTo2( + y: Numbers.roundTo2( typingSpeedUnit.fromWpm(dataPoint.totalWpm) / dataPoint.amount ), }); @@ -743,7 +746,7 @@ async function fillContent(): Promise { // add last point to pb pb.push({ x: 1, - y: Misc.lastElementFromArray(pb)?.y as number, + y: Arrays.lastElementFromArray(pb)?.y as number, }); const avgTen = []; @@ -846,7 +849,7 @@ async function fillContent(): Promise { } $(".pageAccount .timeTotalFiltered .val").text( - Misc.secondsToString(Math.round(totalSecondsFiltered), true, true) + DateTime.secondsToString(Math.round(totalSecondsFiltered), true, true) ); const speedUnit = Config.typingSpeedUnit; @@ -916,7 +919,7 @@ async function fillContent(): Promise { const wpmPoints = filteredResults.map((r) => r.wpm).reverse(); - const trend = Misc.findLineByLeastSquares(wpmPoints); + const trend = Numbers.findLineByLeastSquares(wpmPoints); if (trend) { const wpmChange = trend[1][1] - trend[0][1]; const wpmChangePerHour = wpmChange * (3600 / totalSecondsFiltered); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index c295d7f00d57..fb83c20b129f 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -2,6 +2,8 @@ import SettingsGroup from "../settings/settings-group"; import Config, * as UpdateConfig from "../config"; import * as Sound from "../controllers/sound-controller"; import * as Misc from "../utils/misc"; +import * as Strings from "../utils/strings"; +import * as JSONData from "../utils/json-data"; import * as DB from "../db"; import { toggleFunbox } from "../test/funbox/funbox"; import * as TagController from "../controllers/tag-controller"; @@ -440,7 +442,7 @@ async function fillSettingsPage(): Promise { let languageGroups; try { - languageGroups = await Misc.getLanguageGroups(); + languageGroups = await JSONData.getLanguageGroups(); } catch (e) { console.error( Misc.createErrorMessage( @@ -460,7 +462,7 @@ async function fillSettingsPage(): Promise { html += ``; for (const language of group.languages) { const selected = language === Config.language ? "selected" : ""; - const text = Misc.getLanguageDisplayString(language); + const text = Strings.getLanguageDisplayString(language); html += ``; } html += ``; @@ -476,7 +478,7 @@ async function fillSettingsPage(): Promise { let layoutsList; try { - layoutsList = await Misc.getLayoutsList(); + layoutsList = await JSONData.getLayoutsList(); } catch (e) { console.error(Misc.createErrorMessage(e, "Failed to refresh keymap")); } @@ -519,7 +521,7 @@ async function fillSettingsPage(): Promise { let themes; try { - themes = await Misc.getThemesList(); + themes = await JSONData.getThemesList(); } catch (e) { console.error( Misc.createErrorMessage(e, "Failed to load themes into dropdown boxes") @@ -579,7 +581,7 @@ async function fillSettingsPage(): Promise { let funboxList; try { - funboxList = await Misc.getFunboxList(); + funboxList = await JSONData.getFunboxList(); } catch (e) { console.error(Misc.createErrorMessage(e, "Failed to get funbox list")); } @@ -628,7 +630,7 @@ async function fillSettingsPage(): Promise { let fontsList; try { - fontsList = await Misc.getFontsList(); + fontsList = await JSONData.getFontsList(); } catch (e) { console.error( Misc.createErrorMessage(e, "Failed to update fonts settings buttons") @@ -807,7 +809,7 @@ function setActiveFunboxButton(): void { $(`.pageSettings .section[data-config-name='funbox'] .button`).removeClass( "disabled" ); - Misc.getFunboxList() + JSONData.getFunboxList() .then((funboxModes) => { funboxModes.forEach((funbox) => { if ( diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts index 66238cad5792..29d036566e6d 100644 --- a/frontend/src/ts/ready.ts +++ b/frontend/src/ts/ready.ts @@ -1,6 +1,7 @@ import * as ManualRestart from "./test/manual-restart-tracker"; import Config, * as UpdateConfig from "./config"; import * as Misc from "./utils/misc"; +import * as JSONData from "./utils/json-data"; import * as MonkeyPower from "./elements/monkey-power"; import * as NewVersionNotification from "./elements/version-check"; import * as Notifications from "./elements/notifications"; @@ -22,7 +23,7 @@ if (Misc.isDevEnvironment()) { `` ); } else { - Misc.getLatestReleaseFromGitHub() + JSONData.getLatestReleaseFromGitHub() .then((v) => { $("footer .currentVersion .text").text(v); void NewVersionNotification.show(v); diff --git a/frontend/src/ts/settings/theme-picker.ts b/frontend/src/ts/settings/theme-picker.ts index 27cdfa961b33..3af265f29bc4 100644 --- a/frontend/src/ts/settings/theme-picker.ts +++ b/frontend/src/ts/settings/theme-picker.ts @@ -1,6 +1,8 @@ import Config, * as UpdateConfig from "../config"; import * as ThemeController from "../controllers/theme-controller"; import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; +import * as Colors from "../utils/colors"; import * as Notifications from "../elements/notifications"; import * as ThemeColors from "../elements/theme-colors"; import * as ChartController from "../controllers/chart-controller"; @@ -82,7 +84,7 @@ function updateColors( } $(".colorConverter").css("color", color); - const hexColor: string | undefined = Misc.convertRGBtoHEX( + const hexColor: string | undefined = Colors.rgbStringtoHex( $(".colorConverter").css("color") ); if (hexColor === undefined) { @@ -171,7 +173,7 @@ export async function refreshButtons(): Promise { let themes; try { - themes = await Misc.getSortedThemesList(); + themes = await JSONData.getSortedThemesList(); } catch (e) { Notifications.add( Misc.createErrorMessage(e, "Failed to refresh theme buttons"), diff --git a/frontend/src/ts/test/british-english.ts b/frontend/src/ts/test/british-english.ts index ac7e1878f9d6..dd5b8ab222ef 100644 --- a/frontend/src/ts/test/british-english.ts +++ b/frontend/src/ts/test/british-english.ts @@ -1,8 +1,6 @@ import Config from "../config"; -import { - cachedFetchJson, - capitalizeFirstLetterOfEachWord, -} from "../utils/misc"; +import { capitalizeFirstLetterOfEachWord } from "../utils/strings"; +import { cachedFetchJson } from "../utils/json-data"; import * as CustomText from "../test/custom-text"; type BritishEnglishReplacement = { diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts index 6ed74bdb440c..842d9bca4e19 100644 --- a/frontend/src/ts/test/caret.ts +++ b/frontend/src/ts/test/caret.ts @@ -1,4 +1,5 @@ -import * as Misc from "../utils/misc"; +import * as Numbers from "../utils/numbers"; +import * as JSONData from "../utils/json-data"; import Config from "../config"; import * as TestInput from "./test-input"; import * as SlowTimer from "../states/slow-timer"; @@ -96,7 +97,7 @@ export async function updatePosition(noAnim = false): Promise { Math.min(currentLetterIndex - 1, currentWordNodeList.length - 1) ] as HTMLElement; - const currentLanguage = await Misc.getCurrentLanguage(Config.language); + const currentLanguage = await JSONData.getCurrentLanguage(Config.language); const isLanguageRightToLeft = currentLanguage.rightToLeft; const letterPosLeft = getTargetPositionLeft( fullWidthCaret, @@ -112,7 +113,7 @@ export async function updatePosition(noAnim = false): Promise { const letterHeight = currentLetter?.offsetHeight ?? previousLetter?.offsetHeight ?? - Config.fontSize * Misc.convertRemToPixels(1); + Config.fontSize * Numbers.convertRemToPixels(1); const diff = letterHeight - caret.offsetHeight; diff --git a/frontend/src/ts/test/english-punctuation.ts b/frontend/src/ts/test/english-punctuation.ts index d43deaeb75f9..8cdf09741692 100644 --- a/frontend/src/ts/test/english-punctuation.ts +++ b/frontend/src/ts/test/english-punctuation.ts @@ -1,7 +1,5 @@ -import { - capitalizeFirstLetterOfEachWord, - randomElementFromArray, -} from "../utils/misc"; +import { randomElementFromArray } from "../utils/arrays"; +import { capitalizeFirstLetterOfEachWord } from "../utils/strings"; type Pair = [string, string[]]; diff --git a/frontend/src/ts/test/funbox/funbox-validation.ts b/frontend/src/ts/test/funbox/funbox-validation.ts index dbea0b024edb..9325daaea119 100644 --- a/frontend/src/ts/test/funbox/funbox-validation.ts +++ b/frontend/src/ts/test/funbox/funbox-validation.ts @@ -1,6 +1,7 @@ import * as FunboxList from "./funbox-list"; import * as Notifications from "../../elements/notifications"; import * as Misc from "../../utils/misc"; +import * as Strings from "../../utils/strings"; export function checkFunboxForcedConfigs( key: string, @@ -139,7 +140,7 @@ export function canSetConfigWithCurrentFunboxes( if (errorCount > 0) { if (!noNotification) { Notifications.add( - `You can't set ${Misc.camelCaseToWords( + `You can't set ${Strings.camelCaseToWords( key )} to ${value} with currently active funboxes.`, 0, @@ -185,8 +186,8 @@ export function canSetFunboxWithConfig( const errorStrings = []; for (const error of errors) { errorStrings.push( - `${Misc.capitalizeFirstLetter( - Misc.camelCaseToWords(error.key) + `${Strings.capitalizeFirstLetter( + Strings.camelCaseToWords(error.key) )} cannot be set to ${error.value}.` ); } diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts index 471dd9688214..00915893ce82 100644 --- a/frontend/src/ts/test/funbox/funbox.ts +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -1,5 +1,10 @@ import * as Notifications from "../../elements/notifications"; import * as Misc from "../../utils/misc"; +import * as JSONData from "../../utils/json-data"; +import * as GetText from "../../utils/generate"; +import * as Numbers from "../../utils/numbers"; +import * as Arrays from "../../utils/arrays"; +import * as Strings from "../../utils/strings"; import * as ManualRestart from "../manual-restart-tracker"; import Config, * as UpdateConfig from "../../config"; import * as MemoryTimer from "./memory-funbox-timer"; @@ -20,6 +25,7 @@ import { } from "./funbox-validation"; import { Wordset } from "../wordset"; import * as LayoutfluidFunboxTimer from "./layoutfluid-funbox-timer"; +import * as DDR from "../../utils/ddr"; const prefixSize = 2; @@ -41,7 +47,7 @@ class CharDistribution { } public randomChar(): string { - const randomIndex = Misc.randomIntFromRange(0, this.count - 1); + const randomIndex = Numbers.randomIntFromRange(0, this.count - 1); let runningCount = 0; for (const [char, charCount] of Object.entries(this.chars)) { runningCount += charCount; @@ -126,7 +132,7 @@ FunboxList.setFunboxFunctions("tts", { FunboxList.setFunboxFunctions("arrows", { getWord(_wordset, wordIndex): string { - return Misc.chart2Word(wordIndex === 0); + return DDR.chart2Word(wordIndex === 0); }, rememberSettings(): void { save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); @@ -229,7 +235,7 @@ FunboxList.setFunboxFunctions("backwards", { FunboxList.setFunboxFunctions("capitals", { alterText(word: string): string { - return Misc.capitalizeFirstLetterOfEachWord(word); + return Strings.capitalizeFirstLetterOfEachWord(word); }, }); @@ -308,13 +314,13 @@ FunboxList.setFunboxFunctions("layoutfluid", { FunboxList.setFunboxFunctions("gibberish", { getWord(): string { - return Misc.getGibberish(); + return GetText.getGibberish(); }, }); FunboxList.setFunboxFunctions("58008", { getWord(): string { - let num = Misc.getNumbers(7); + let num = GetText.getNumbers(7); if (Config.language.startsWith("kurdish")) { num = Misc.convertNumberToArabic(num); } else if (Config.language.startsWith("nepali")) { @@ -325,21 +331,21 @@ FunboxList.setFunboxFunctions("58008", { punctuateWord(word: string): string { if (word.length > 3) { if (Math.random() < 0.5) { - word = Misc.setCharAt( + word = Strings.replaceCharAt( word, - Misc.randomIntFromRange(1, word.length - 2), + Numbers.randomIntFromRange(1, word.length - 2), "." ); } if (Math.random() < 0.75) { - const index = Misc.randomIntFromRange(1, word.length - 2); + const index = Numbers.randomIntFromRange(1, word.length - 2); if ( word[index - 1] !== "." && word[index + 1] !== "." && word[index + 1] !== "0" ) { - const special = Misc.randomElementFromArray(["/", "*", "-", "+"]); - word = Misc.setCharAt(word, index, special); + const special = Arrays.randomElementFromArray(["/", "*", "-", "+"]); + word = Strings.replaceCharAt(word, index, special); } } } @@ -358,7 +364,7 @@ FunboxList.setFunboxFunctions("58008", { FunboxList.setFunboxFunctions("ascii", { getWord(): string { - return Misc.getASCII(); + return GetText.getASCII(); }, punctuateWord(word: string): string { return word; @@ -367,7 +373,7 @@ FunboxList.setFunboxFunctions("ascii", { FunboxList.setFunboxFunctions("specials", { getWord(): string { - return Misc.getSpecials(); + return GetText.getSpecials(); }, }); @@ -490,7 +496,7 @@ FunboxList.setFunboxFunctions("IPv6", { FunboxList.setFunboxFunctions("binary", { getWord(): string { - return Misc.getBinary(); + return GetText.getBinary(); }, }); @@ -528,7 +534,7 @@ export function toggleFunbox(funbox: string): boolean { !Config.funbox.split("#").includes(funbox) ) { Notifications.add( - `${Misc.capitalizeFirstLetter( + `${Strings.capitalizeFirstLetter( funbox.replace(/_/g, " ") )} funbox is not compatible with the current funbox selection`, 0 @@ -600,7 +606,7 @@ export async function activate(funbox?: string): Promise { let language; try { - language = await Misc.getCurrentLanguage(Config.language); + language = await JSONData.getCurrentLanguage(Config.language); } catch (e) { Notifications.add( Misc.createErrorMessage(e, "Failed to activate funbox"), @@ -700,7 +706,7 @@ export async function rememberSettings(): Promise { FunboxList.setFunboxFunctions("morse", { alterText(word: string): string { - return Misc.convertToMorse(word); + return GetText.getMorse(word); }, }); diff --git a/frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts b/frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts index 4e2e4bb9d3b5..95df4a5cf4ad 100644 --- a/frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts +++ b/frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts @@ -1,4 +1,4 @@ -import { capitalizeFirstLetter } from "../../utils/misc"; +import { capitalizeFirstLetter } from "../../utils/strings"; export function show(): void { $("#typingTest #layoutfluidTimer").stop(true, true).animate( diff --git a/frontend/src/ts/test/ip-addresses.ts b/frontend/src/ts/test/ip-addresses.ts index c320718c7654..166613a9cecb 100644 --- a/frontend/src/ts/test/ip-addresses.ts +++ b/frontend/src/ts/test/ip-addresses.ts @@ -1,4 +1,4 @@ -import { randomIntFromRange } from "../utils/misc"; +import { randomIntFromRange } from "../utils/numbers"; function getRandomIPvXaddress( bits: number, diff --git a/frontend/src/ts/test/layout-emulator.ts b/frontend/src/ts/test/layout-emulator.ts index d14805474930..37210d6f5955 100644 --- a/frontend/src/ts/test/layout-emulator.ts +++ b/frontend/src/ts/test/layout-emulator.ts @@ -1,5 +1,6 @@ import Config from "../config"; import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import { capsState } from "./caps-warning"; import * as Notifications from "../elements/notifications"; @@ -31,7 +32,7 @@ export async function getCharFromEvent( let layout; try { - layout = await Misc.getLayout(Config.layout); + layout = await JSONData.getLayout(Config.layout); } catch (e) { Notifications.add( Misc.createErrorMessage(e, "Failed to emulate event"), diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index bbba5d9ed33a..85e5ef478d73 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -4,6 +4,8 @@ import Config from "../config"; import * as DB from "../db"; import * as SlowTimer from "../states/slow-timer"; import * as Misc from "../utils/misc"; +import * as Numbers from "../utils/numbers"; +import * as JSONData from "../utils/json-data"; import * as TestState from "./test-state"; import * as ConfigEvent from "../observables/config-event"; @@ -47,7 +49,7 @@ async function resetCaretPosition(): Promise { if (firstLetter === undefined || firstLetterHeight === undefined) return; - const currentLanguage = await Misc.getCurrentLanguage(Config.language); + const currentLanguage = await JSONData.getCurrentLanguage(Config.language); const isLanguageRightToLeft = currentLanguage.rightToLeft; caret.stop(true, true).animate( @@ -215,13 +217,15 @@ export async function update(expectedStepEnd: number): Promise { ); } - const currentLanguage = await Misc.getCurrentLanguage(Config.language); + const currentLanguage = await JSONData.getCurrentLanguage( + Config.language + ); const isLanguageRightToLeft = currentLanguage.rightToLeft; newTop = word.offsetTop + currentLetter.offsetTop - - Config.fontSize * Misc.convertRemToPixels(1) * 0.1; + Config.fontSize * Numbers.convertRemToPixels(1) * 0.1; newLeft; if (settings.currentLetterIndex === -1) { newLeft = diff --git a/frontend/src/ts/test/replay.ts b/frontend/src/ts/test/replay.ts index 15d7265e79cf..e28add4169b3 100644 --- a/frontend/src/ts/test/replay.ts +++ b/frontend/src/ts/test/replay.ts @@ -1,6 +1,7 @@ import config from "../config"; import * as Sound from "../controllers/sound-controller"; import * as TestInput from "./test-input"; +import * as Arrays from "../utils/arrays"; type ReplayAction = | "correctLetter" @@ -270,7 +271,7 @@ function playReplay(): void { let swTime = Math.round(lastTime / 1000); //starting time const swEndTime = Math.round( - (replayData[replayData.length - 1] as Replay).time / 1000 + (Arrays.lastElementFromArray(replayData) as Replay).time / 1000 ); while (swTime <= swEndTime) { const time = swTime; @@ -299,7 +300,7 @@ function playReplay(): void { "aria-label", "Start replay" ); - }, (replayData[replayData.length - 1] as Replay).time - lastTime) + }, (Arrays.lastElementFromArray(replayData) as Replay).time - lastTime) ); } diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 6b6cb75eaaf9..1ee32ce1eba7 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -12,7 +12,12 @@ import { isAuthenticated } from "../firebase"; import * as quoteRateModal from "../modals/quote-rate"; import * as GlarsesMode from "../states/glarses-mode"; import * as SlowTimer from "../states/slow-timer"; +import * as DateTime from "../utils/date-and-time"; import * as Misc from "../utils/misc"; +import * as Strings from "../utils/strings"; +import * as JSONData from "../utils/json-data"; +import * as Numbers from "../utils/numbers"; +import * as Arrays from "../utils/arrays"; import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; import * as FunboxList from "./funbox/funbox-list"; import * as PbCrown from "./pb-crown"; @@ -53,7 +58,7 @@ async function updateGraph(): Promise { for (let i = 1; i <= TestInput.wpmHistory.length; i++) { if (TestStats.lastSecondNotRound && i === TestInput.wpmHistory.length) { - labels.push(Misc.roundTo2(result.testDuration).toString()); + labels.push(Numbers.roundTo2(result.testDuration).toString()); } else { labels.push(i.toString()); } @@ -64,7 +69,7 @@ async function updateGraph(): Promise { const chartData1 = [ ...TestInput.wpmHistory.map((a) => - Misc.roundTo2(typingSpeedUnit.fromWpm(a)) + Numbers.roundTo2(typingSpeedUnit.fromWpm(a)) ), ]; @@ -72,7 +77,7 @@ async function updateGraph(): Promise { const chartData2 = [ ...result.chartData.raw.map((a) => - Misc.roundTo2(typingSpeedUnit.fromWpm(a)) + Numbers.roundTo2(typingSpeedUnit.fromWpm(a)) ), ]; @@ -88,7 +93,7 @@ async function updateGraph(): Promise { let smoothedRawData = chartData2; if (!useUnsmoothedRaw) { - smoothedRawData = Misc.smooth(smoothedRawData, 1); + smoothedRawData = Arrays.smooth(smoothedRawData, 1); smoothedRawData = smoothedRawData.map((a) => Math.round(a)); } @@ -175,7 +180,7 @@ export async function updateGraphPBLine(): Promise { ); if (lpb === 0) return; const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); - const chartlpb = Misc.roundTo2(typingSpeedUnit.fromWpm(lpb)).toFixed(2); + const chartlpb = Numbers.roundTo2(typingSpeedUnit.fromWpm(lpb)).toFixed(2); resultAnnotation.push({ display: true, type: "line", @@ -247,9 +252,9 @@ function updateWpmAndAcc(): void { $("#result .stats .raw .bottom").removeAttr("aria-label"); } - let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s"; + let time = Numbers.roundTo2(result.testDuration).toFixed(2) + "s"; if (result.testDuration > 61) { - time = Misc.secondsToString(Misc.roundTo2(result.testDuration)); + time = DateTime.secondsToString(Numbers.roundTo2(result.testDuration)); } $("#result .stats .time .bottom .text").text(time); // $("#result .stats .acc .bottom").removeAttr("aria-label"); @@ -309,7 +314,7 @@ function updateConsistency(): void { } function updateTime(): void { - const afkSecondsPercent = Misc.roundTo2( + const afkSecondsPercent = Numbers.roundTo2( (result.afkDuration / result.testDuration) * 100 ); $("#result .stats .time .bottom .afk").text(""); @@ -322,20 +327,20 @@ function updateTime(): void { ); if (Config.alwaysShowDecimalPlaces) { - let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s"; + let time = Numbers.roundTo2(result.testDuration).toFixed(2) + "s"; if (result.testDuration > 61) { - time = Misc.secondsToString(Misc.roundTo2(result.testDuration)); + time = DateTime.secondsToString(Numbers.roundTo2(result.testDuration)); } $("#result .stats .time .bottom .text").text(time); } else { let time = Math.round(result.testDuration) + "s"; if (result.testDuration > 61) { - time = Misc.secondsToString(Math.round(result.testDuration)); + time = DateTime.secondsToString(Math.round(result.testDuration)); } $("#result .stats .time .bottom .text").text(time); $("#result .stats .time .bottom").attr( "aria-label", - `${Misc.roundTo2(result.testDuration)}s (${ + `${Numbers.roundTo2(result.testDuration)}s (${ result.afkDuration }s afk ${afkSecondsPercent}%)` ); @@ -449,7 +454,7 @@ async function updateTags(dontSave: boolean): Promise { const funboxes = result.funbox?.split("#") ?? []; const funboxObjects = await Promise.all( - funboxes.map(async (f) => Misc.getFunbox(f)) + funboxes.map(async (f) => JSONData.getFunbox(f)) ); const allFunboxesCanGetPb = funboxObjects.every((f) => f?.canGetPb); @@ -497,7 +502,7 @@ async function updateTags(dontSave: boolean): Promise { ).removeClass("hidden"); $(`#result .stats .tags .bottom div[tagid="${tag._id}"]`).attr( "aria-label", - "+" + Misc.roundTo2(result.wpm - tpb) + "+" + Numbers.roundTo2(result.wpm - tpb) ); // console.log("new pb for tag " + tag.display); } else { @@ -526,7 +531,7 @@ async function updateTags(dontSave: boolean): Promise { position: annotationSide, xAdjust: labelAdjust, enabled: true, - content: `${tag.display} PB: ${Misc.roundTo2( + content: `${tag.display} PB: ${Numbers.roundTo2( typingSpeedUnit.fromWpm(tpb) ).toFixed(2)}`, }, @@ -562,7 +567,7 @@ function updateTestType(randomQuote: MonkeyTypes.Quote | null): void { f.properties?.includes("ignoresLanguage") ) !== undefined; if (Config.mode !== "custom" && !ignoresLanguage) { - testType += "
" + Misc.getLanguageDisplayString(result.language); + testType += "
" + Strings.getLanguageDisplayString(result.language); } if (Config.punctuation) { testType += "
punctuation"; diff --git a/frontend/src/ts/test/shift-tracker.ts b/frontend/src/ts/test/shift-tracker.ts index fa68c8c5308f..627e5679e7cd 100644 --- a/frontend/src/ts/test/shift-tracker.ts +++ b/frontend/src/ts/test/shift-tracker.ts @@ -1,5 +1,5 @@ import Config from "../config"; -import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import { capsState } from "./caps-warning"; import * as Notifications from "../elements/notifications"; @@ -124,7 +124,7 @@ async function updateKeymapLegendCasing(): Promise { : Config.layout : Config.keymapLayout; - const layout = await Misc.getLayout(layoutName).catch(() => undefined); + const layout = await JSONData.getLayout(layoutName).catch(() => undefined); if (layout === undefined) { Notifications.add("Failed to load keymap layout", -1); diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index 3f2167614e00..1a03a00e3ed0 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -1,5 +1,6 @@ import * as TestWords from "./test-words"; -import { mean, roundTo2 } from "../utils/misc"; +import { lastElementFromArray } from "../utils/arrays"; +import { mean, roundTo2 } from "../utils/numbers"; const keysToTrack = [ "NumpadMultiply", @@ -167,7 +168,7 @@ class Input { } getHistoryLast(): string | undefined { - return this.history[this.history.length - 1]; + return lastElementFromArray(this.history); } } @@ -308,7 +309,7 @@ export function forceKeyup(now: number): void { for (const keyOrder of keysOrder) { recordKeyupTime(now, keyOrder[0]); } - const last = keysOrder[keysOrder.length - 1]?.[0] as string; + const last = lastElementFromArray(keysOrder)?.[0] as string; const index = keyDownData[last]?.index; if (last !== undefined && index !== undefined) { keypressTimings.duration.array[index] = avg; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 6417ebffe51b..49a55218c968 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -2,8 +2,11 @@ import Ape from "../ape"; import * as TestUI from "./test-ui"; import * as ManualRestart from "./manual-restart-tracker"; import Config, * as UpdateConfig from "../config"; +import * as Strings from "../utils/strings"; import * as Misc from "../utils/misc"; - +import * as Arrays from "../utils/arrays"; +import * as JSONData from "../utils/json-data"; +import * as Numbers from "../utils/numbers"; import * as Notifications from "../elements/notifications"; import * as CustomText from "./custom-text"; import * as CustomTextState from "../states/custom-text-name"; @@ -205,11 +208,11 @@ export function restart(options = {} as RestartOptions): void { TestInput.pushAfkToHistory(); const testSeconds = TestStats.calculateTestSeconds(performance.now()); const afkseconds = TestStats.calculateAfkSeconds(testSeconds); - let tt = Misc.roundTo2(testSeconds - afkseconds); + let tt = Numbers.roundTo2(testSeconds - afkseconds); if (tt < 0) tt = 0; TestStats.incrementIncompleteSeconds(tt); TestStats.incrementRestartCount(); - const acc = Misc.roundTo2(TestStats.calculateAccuracy()); + const acc = Numbers.roundTo2(TestStats.calculateAccuracy()); TestStats.pushIncompleteTest(acc, tt); } } @@ -348,7 +351,7 @@ export function restart(options = {} as RestartOptions): void { TestUI.showWords(); if (Config.keymapMode === "next" && Config.mode !== "zen") { void KeymapEvent.highlight( - Misc.nthElementFromArray( + Arrays.nthElementFromArray( [...TestWords.words.getCurrent()], 0 ) as string @@ -443,7 +446,7 @@ export async function init(): Promise { let language; try { - language = await Misc.getLanguage(Config.language); + language = await JSONData.getLanguage(Config.language); } catch (e) { Notifications.add( Misc.createErrorMessage(e, "Failed to load language"), @@ -550,7 +553,7 @@ export async function init(): Promise { if (Config.keymapMode === "next" && Config.mode !== "zen") { void KeymapEvent.highlight( - Misc.nthElementFromArray([...TestWords.words.getCurrent()], 0) as string + Arrays.nthElementFromArray([...TestWords.words.getCurrent()], 0) as string ); } Funbox.toggleScript(TestWords.words.getCurrent()); @@ -634,10 +637,10 @@ export async function addWord(): Promise { const language: MonkeyTypes.LanguageObject = Config.mode !== "custom" - ? await Misc.getCurrentLanguage(Config.language) + ? await JSONData.getCurrentLanguage(Config.language) : { //borrow the direction of the current language - ...(await Misc.getCurrentLanguage(Config.language)), + ...(await JSONData.getCurrentLanguage(Config.language)), words: CustomText.text, }; const wordset = await Wordset.withWords(language.words); @@ -699,14 +702,14 @@ function buildCompletedEvent( difficultyFailed: boolean ): SharedTypes.CompletedEvent { //build completed event object - let stfk = Misc.roundTo2( + let stfk = Numbers.roundTo2( TestInput.keypressTimings.spacing.first - TestStats.start ); if (stfk < 0 || Config.mode === "zen") { stfk = 0; } - let lkte = Misc.roundTo2( + let lkte = Numbers.roundTo2( TestStats.end - TestInput.keypressTimings.spacing.last ); if (lkte < 0 || Config.mode === "zen") { @@ -750,9 +753,9 @@ function buildCompletedEvent( ); } - const stddev = Misc.stdDev(rawPerSecond); - const avg = Misc.mean(rawPerSecond); - let consistency = Misc.roundTo2(Misc.kogasa(stddev / avg)); + const stddev = Numbers.stdDev(rawPerSecond); + const avg = Numbers.mean(rawPerSecond); + let consistency = Numbers.roundTo2(Misc.kogasa(stddev / avg)); let keyConsistencyArray = TestInput.keypressTimings.spacing.array.slice(); if (keyConsistencyArray.length > 0) { keyConsistencyArray = keyConsistencyArray.slice( @@ -760,9 +763,9 @@ function buildCompletedEvent( keyConsistencyArray.length - 1 ); } - let keyConsistency = Misc.roundTo2( + let keyConsistency = Numbers.roundTo2( Misc.kogasa( - Misc.stdDev(keyConsistencyArray) / Misc.mean(keyConsistencyArray) + Numbers.stdDev(keyConsistencyArray) / Numbers.mean(keyConsistencyArray) ) ); if (!consistency || isNaN(consistency)) { @@ -784,9 +787,9 @@ function buildCompletedEvent( }; //wpm consistency - const stddev3 = Misc.stdDev(chartData.wpm ?? []); - const avg3 = Misc.mean(chartData.wpm ?? []); - const wpmCons = Misc.roundTo2(Misc.kogasa(stddev3 / avg3)); + const stddev3 = Numbers.stdDev(chartData.wpm ?? []); + const avg3 = Numbers.mean(chartData.wpm ?? []); + const wpmCons = Numbers.roundTo2(Misc.kogasa(stddev3 / avg3)); const wpmConsistency = isNaN(wpmCons) ? 0 : wpmCons; let customText: SharedTypes.CustomText | null = null; @@ -811,7 +814,7 @@ function buildCompletedEvent( const afkDuration = TestStats.calculateAfkSeconds(duration); let language = Config.language; if (Config.mode === "quote") { - language = Misc.removeLanguageSize(Config.language); + language = Strings.removeLanguageSize(Config.language); } const quoteLength = TestWords.randomQuote?.group ?? -1; @@ -840,13 +843,13 @@ function buildCompletedEvent( incompleteTestSeconds: TestStats.incompleteSeconds < 0 ? 0 - : Misc.roundTo2(TestStats.incompleteSeconds), + : Numbers.roundTo2(TestStats.incompleteSeconds), difficulty: Config.difficulty, blindMode: Config.blindMode, tags: activeTagsIds, keySpacing: TestInput.keypressTimings.spacing.array, keyDuration: TestInput.keypressTimings.duration.array, - keyOverlap: Misc.roundTo2(TestInput.keyOverlap.total), + keyOverlap: Numbers.roundTo2(TestInput.keyOverlap.total), lastKeyToEnd: lkte, startToFirstKey: stfk, consistency: consistency, @@ -1036,7 +1039,7 @@ export async function finish(difficultyFailed = false): Promise { if (TestState.isRepeated) { const testSeconds = completedEvent.testDuration; const afkseconds = completedEvent.afkDuration; - let tt = Misc.roundTo2(testSeconds - afkseconds); + let tt = Numbers.roundTo2(testSeconds - afkseconds); if (tt < 0) tt = 0; const acc = completedEvent.acc; TestStats.incrementIncompleteSeconds(tt); @@ -1087,7 +1090,7 @@ export async function finish(difficultyFailed = false): Promise { completedEvent.testDuration + (TestStats.incompleteSeconds < 0 ? 0 - : Misc.roundTo2(TestStats.incompleteSeconds)) - + : Numbers.roundTo2(TestStats.incompleteSeconds)) - completedEvent.afkDuration ); Result.updateTodayTracker(); @@ -1317,11 +1320,11 @@ export function fail(reason: string): void { if (!TestState.savingEnabled) return; const testSeconds = TestStats.calculateTestSeconds(performance.now()); const afkseconds = TestStats.calculateAfkSeconds(testSeconds); - let tt = Misc.roundTo2(testSeconds - afkseconds); + let tt = Numbers.roundTo2(testSeconds - afkseconds); if (tt < 0) tt = 0; TestStats.incrementIncompleteSeconds(tt); TestStats.incrementRestartCount(); - const acc = Misc.roundTo2(TestStats.calculateAccuracy()); + const acc = Numbers.roundTo2(TestStats.calculateAccuracy()); TestStats.pushIncompleteTest(acc, tt); } @@ -1486,7 +1489,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => { ) { setTimeout(() => { void KeymapEvent.highlight( - Misc.nthElementFromArray( + Arrays.nthElementFromArray( [...TestWords.words.getCurrent()], 0 ) as string diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 9ea34a76fe5e..12d384150d02 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -1,10 +1,11 @@ import Hangul from "hangul-js"; import Config from "../config"; -import * as Misc from "../utils/misc"; +import * as Strings from "../utils/strings"; import * as TestInput from "./test-input"; import * as TestWords from "./test-words"; import * as FunboxList from "./funbox/funbox-list"; import * as TestState from "./test-state"; +import * as Numbers from "../utils/numbers"; type CharCount = { spaces: number; @@ -72,7 +73,7 @@ export function getStats(): unknown { ) / TestInput.keypressTimings.spacing.array.length; // @ts-expect-error - ret.keypressTimings.spacing.sd = Misc.stdDev( + ret.keypressTimings.spacing.sd = Numbers.stdDev( TestInput.keypressTimings.spacing.array as number[] ); } catch (e) { @@ -86,7 +87,7 @@ export function getStats(): unknown { ) / TestInput.keypressTimings.duration.array.length; // @ts-expect-error - ret.keypressTimings.duration.sd = Misc.stdDev( + ret.keypressTimings.duration.sd = Numbers.stdDev( TestInput.keypressTimings.duration.array as number[] ); } catch (e) { @@ -145,10 +146,10 @@ export function calculateWpmAndRaw( TestState.isActive ? performance.now() : end ); const chars = countChars(); - const wpm = Misc.roundTo2( + const wpm = Numbers.roundTo2( ((chars.correctWordChars + chars.correctSpaces) * (60 / testSeconds)) / 5 ); - const raw = Misc.roundTo2( + const raw = Numbers.roundTo2( ((chars.allCorrectChars + chars.spaces + chars.incorrectChars + @@ -211,7 +212,7 @@ export function calculateBurst(): number { ?.length ?? 0; } if (wordLength === 0) return 0; - const speed = Misc.roundTo2((wordLength * (60 / timeToWrite)) / 5); + const speed = Numbers.roundTo2((wordLength * (60 / timeToWrite)) / 5); return Math.round(speed); } @@ -291,7 +292,7 @@ function countChars(): CharCount { correctChars += targetWord.length; if ( i < inputWords.length - 1 && - Misc.getLastChar(inputWord as string) !== "\n" + Strings.getLastChar(inputWord as string) !== "\n" ) { correctspaces++; } @@ -372,7 +373,7 @@ export function calculateStats(): Stats { " (performance.now based)" ); if (Config.mode !== "custom") { - testSeconds = Misc.roundTo2(testSeconds); + testSeconds = Numbers.roundTo2(testSeconds); console.debug( "Mode is not custom - rounding to 2. New time: ", testSeconds @@ -380,7 +381,7 @@ export function calculateStats(): Stats { } const chars = countChars(); const { wpm, raw } = calculateWpmAndRaw(true); - const acc = Misc.roundTo2(calculateAccuracy()); + const acc = Numbers.roundTo2(calculateAccuracy()); const ret = { wpm: isNaN(wpm) ? 0 : wpm, wpmRaw: isNaN(raw) ? 0 : raw, @@ -394,7 +395,7 @@ export function calculateStats(): Stats { chars.spaces + chars.incorrectChars + chars.extraChars, - time: Misc.roundTo2(testSeconds), + time: Numbers.roundTo2(testSeconds), spaces: chars.spaces, correctSpaces: chars.correctSpaces, }; diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 307823d5c569..9d2984d3d73f 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -9,7 +9,7 @@ import * as TestStats from "./test-stats"; import * as TestInput from "./test-input"; import * as TestWords from "./test-words"; import * as Monkey from "./monkey"; -import * as Misc from "../utils/misc"; +import * as Numbers from "../utils/numbers"; import * as Notifications from "../elements/notifications"; import * as Caret from "./caret"; import * as SlowTimer from "../states/slow-timer"; @@ -77,7 +77,7 @@ function monkey(wpmAndRaw: MonkeyTypes.WpmAndRaw): void { function calculateAcc(): number { if (timerDebug) console.time("calculate acc"); - const acc = Misc.roundTo2(TestStats.calculateAccuracy()); + const acc = Numbers.roundTo2(TestStats.calculateAccuracy()); if (timerDebug) console.timeEnd("calculate acc"); return acc; } diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 8496bd18f546..cb8813e4123c 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -9,6 +9,10 @@ import * as Caret from "./caret"; import * as OutOfFocus from "./out-of-focus"; import * as Replay from "./replay"; import * as Misc from "../utils/misc"; +import * as Strings from "../utils/strings"; +import * as JSONData from "../utils/json-data"; +import * as Numbers from "../utils/numbers"; +import { blendTwoHexColors } from "../utils/colors"; import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; import * as SlowTimer from "../states/slow-timer"; import * as CompositionState from "../states/composition"; @@ -55,7 +59,7 @@ async function joinOverlappingHints( activeWordLetters: NodeListOf, hintElements: HTMLCollection ): Promise { - const currentLanguage = await Misc.getCurrentLanguage(Config.language); + const currentLanguage = await JSONData.getCurrentLanguage(Config.language); const isLanguageRTL = currentLanguage.rightToLeft; let i = 0; @@ -108,8 +112,8 @@ const debouncedZipfCheck = debounce(250, async () => { const supports = await Misc.checkIfLanguageSupportsZipf(Config.language); if (supports === "no") { Notifications.add( - `${Misc.capitalizeFirstLetter( - Misc.getLanguageDisplayString(Config.language) + `${Strings.capitalizeFirstLetter( + Strings.getLanguageDisplayString(Config.language) )} does not support Zipf funbox, because the list is not ordered by frequency. Please try another word list.`, 0, { @@ -119,8 +123,8 @@ const debouncedZipfCheck = debounce(250, async () => { } if (supports === "unknown") { Notifications.add( - `${Misc.capitalizeFirstLetter( - Misc.getLanguageDisplayString(Config.language) + `${Strings.capitalizeFirstLetter( + Strings.getLanguageDisplayString(Config.language) )} may not support Zipf funbox, because we don't know if it's ordered by frequency or not. If you would like to add this label, please contact us.`, 0, { @@ -255,7 +259,7 @@ async function updateHintsPosition(): Promise { ) return; - const currentLanguage = await Misc.getCurrentLanguage(Config.language); + const currentLanguage = await JSONData.getCurrentLanguage(Config.language); const isLanguageRTL = currentLanguage.rightToLeft; let wordEl: HTMLElement | undefined; @@ -447,7 +451,7 @@ function updateWordsHeight(force = false): void { } $(".outOfFocusWarning").css( "margin-top", - wordHeight + Misc.convertRemToPixels(1) / 2 + "px" + wordHeight + Numbers.convertRemToPixels(1) / 2 + "px" ); } else { let finalWordsHeight: number, finalWrapperHeight: number; @@ -501,7 +505,7 @@ function updateWordsHeight(force = false): void { .css("overflow", "hidden"); $(".outOfFocusWarning").css( "margin-top", - finalWrapperHeight / 2 - Misc.convertRemToPixels(1) / 2 + "px" + finalWrapperHeight / 2 - Numbers.convertRemToPixels(1) / 2 + "px" ); } @@ -625,8 +629,8 @@ export async function screenshot(): Promise { true ) as number; /*clientHeight/offsetHeight from div#target*/ try { - const paddingX = Misc.convertRemToPixels(2); - const paddingY = Misc.convertRemToPixels(2); + const paddingX = Numbers.convertRemToPixels(2); + const paddingY = Numbers.convertRemToPixels(2); const canvas = await ( await gethtml2canvas() @@ -954,7 +958,7 @@ export function updatePremid(): void { fbtext = " " + Config.funbox.split("#").join(" "); } $(".pageTest #premidTestMode").text( - `${Config.mode} ${mode2} ${Misc.getLanguageDisplayString( + `${Config.mode} ${mode2} ${Strings.getLanguageDisplayString( Config.language )}${fbtext}` ); @@ -1290,9 +1294,9 @@ export async function applyBurstHeatmap(): Promise { let colors = [ themeColors.colorfulError, - Misc.blendTwoHexColors(themeColors.colorfulError, themeColors.text, 0.5), + blendTwoHexColors(themeColors.colorfulError, themeColors.text, 0.5), themeColors.text, - Misc.blendTwoHexColors(themeColors.main, themeColors.text, 0.5), + blendTwoHexColors(themeColors.main, themeColors.text, 0.5), themeColors.main, ]; let unreachedColor = themeColors.sub; @@ -1300,13 +1304,9 @@ export async function applyBurstHeatmap(): Promise { if (themeColors.main === themeColors.text) { colors = [ themeColors.colorfulError, - Misc.blendTwoHexColors( - themeColors.colorfulError, - themeColors.text, - 0.5 - ), + blendTwoHexColors(themeColors.colorfulError, themeColors.text, 0.5), themeColors.sub, - Misc.blendTwoHexColors(themeColors.sub, themeColors.text, 0.5), + blendTwoHexColors(themeColors.sub, themeColors.text, 0.5), themeColors.main, ]; unreachedColor = themeColors.subAlt; diff --git a/frontend/src/ts/test/timer-progress.ts b/frontend/src/ts/test/timer-progress.ts index 3fd1809506af..a025c655ff45 100644 --- a/frontend/src/ts/test/timer-progress.ts +++ b/frontend/src/ts/test/timer-progress.ts @@ -1,6 +1,6 @@ import Config from "../config"; import * as CustomText from "./custom-text"; -import * as Misc from "../utils/misc"; +import * as DateTime from "../utils/date-and-time"; import * as TestWords from "./test-words"; import * as TestInput from "./test-input"; import * as Time from "../states/time"; @@ -128,17 +128,17 @@ export function update(): void { "linear" ); } else if (Config.timerStyle === "text") { - let displayTime = Misc.secondsToString(maxtime - time); + let displayTime = DateTime.secondsToString(maxtime - time); if (maxtime === 0) { - displayTime = Misc.secondsToString(time); + displayTime = DateTime.secondsToString(time); } if (timerNumberElement !== null) { timerNumberElement.innerHTML = "
" + displayTime + "
"; } } else if (Config.timerStyle === "mini") { - let displayTime = Misc.secondsToString(maxtime - time); + let displayTime = DateTime.secondsToString(maxtime - time); if (maxtime === 0) { - displayTime = Misc.secondsToString(time); + displayTime = DateTime.secondsToString(time); } if (miniTimerNumberElement !== null) { miniTimerNumberElement.innerHTML = displayTime; diff --git a/frontend/src/ts/test/today-tracker.ts b/frontend/src/ts/test/today-tracker.ts index 43da50fbb968..592d2077db23 100644 --- a/frontend/src/ts/test/today-tracker.ts +++ b/frontend/src/ts/test/today-tracker.ts @@ -1,4 +1,4 @@ -import * as Misc from "../utils/misc"; +import * as DateTime from "../utils/date-and-time"; import * as DB from "../db"; let seconds = 0; @@ -17,7 +17,7 @@ export function addSeconds(s: number): void { } export function getString(): string { - const secString = Misc.secondsToString(Math.round(seconds), true, true); + const secString = DateTime.secondsToString(Math.round(seconds), true, true); return secString + (addedAllToday ? " today" : " session"); } diff --git a/frontend/src/ts/test/tts.ts b/frontend/src/ts/test/tts.ts index 9e79b7fbc83e..27b9dd98c245 100644 --- a/frontend/src/ts/test/tts.ts +++ b/frontend/src/ts/test/tts.ts @@ -1,5 +1,5 @@ import Config from "../config"; -import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import * as ConfigEvent from "../observables/config-event"; import * as TTSEvent from "../observables/tts-event"; @@ -7,7 +7,7 @@ let voice: SpeechSynthesisUtterance | undefined; export async function setLanguage(lang = Config.language): Promise { if (!voice) return; - const language = await Misc.getLanguage(lang); + const language = await JSONData.getLanguage(lang); const bcp = language.bcp47 ?? "en-US"; voice.lang = bcp; } diff --git a/frontend/src/ts/test/wikipedia.ts b/frontend/src/ts/test/wikipedia.ts index 481c35c8f277..20fbef8c258f 100644 --- a/frontend/src/ts/test/wikipedia.ts +++ b/frontend/src/ts/test/wikipedia.ts @@ -1,5 +1,6 @@ import * as Loader from "../elements/loader"; import * as Misc from "../utils/misc"; +import * as JSONData from "../utils/json-data"; import { Section } from "../utils/misc"; export async function getTLD( @@ -249,7 +250,7 @@ export async function getSection(language: string): Promise
{ let currentLanguageGroup: MonkeyTypes.LanguageGroup | undefined; try { - currentLanguageGroup = await Misc.findCurrentGroup(language); + currentLanguageGroup = await JSONData.getCurrentGroup(language); } catch (e) { console.error( Misc.createErrorMessage(e, "Failed to find current language group") diff --git a/frontend/src/ts/test/words-generator.ts b/frontend/src/ts/test/words-generator.ts index 3bd7ce9286c8..976f9a5a88d1 100644 --- a/frontend/src/ts/test/words-generator.ts +++ b/frontend/src/ts/test/words-generator.ts @@ -9,7 +9,10 @@ import * as LazyMode from "./lazy-mode"; import * as EnglishPunctuation from "./english-punctuation"; import * as PractiseWords from "./practise-words"; import * as Misc from "../utils/misc"; +import * as Strings from "../utils/strings"; +import * as Arrays from "../utils/arrays"; import * as TestState from "../test/test-state"; +import * as GetText from "../utils/generate"; function shouldCapitalize(lastChar: string): boolean { return /[?!.؟]/.test(lastChar); @@ -26,7 +29,7 @@ export async function punctuateWord( const currentLanguage = Config.language.split("_")[0]; - const lastChar = Misc.getLastChar(previousWord); + const lastChar = Strings.getLastChar(previousWord); const funbox = FunboxList.get(Config.funbox).find( (f) => f.functions?.punctuateWord @@ -41,7 +44,7 @@ export async function punctuateWord( ) { //always capitalise the first word or if there was a dot unless using a code alphabet or the Georgian language - word = Misc.capitalizeFirstLetterOfEachWord(word); + word = Strings.capitalizeFirstLetterOfEachWord(word); if (currentLanguage === "turkish") { word = word.replace(/I/g, "İ"); @@ -258,12 +261,12 @@ export async function punctuateWord( !Config.language.startsWith("code_css")) || Config.language.startsWith("code_arduino") ) { - word = Misc.randomElementFromArray(specialsC); + word = Arrays.randomElementFromArray(specialsC); } else { if (Config.language.startsWith("code_javascript")) { - word = Misc.randomElementFromArray([...specials, "`"]); + word = Arrays.randomElementFromArray([...specials, "`"]); } else { - word = Misc.randomElementFromArray(specials); + word = Arrays.randomElementFromArray(specials); } } } else if ( @@ -529,8 +532,8 @@ export async function generateWords( i, language, limit, - Misc.nthElementFromArray(ret.words, -1) ?? "", - Misc.nthElementFromArray(ret.words, -2) ?? "" + Arrays.nthElementFromArray(ret.words, -1) ?? "", + Arrays.nthElementFromArray(ret.words, -2) ?? "" ); ret.words.push(nextWord.word); ret.sectionIndexes.push(nextWord.sectionIndex); @@ -624,7 +627,7 @@ async function generateQuoteWords( rq = randomQuote; } - rq.language = Misc.removeLanguageSize(Config.language); + rq.language = Strings.removeLanguageSize(Config.language); rq.text = rq.text.replace(/ +/gm, " "); rq.text = rq.text.replace(/( *(\r\n|\r|\n) *)/g, "\n "); rq.text = rq.text.replace(/…/g, "..."); @@ -658,8 +661,8 @@ async function generateQuoteWords( i, language, limit, - Misc.nthElementFromArray(ret.words, -1) ?? "", - Misc.nthElementFromArray(ret.words, -2) ?? "" + Arrays.nthElementFromArray(ret.words, -1) ?? "", + Arrays.nthElementFromArray(ret.words, -2) ?? "" ); ret.words.push(nextWord.word); ret.sectionIndexes.push(i); @@ -721,8 +724,8 @@ export async function getNextWord( } else if (Config.mode === "custom" && CustomText.isSectionRandom) { randomWord = wordset.randomWord(funboxFrequency); - const previousSection = Misc.nthElementFromArray(sectionHistory, -1); - const previousSection2 = Misc.nthElementFromArray(sectionHistory, -2); + const previousSection = Arrays.nthElementFromArray(sectionHistory, -1); + const previousSection2 = Arrays.nthElementFromArray(sectionHistory, -2); let regenerationCount = 0; while ( @@ -821,7 +824,7 @@ export async function getNextWord( } if (Config.numbers) { if (Math.random() < 0.1) { - randomWord = Misc.getNumbers(4); + randomWord = GetText.getNumbers(4); if (Config.language.startsWith("kurdish")) { randomWord = Misc.convertNumberToArabic(randomWord); diff --git a/frontend/src/ts/test/wordset.ts b/frontend/src/ts/test/wordset.ts index efcf2a2f933e..0eefcd4e26b3 100644 --- a/frontend/src/ts/test/wordset.ts +++ b/frontend/src/ts/test/wordset.ts @@ -1,5 +1,6 @@ import * as FunboxList from "./funbox/funbox-list"; -import { dreymarIndex, randomElementFromArray } from "../utils/misc"; +import { dreymarIndex } from "../utils/misc"; +import { randomElementFromArray } from "../utils/arrays"; import Config from "../config"; let currentWordset: Wordset | null = null; diff --git a/frontend/src/ts/utils/arrays.ts b/frontend/src/ts/utils/arrays.ts new file mode 100644 index 000000000000..772e56223514 --- /dev/null +++ b/frontend/src/ts/utils/arrays.ts @@ -0,0 +1,101 @@ +import { randomIntFromRange } from "./numbers"; + +/** + * Applies a smoothing algorithm to an array of numbers. + * @param arr The input array of numbers. + * @param windowSize The size of the window used for smoothing. + * @param getter An optional function to extract values from the array elements. Defaults to the identity function. + * @returns An array of smoothed values, where each value is the average of itself and its neighbors within the window. + */ +export function smooth( + arr: number[], + windowSize: number, + getter = (value: number): number => value +): number[] { + const get = getter; + const result = []; + + for (let i = 0; i < arr.length; i += 1) { + const leftOffeset = i - windowSize; + const from = leftOffeset >= 0 ? leftOffeset : 0; + const to = i + windowSize + 1; + + let count = 0; + let sum = 0; + for (let j = from; j < to && j < arr.length; j += 1) { + sum += get(arr[j] as number); + count += 1; + } + + result[i] = sum / count; + } + + return result; +} + +/** + * Shuffle an array of elements using the Fisher–Yates algorithm. + * This function mutates the input array. + * @param elements + */ +export function shuffle(elements: T[]): void { + for (let i = elements.length - 1; i > 0; --i) { + const j = randomIntFromRange(0, i); + const temp = elements[j]; + elements[j] = elements[i] as T; + elements[i] = temp as T; + } +} + +/** + * Returns the last element of an array. + * @param array The input array. + * @returns The last element of the array, or undefined if the array is empty. + */ +export function lastElementFromArray(array: T[]): T | undefined { + return array[array.length - 1]; +} + +/** + * Checks if two unsorted arrays are equal, i.e., they have the same elements regardless of order. + * @param a The first array. + * @param b The second array. + * @returns True if the arrays are equal, false otherwise. + */ +export function areUnsortedArraysEqual(a: unknown[], b: unknown[]): boolean { + return a.length === b.length && a.every((v) => b.includes(v)); +} + +/** + * Checks if two sorted arrays are equal, i.e., they have the same elements in the same order. + * @param a The first array. + * @param b The second array. + * @returns True if the arrays are equal, false otherwise. + */ +export function areSortedArraysEqual(a: unknown[], b: unknown[]): boolean { + return a.length === b.length && a.every((v, i) => v === b[i]); +} + +/** + * Returns a random element from an array. + * @param array The input array. + * @returns A random element from the array. + */ +export function randomElementFromArray(array: T[]): T { + return array[randomIntFromRange(0, array.length - 1)] as T; +} + +/** + * Returns the element at the specified index from an array. + * Negative index values count from the end of the array. + * @param array The input array. + * @param index The index of the element to return. + * @returns The element at the specified index, or undefined if the index is out of bounds. + */ +export function nthElementFromArray( + array: T[], + index: number +): T | undefined { + index = index < 0 ? array.length + index : index; + return array[index]; +} diff --git a/frontend/src/ts/utils/colors.ts b/frontend/src/ts/utils/colors.ts new file mode 100644 index 000000000000..c8b055e9acc5 --- /dev/null +++ b/frontend/src/ts/utils/colors.ts @@ -0,0 +1,189 @@ +/** + * Utility functions for color conversions and operations. + */ + +import { normal as normalBlend } from "color-blend"; + +/** + * Blends two hexadecimal colors with a given opacity. + * @param color1 The first hexadecimal color value. + * @param color2 The second hexadecimal color value. + * @param opacity The opacity value between 0 and 1. + * @returns A new hexadecimal color value representing the blend of color1 and color2. + */ +export function blendTwoHexColors( + color1: string, + color2: string, + opacity: number +): string { + const rgb1 = hexToRgb(color1); + const rgb2 = hexToRgb(color2); + + if (rgb1 && rgb2) { + const rgba1 = { + r: rgb1.r, + g: rgb1.g, + b: rgb1.b, + a: 1, + }; + const rgba2 = { + r: rgb2.r, + g: rgb2.g, + b: rgb2.b, + a: opacity, + }; + const blended = normalBlend(rgba1, rgba2); + return rgbToHex(blended.r, blended.g, blended.b); + } else { + return "#000000"; + } +} + +/** + * Converts a hexadecimal color string to an RGB object. + * @param hex The hexadecimal color string (e.g., "#ff0000" or "#f00"). + * @returns An object with 'r', 'g', and 'b' properties representing the red, green, and blue components of the color, or undefined if the input is invalid. + */ +function hexToRgb(hex: string): + | { + r: number; + g: number; + b: number; + } + | undefined { + if (hex.length !== 4 && hex.length !== 7 && !hex.startsWith("#")) { + return undefined; + } + let r: number; + let g: number; + let b: number; + if (hex.length === 4) { + r = ("0x" + hex[1] + hex[1]) as unknown as number; + g = ("0x" + hex[2] + hex[2]) as unknown as number; + b = ("0x" + hex[3] + hex[3]) as unknown as number; + } else if (hex.length === 7) { + r = ("0x" + hex[1] + hex[2]) as unknown as number; + g = ("0x" + hex[3] + hex[4]) as unknown as number; + b = ("0x" + hex[5] + hex[6]) as unknown as number; + } else { + return undefined; + } + + return { r, g, b }; +} + +/** + * Converts RGB values to a hexadecimal color string. + * @param r The red component (0-255). + * @param g The green component (0-255). + * @param b The blue component (0-255). + * @returns The hexadecimal color string (e.g., "#ff0000" for red). + */ +function rgbToHex(r: number, g: number, b: number): string { + return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); +} + +/** + * Converts a hexadecimal color string to its HSL (Hue, Saturation, Lightness) representation. + * @param hex The hexadecimal color string (e.g., "#ff0000" or "#f00"). + * @returns An object with 'hue', 'sat', 'lgt', and 'string' properties representing the HSL values and an HSL string representation. + */ +export function hexToHSL(hex: string): { + hue: number; + sat: number; + lgt: number; + string: string; +} { + // Convert hex to RGB first + let r: number; + let g: number; + let b: number; + if (hex.length === 4) { + r = ("0x" + hex[1] + hex[1]) as unknown as number; + g = ("0x" + hex[2] + hex[2]) as unknown as number; + b = ("0x" + hex[3] + hex[3]) as unknown as number; + } else if (hex.length === 7) { + r = ("0x" + hex[1] + hex[2]) as unknown as number; + g = ("0x" + hex[3] + hex[4]) as unknown as number; + b = ("0x" + hex[5] + hex[6]) as unknown as number; + } else { + r = 0x00; + g = 0x00; + b = 0x00; + } + // Then to HSL + r /= 255; + g /= 255; + b /= 255; + const cmin = Math.min(r, g, b); + const cmax = Math.max(r, g, b); + const delta = cmax - cmin; + let h = 0; + let s = 0; + let l = 0; + + if (delta === 0) h = 0; + else if (cmax === r) h = ((g - b) / delta) % 6; + else if (cmax === g) h = (b - r) / delta + 2; + else h = (r - g) / delta + 4; + + h = Math.round(h * 60); + + if (h < 0) h += 360; + + l = (cmax + cmin) / 2; + s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + s = +(s * 100).toFixed(1); + l = +(l * 100).toFixed(1); + + return { + hue: h, + sat: s, + lgt: l, + string: "hsl(" + h + "," + s + "%," + l + "%)", + }; +} + +/** + * Checks if a color is considered light based on its hexadecimal representation. + * @param hex The hexadecimal color string. + * @returns True if the color is considered light, false otherwise. + */ +export function isColorLight(hex: string): boolean { + const hsl = hexToHSL(hex); + return hsl.lgt >= 50; +} + +/** + * Checks if a color is considered dark based on its hexadecimal representation. + * @param hex The hexadecimal color string. + * @returns True if the color is considered dark, false otherwise. + */ +export function isColorDark(hex: string): boolean { + const hsl = hexToHSL(hex); + return hsl.lgt < 50; +} + +/** + * Converts an RGB string (e.g., "rgb(255, 0, 0)") to a hexadecimal color string. + * @param rgb The RGB string. + * @returns The equivalent hexadecimal color string. + */ +export function rgbStringtoHex(rgb: string): string | undefined { + const match: RegExpMatchArray | null = rgb.match( + /^rgb\((\d+), \s*(\d+), \s*(\d+)\)$/ + ); + if (match === null) return; + if (match.length < 3) return; + function hexCode(i: string): string { + // Take the last 2 characters and convert + // them to Hexadecimal. + return ("0" + parseInt(i).toString(16)).slice(-2); + } + return ( + "#" + + hexCode(match[1] as string) + + hexCode(match[2] as string) + + hexCode(match[3] as string) + ); +} diff --git a/frontend/src/ts/utils/date-and-time.ts b/frontend/src/ts/utils/date-and-time.ts new file mode 100644 index 000000000000..a5f8cbcd1bfd --- /dev/null +++ b/frontend/src/ts/utils/date-and-time.ts @@ -0,0 +1,164 @@ +import { roundTo2 } from "./numbers"; + +/** + * Returns the current day's timestamp adjusted by the hour offset. + * @param hourOffset The offset in hours. Default is 0. + * @returns The timestamp of the start of the current day adjusted by the hour offset. + */ +export function getCurrentDayTimestamp(hourOffset = 0): number { + const offsetMilis = hourOffset * MILISECONDS_IN_HOUR; + const currentTime = Date.now(); + return getStartOfDayTimestamp(currentTime, offsetMilis); +} + +const MILISECONDS_IN_HOUR = 3600000; +const MILLISECONDS_IN_DAY = 86400000; + +/** + * Returns the timestamp of the start of the day for the given timestamp adjusted by the offset. + * @param timestamp The timestamp for which to get the start of the day. + * @param offsetMilis The offset in milliseconds. Default is 0. + * @returns The timestamp of the start of the day for the given timestamp adjusted by the offset. + */ +export function getStartOfDayTimestamp( + timestamp: number, + offsetMilis = 0 +): number { + return timestamp - ((timestamp - offsetMilis) % MILLISECONDS_IN_DAY); +} + +/** + * Checks if the given timestamp is from yesterday, adjusted by the hour offset. + * @param timestamp The timestamp to check. + * @param hourOffset The offset in hours. Default is 0. + * @returns True if the timestamp is from yesterday, false otherwise. + */ +export function isYesterday(timestamp: number, hourOffset = 0): boolean { + const offsetMilis = hourOffset * MILISECONDS_IN_HOUR; + const yesterday = getStartOfDayTimestamp( + Date.now() - MILLISECONDS_IN_DAY, + offsetMilis + ); + const date = getStartOfDayTimestamp(timestamp, offsetMilis); + + return yesterday === date; +} + +/** + * Checks if the given timestamp is from today, adjusted by the hour offset. + * @param timestamp The timestamp to check. + * @param hourOffset The offset in hours. Default is 0. + * @returns True if the timestamp is from today, false otherwise. + */ +export function isToday(timestamp: number, hourOffset = 0): boolean { + const offsetMilis = hourOffset * MILISECONDS_IN_HOUR; + const today = getStartOfDayTimestamp(Date.now(), offsetMilis); + const date = getStartOfDayTimestamp(timestamp, offsetMilis); + + return today === date; +} + +/** + * Converts seconds to a human-readable string representation of time. + * @param sec The number of seconds to convert. + * @param alwaysShowMinutes Whether to always show minutes, even if the value is 0. Default is false. + * @param alwaysShowHours Whether to always show hours, even if the value is 0. Default is false. + * @param delimiter The delimiter to use between time components. Default is ":". + * @param showSeconds Whether to show seconds. Default is true. + * @param showDays Whether to show days. Default is false. + * @returns A human-readable string representation of the time. + */ +export function secondsToString( + sec: number, + alwaysShowMinutes = false, + alwaysShowHours = false, + delimiter: ":" | "text" = ":", + showSeconds = true, + showDays = false +): string { + sec = Math.abs(sec); + let days = 0; + let hours; + if (showDays) { + days = Math.floor(sec / 86400); + hours = Math.floor((sec % 86400) / 3600); + } else { + hours = Math.floor(sec / 3600); + } + const minutes = Math.floor((sec % 3600) / 60); + const seconds = roundTo2((sec % 3600) % 60); + + let daysString; + let hoursString; + let minutesString; + let secondsString; + + if (showDays) { + days < 10 && delimiter !== "text" + ? (daysString = "0" + days) + : (daysString = days); + } + hours < 10 && delimiter !== "text" + ? (hoursString = "0" + hours) + : (hoursString = hours); + minutes < 10 && delimiter !== "text" + ? (minutesString = "0" + minutes) + : (minutesString = minutes); + seconds < 10 && + (minutes > 0 || hours > 0 || alwaysShowMinutes) && + delimiter !== "text" + ? (secondsString = "0" + seconds) + : (secondsString = seconds); + + let ret = ""; + if (days > 0 && showDays) { + ret += daysString; + if (delimiter === "text") { + if (days === 1) { + ret += " day "; + } else { + ret += " days "; + } + } else { + ret += delimiter; + } + } + if (hours > 0 || alwaysShowHours) { + ret += hoursString; + if (delimiter === "text") { + if (hours === 1) { + ret += " hour "; + } else { + ret += " hours "; + } + } else { + ret += delimiter; + } + } + if (minutes > 0 || hours > 0 || alwaysShowMinutes) { + ret += minutesString; + if (delimiter === "text") { + if (minutes === 1) { + ret += " minute "; + } else { + ret += " minutes "; + } + } else if (showSeconds) { + ret += delimiter; + } + } + if (showSeconds) { + ret += secondsString; + if (delimiter === "text") { + if (seconds === 1) { + ret += " second"; + } else { + ret += " seconds"; + } + } + } + if (hours === 0 && minutes === 0 && !showSeconds && delimiter === "text") { + ret = "less than 1 minute"; + } + return ret.trim(); +} diff --git a/frontend/src/ts/utils/ddr.ts b/frontend/src/ts/utils/ddr.ts new file mode 100644 index 000000000000..4e12ce997e86 --- /dev/null +++ b/frontend/src/ts/utils/ddr.ts @@ -0,0 +1,61 @@ +// code for "generateStep" is from Mirin's "Queue" modfile, +// converted from lua to typescript by Spax +// lineout: https://youtu.be/LnnArS9yrSs +let footTrack = false; +let currFacing = 0; +let facingCount = 0; +let lastLeftStep = 0, + lastRightStep = 3, + leftStepCount = 0, + rightStepCount = 0; +function generateStep(leftRightOverride: boolean): number { + facingCount--; + let randomStep = Math.round(Math.random()); + let stepValue = Math.round(Math.random() * 5 - 0.5); + if (leftRightOverride) { + footTrack = Boolean(Math.round(Math.random())); + if (footTrack) stepValue = 3; + else stepValue = 0; + } else { + //right foot + if (footTrack) { + if (lastLeftStep === randomStep) leftStepCount++; + else leftStepCount = 0; + if (leftStepCount > 1 || (rightStepCount > 0 && leftStepCount > 0)) { + randomStep = 1 - randomStep; + leftStepCount = 0; + } + lastLeftStep = randomStep; + stepValue = randomStep * (currFacing + 1); + //left foot + } else { + if (lastRightStep === randomStep) rightStepCount++; + else rightStepCount = 0; + if (rightStepCount > 1 || (rightStepCount > 0 && leftStepCount > 0)) { + randomStep = 1 - randomStep; + rightStepCount = 0; + } + lastRightStep = randomStep; + stepValue = 3 - randomStep * (currFacing + 1); + } + //alternation + footTrack = !footTrack; + + if (facingCount < 0 && randomStep === 0) { + currFacing = 1 - currFacing; + facingCount = Math.floor(Math.random() * 3) + 3; + } + } + + return stepValue; +} + +export function chart2Word(first: boolean): string { + const arrowArray = ["←", "↓", "↑", "→"]; + let measure = ""; + for (let i = 0; i < 4; i++) { + measure += arrowArray[generateStep(i === 0 && first)]; + } + + return measure; +} diff --git a/frontend/src/ts/utils/format.ts b/frontend/src/ts/utils/format.ts index bd8d971c52ae..02e520585002 100644 --- a/frontend/src/ts/utils/format.ts +++ b/frontend/src/ts/utils/format.ts @@ -1,6 +1,6 @@ -import * as Misc from "./misc"; import Config from "../config"; import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; +import * as Numbers from "../utils/numbers"; export type FormatOptions = { showDecimalPlaces?: boolean; @@ -75,7 +75,7 @@ export class Formatting { ? formatOptions.showDecimalPlaces : this.config.alwaysShowDecimalPlaces ) { - return Misc.roundTo2(value).toFixed(2) + suffix; + return Numbers.roundTo2(value).toFixed(2) + suffix; } return (formatOptions.rounding ?? Math.round)(value).toString() + suffix; } diff --git a/frontend/src/ts/utils/generate.ts b/frontend/src/ts/utils/generate.ts new file mode 100644 index 000000000000..cf39630a1c48 --- /dev/null +++ b/frontend/src/ts/utils/generate.ts @@ -0,0 +1,193 @@ +import { randomIntFromRange } from "./numbers"; +import * as Arrays from "./arrays"; +import * as Strings from "./strings"; + +/** + * Generates a random binary string of length 8. + * @returns The generated binary string. + */ +export function getBinary(): string { + const ret = Math.floor(Math.random() * 256).toString(2); + return ret.padStart(8, "0"); +} + +/** + * Converts a word to Morse code. + * @param word The word to convert to Morse code. + * @returns The Morse code representation of the word. + */ +export function getMorse(word: string): string { + const morseCode: Record = { + a: ".-", + b: "-...", + c: "-.-.", + d: "-..", + e: ".", + f: "..-.", + g: "--.", + h: "....", + i: "..", + j: ".---", + k: "-.-", + l: ".-..", + m: "--", + n: "-.", + o: "---", + p: ".--.", + q: "--.-", + r: ".-.", + s: "...", + t: "-", + u: "..-", + v: "...-", + w: ".--", + x: "-..-", + y: "-.--", + z: "--..", + "0": "-----", + "1": ".----", + "2": "..---", + "3": "...--", + "4": "....-", + "5": ".....", + "6": "-....", + "7": "--...", + "8": "---..", + "9": "----.", + ".": ".-.-.-", + ",": "--..--", + "?": "..--..", + "'": ".----.", + "/": "-..-.", + "(": "-.--.", + ")": "-.--.-", + "&": ".-...", + ":": "---...", + ";": "-.-.-.", + "=": "-...-", + "+": ".-.-.", + "-": "-....-", + _: "..--.-", + '"': ".-..-.", + $: "...-..-", + "!": "-.-.--", + "@": ".--.-.", + }; + + let morseWord = ""; + + const deAccentedWord = Strings.replaceSpecialChars(word); + for (let i = 0; i < deAccentedWord.length; i++) { + const letter = morseCode[deAccentedWord.toLowerCase()[i] as string]; + morseWord += letter !== undefined ? letter + "/" : ""; + } + return morseWord; +} + +/** + * Generates a random gibberish string of lowercase letters. + * @returns The generated gibberish string. + */ +export function getGibberish(): string { + const randLen = randomIntFromRange(1, 7); + let ret = ""; + for (let i = 0; i < randLen; i++) { + ret += String.fromCharCode(97 + randomIntFromRange(0, 25)); + } + return ret; +} + +/** + * Retrieves the words from the HTML elements with IDs "words" and returns them as a string. + * @returns The words extracted from the HTML elements. + */ +export function getWords(): string { + const words = [...document.querySelectorAll("#words .word")] + .map((word) => { + return [...word.querySelectorAll("letter")] + .map((letter) => letter.textContent) + .join(""); + }) + .join(" "); + + return words; +} + +/** + * Generates a random ASCII string of printable characters. + * @returns The generated ASCII string. + */ +export function getASCII(): string { + const randLen = randomIntFromRange(1, 10); + let ret = ""; + for (let i = 0; i < randLen; i++) { + const ran = 33 + randomIntFromRange(0, 93); + ret += String.fromCharCode(ran); + } + return ret; +} + +/** + * Generates a random string of special characters. + * @returns The generated string of special characters. + */ +export function getSpecials(): string { + const randLen = randomIntFromRange(1, 7); + let ret = ""; + const specials = [ + "`", + "~", + "!", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "(", + ")", + "-", + "_", + "=", + "+", + "{", + "}", + "[", + "]", + "'", + '"', + "/", + "\\", + "|", + "?", + ";", + ":", + ">", + "<", + ]; + for (let i = 0; i < randLen; i++) { + ret += Arrays.randomElementFromArray(specials); + } + return ret; +} + +/** + * Generates a random string of digits with a specified length. + * @param len The length of the generated string. + * @returns The generated string of digits. + */ +export function getNumbers(len: number): string { + const randLen = randomIntFromRange(1, len); + let ret = ""; + for (let i = 0; i < randLen; i++) { + let randomNum; + if (i === 0) { + randomNum = randomIntFromRange(1, 9); + } else { + randomNum = randomIntFromRange(0, 9); + } + ret += randomNum.toString(); + } + return ret; +} diff --git a/frontend/src/ts/utils/json-data.ts b/frontend/src/ts/utils/json-data.ts new file mode 100644 index 000000000000..66b5a4bcb6dd --- /dev/null +++ b/frontend/src/ts/utils/json-data.ts @@ -0,0 +1,369 @@ +import { hexToHSL } from "./colors"; + +/** + * Fetches JSON data from the specified URL using the fetch API. + * @param url - The URL to fetch the JSON data from. + * @returns A promise that resolves to the parsed JSON data. + * @throws {Error} If the URL is not provided or if the fetch request fails. + */ +async function fetchJson(url: string): Promise { + try { + if (!url) throw new Error("No URL"); + const res = await fetch(url); + if (res.ok) { + return await res.json(); + } else { + throw new Error(`${res.status} ${res.statusText}`); + } + } catch (e) { + console.error("Error fetching JSON: " + url, e); + throw e; + } +} + +/** + * Memoizes an asynchronous function. + * @template P The type of the function's parameters. + * @template T The type of the function. + * @param {T} fn The asynchronous function to memoize. + * @param {(...args: Parameters) => P} [getKey] Optional function to generate cache keys based on function arguments. + * @returns {T} The memoized function. + */ +export function memoizeAsync(...args: P[]) => Promise>( + fn: T, + getKey?: (...args: Parameters) => P +): T { + const cache = new Map>>(); + + return (async (...args: Parameters): Promise> => { + const key = getKey ? getKey.apply(args) : (args[0] as P); + + if (cache.has(key)) { + const ret = await cache.get(key); + if (ret !== undefined) { + return ret as ReturnType; + } + } + + // eslint-disable-next-line prefer-spread + const result = fn.apply(null, args) as Promise>; + cache.set(key, result); + + return result; + }) as T; +} + +/** + * Memoizes the fetchJson function to cache the results of fetch requests. + * @param url - The URL used to fetch JSON data. + * @returns A promise that resolves to the cached JSON data. + */ +export const cachedFetchJson = memoizeAsync( + fetchJson +); + +/** + * Fetches the layouts list from the server. + * @returns A promise that resolves to the layouts list. + */ +export async function getLayoutsList(): Promise { + try { + const layoutsList = await cachedFetchJson( + "/layouts/_list.json" + ); + return layoutsList; + } catch (e) { + throw new Error("Layouts JSON fetch failed"); + } +} + +/** + * Fetches a layout by name from the server. + * @param layoutName The name of the layout to fetch. + * @returns A promise that resolves to the layout object. + * @throws {Error} If the layout list or layout doesn't exist. + */ +export async function getLayout( + layoutName: string +): Promise { + const layouts = await getLayoutsList(); + const layout = layouts[layoutName]; + if (layout === undefined) { + throw new Error(`Layout ${layoutName} is undefined`); + } + return layout; +} + +let themesList: MonkeyTypes.Theme[] | undefined; + +/** + * Fetches the list of themes from the server, sorting them alphabetically by name. + * If the list has already been fetched, returns the cached list. + * @returns A promise that resolves to the sorted list of themes. + */ +export async function getThemesList(): Promise { + if (!themesList) { + let themes = await cachedFetchJson( + "/themes/_list.json" + ); + + themes = themes.sort(function (a: MonkeyTypes.Theme, b: MonkeyTypes.Theme) { + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); + if (nameA < nameB) return -1; + if (nameA > nameB) return 1; + return 0; + }); + themesList = themes; + return themesList; + } else { + return themesList; + } +} + +let sortedThemesList: MonkeyTypes.Theme[] | undefined; + +/** + * Fetches the sorted list of themes from the server. + * @returns A promise that resolves to the sorted list of themes. + */ +export async function getSortedThemesList(): Promise { + if (!sortedThemesList) { + if (!themesList) { + await getThemesList(); + } + if (!themesList) { + throw new Error("Themes list is undefined"); + } + let sorted = [...themesList]; + sorted = sorted.sort((a, b) => { + const b1 = hexToHSL(a.bgColor); + const b2 = hexToHSL(b.bgColor); + return b2.lgt - b1.lgt; + }); + sortedThemesList = sorted; + return sortedThemesList; + } else { + return sortedThemesList; + } +} + +/** + * Fetches the list of languages from the server. + * @returns A promise that resolves to the list of languages. + */ +export async function getLanguageList(): Promise { + try { + const languageList = await cachedFetchJson( + "/languages/_list.json" + ); + return languageList; + } catch (e) { + throw new Error("Language list JSON fetch failed"); + } +} + +/** + * Fetches the list of language groups from the server. + * @returns A promise that resolves to the list of language groups. + */ +export async function getLanguageGroups(): Promise< + MonkeyTypes.LanguageGroup[] +> { + try { + const languageGroupList = await cachedFetchJson< + MonkeyTypes.LanguageGroup[] + >("/languages/_groups.json"); + return languageGroupList; + } catch (e) { + throw new Error("Language groups JSON fetch failed"); + } +} + +let currentLanguage: MonkeyTypes.LanguageObject; + +/** + * Fetches the language object for a given language from the server. + * @param lang The language code. + * @returns A promise that resolves to the language object. + */ +export async function getLanguage( + lang: string +): Promise { + // try { + if (currentLanguage === undefined || currentLanguage.name !== lang) { + currentLanguage = await cachedFetchJson( + `/languages/${lang}.json` + ); + } + return currentLanguage; +} + +/** + * Fetches the current language object. + * @param languageName The name of the language. + * @returns A promise that resolves to the current language object. + */ +export async function getCurrentLanguage( + languageName: string +): Promise { + return await getLanguage(languageName); +} + +/** + * Fetches the language group for a given language. + * @param language The language code. + * @returns A promise that resolves to the language group. + */ +export async function getCurrentGroup( + language: string +): Promise { + let retgroup: MonkeyTypes.LanguageGroup | undefined; + const groups = await getLanguageGroups(); + groups.forEach((group) => { + if (retgroup === undefined) { + if (group.languages.includes(language)) { + retgroup = group; + } + } + }); + return retgroup; +} + +let funboxList: MonkeyTypes.FunboxMetadata[] | undefined; + +/** + * Fetches the list of funbox metadata from the server. + * @returns A promise that resolves to the list of funbox metadata. + */ +export async function getFunboxList(): Promise { + if (!funboxList) { + let list = await cachedFetchJson( + "/funbox/_list.json" + ); + list = list.sort(function ( + a: MonkeyTypes.FunboxMetadata, + b: MonkeyTypes.FunboxMetadata + ) { + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); + if (nameA < nameB) return -1; + if (nameA > nameB) return 1; + return 0; + }); + funboxList = list; + return funboxList; + } else { + return funboxList; + } +} + +/** + * Fetches the funbox metadata for a given funbox from the server. + * @param funbox The name of the funbox. + * @returns A promise that resolves to the funbox metadata. + */ +export async function getFunbox( + funbox: string +): Promise { + const list: MonkeyTypes.FunboxMetadata[] = await getFunboxList(); + return list.find(function (element) { + return element.name === funbox; + }); +} + +let fontsList: MonkeyTypes.FontObject[] | undefined; + +/** + * Fetches the list of font objects from the server. + * @returns A promise that resolves to the list of font objects. + */ +export async function getFontsList(): Promise { + if (!fontsList) { + let list = await cachedFetchJson( + "/fonts/_list.json" + ); + list = list.sort(function ( + a: MonkeyTypes.FontObject, + b: MonkeyTypes.FontObject + ) { + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); + if (nameA < nameB) return -1; + if (nameA > nameB) return 1; + return 0; + }); + fontsList = list; + return fontsList; + } else { + return fontsList; + } +} + +/** + * Fetches the list of challenges from the server. + * @returns A promise that resolves to the list of challenges. + */ +export async function getChallengeList(): Promise { + try { + const data = await cachedFetchJson( + "/challenges/_list.json" + ); + return data; + } catch (e) { + throw new Error("Challenge list JSON fetch failed"); + } +} + +/** + * Fetches the list of supporters from the server. + * @returns A promise that resolves to the list of supporters. + */ +export async function getSupportersList(): Promise { + try { + const data = await cachedFetchJson("/about/supporters.json"); + return data; + } catch (e) { + throw new Error("Supporters list JSON fetch failed"); + } +} + +/** + * Fetches the list of contributors from the server. + * @returns A promise that resolves to the list of contributors. + */ +export async function getContributorsList(): Promise { + try { + const data = await cachedFetchJson("/about/contributors.json"); + return data; + } catch (e) { + throw new Error("Contributors list JSON fetch failed"); + } +} + +/** + * Fetches the latest release name from GitHub. + * @returns A promise that resolves to the latest release name. + */ +export async function getLatestReleaseFromGitHub(): Promise { + type releaseType = { name: string }; + const releases = await cachedFetchJson( + "https://api.github.com/repos/monkeytypegame/monkeytype/releases?per_page=1" + ); + if (releases[0] === undefined || releases[0].name === undefined) { + throw new Error("No release found"); + } + return releases[0].name; +} + +/** + * Fetches the list of releases from GitHub. + * @returns A promise that resolves to the list of releases. + */ +export async function getReleasesFromGitHub(): Promise< + MonkeyTypes.GithubRelease[] +> { + return cachedFetchJson( + "https://api.github.com/repos/monkeytypegame/monkeytype/releases?per_page=5" + ); +} diff --git a/frontend/src/ts/utils/levels.ts b/frontend/src/ts/utils/levels.ts new file mode 100644 index 000000000000..4d2897c8e41b --- /dev/null +++ b/frontend/src/ts/utils/levels.ts @@ -0,0 +1,17 @@ +/** + * Calculates the level based on the XP. + * @param xp The experience points. + * @returns The calculated level. + */ +export function getLevel(xp: number): number { + return (1 / 98) * (-151 + Math.sqrt(392 * xp + 22801)) + 1; +} + +/** + * Calculates the experience points required for a given level. + * @param level The level. + * @returns The experience points required for the level. + */ +export function getXpForLevel(level: number): number { + return 49 * (level - 1) + 100; +} diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 37102469bd8c..dcd294dcf9a2 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -1,487 +1,7 @@ import * as Loader from "../elements/loader"; -import { normal as normalBlend } from "color-blend"; import { envConfig } from "../constants/env-config"; - -//todo split this file into smaller util files (grouped by functionality) - -async function fetchJson(url: string): Promise { - try { - if (!url) throw new Error("No URL"); - const res = await fetch(url); - if (res.ok) { - return await res.json(); - } else { - throw new Error(`${res.status} ${res.statusText}`); - } - } catch (e) { - console.error("Error fetching JSON: " + url, e); - throw e; - } -} - -export const cachedFetchJson = memoizeAsync( - fetchJson -); - -export async function getLayoutsList(): Promise { - try { - const layoutsList = await cachedFetchJson( - "/layouts/_list.json" - ); - return layoutsList; - } catch (e) { - throw new Error("Layouts JSON fetch failed"); - } -} - -/** - * @throws {Error} If layout list or layout doesnt exist. - */ -export async function getLayout( - layoutName: string -): Promise { - const layouts = await getLayoutsList(); - const layout = layouts[layoutName]; - if (layout === undefined) { - throw new Error(`Layout ${layoutName} is undefined`); - } - return layout; -} - -let themesList: MonkeyTypes.Theme[] | undefined; -export async function getThemesList(): Promise { - if (!themesList) { - let themes = await cachedFetchJson( - "/themes/_list.json" - ); - - themes = themes.sort(function (a: MonkeyTypes.Theme, b: MonkeyTypes.Theme) { - const nameA = a.name.toLowerCase(); - const nameB = b.name.toLowerCase(); - if (nameA < nameB) return -1; - if (nameA > nameB) return 1; - return 0; - }); - themesList = themes; - return themesList; - } else { - return themesList; - } -} - -let sortedThemesList: MonkeyTypes.Theme[] | undefined; -export async function getSortedThemesList(): Promise { - if (!sortedThemesList) { - if (!themesList) { - await getThemesList(); - } - if (!themesList) { - throw new Error("Themes list is undefined"); - } - let sorted = [...themesList]; - sorted = sorted.sort((a, b) => { - const b1 = hexToHSL(a.bgColor); - const b2 = hexToHSL(b.bgColor); - return b2.lgt - b1.lgt; - }); - sortedThemesList = sorted; - return sortedThemesList; - } else { - return sortedThemesList; - } -} - -export async function getLanguageList(): Promise { - try { - const languageList = await cachedFetchJson( - "/languages/_list.json" - ); - return languageList; - } catch (e) { - throw new Error("Language list JSON fetch failed"); - } -} - -export async function getLanguageGroups(): Promise< - MonkeyTypes.LanguageGroup[] -> { - try { - const languageGroupList = await cachedFetchJson< - MonkeyTypes.LanguageGroup[] - >("/languages/_groups.json"); - return languageGroupList; - } catch (e) { - throw new Error("Language groups JSON fetch failed"); - } -} - -let currentLanguage: MonkeyTypes.LanguageObject; -export async function getLanguage( - lang: string -): Promise { - // try { - if (currentLanguage === undefined || currentLanguage.name !== lang) { - currentLanguage = await cachedFetchJson( - `/languages/${lang}.json` - ); - } - return currentLanguage; - // } catch (e) { - // console.error(`error getting language`); - // console.error(e); - // currentLanguage = await cachedFetchJson( - // `/language/english.json` - // ); - // return currentLanguage; - // } -} - -export async function getCurrentLanguage( - languageName: string -): Promise { - return await getLanguage(languageName); -} - -export async function findCurrentGroup( - language: string -): Promise { - let retgroup: MonkeyTypes.LanguageGroup | undefined; - const groups = await getLanguageGroups(); - groups.forEach((group) => { - if (retgroup === undefined) { - if (group.languages.includes(language)) { - retgroup = group; - } - } - }); - return retgroup; -} - -let funboxList: MonkeyTypes.FunboxMetadata[] | undefined; -export async function getFunboxList(): Promise { - if (!funboxList) { - let list = await cachedFetchJson( - "/funbox/_list.json" - ); - list = list.sort(function ( - a: MonkeyTypes.FunboxMetadata, - b: MonkeyTypes.FunboxMetadata - ) { - const nameA = a.name.toLowerCase(); - const nameB = b.name.toLowerCase(); - if (nameA < nameB) return -1; - if (nameA > nameB) return 1; - return 0; - }); - funboxList = list; - return funboxList; - } else { - return funboxList; - } -} - -export async function getFunbox( - funbox: string -): Promise { - const list: MonkeyTypes.FunboxMetadata[] = await getFunboxList(); - return list.find(function (element) { - return element.name === funbox; - }); -} - -let fontsList: MonkeyTypes.FontObject[] | undefined; -export async function getFontsList(): Promise { - if (!fontsList) { - let list = await cachedFetchJson( - "/fonts/_list.json" - ); - list = list.sort(function ( - a: MonkeyTypes.FontObject, - b: MonkeyTypes.FontObject - ) { - const nameA = a.name.toLowerCase(); - const nameB = b.name.toLowerCase(); - if (nameA < nameB) return -1; - if (nameA > nameB) return 1; - return 0; - }); - fontsList = list; - return fontsList; - } else { - return fontsList; - } -} - -export async function getChallengeList(): Promise { - try { - const data = await cachedFetchJson( - "/challenges/_list.json" - ); - return data; - } catch (e) { - throw new Error("Challenge list JSON fetch failed"); - } -} - -export async function getSupportersList(): Promise { - try { - const data = await cachedFetchJson("/about/supporters.json"); - return data; - } catch (e) { - throw new Error("Supporters list JSON fetch failed"); - } -} - -export async function getContributorsList(): Promise { - try { - const data = await cachedFetchJson("/about/contributors.json"); - return data; - } catch (e) { - throw new Error("Contributors list JSON fetch failed"); - } -} - -export function blendTwoHexColors( - color1: string, - color2: string, - opacity: number -): string { - const rgb1 = hexToRgb(color1); - const rgb2 = hexToRgb(color2); - - if (rgb1 && rgb2) { - const rgba1 = { - r: rgb1.r, - g: rgb1.g, - b: rgb1.b, - a: 1, - }; - const rgba2 = { - r: rgb2.r, - g: rgb2.g, - b: rgb2.b, - a: opacity, - }; - const blended = normalBlend(rgba1, rgba2); - return rgbToHex(blended.r, blended.g, blended.b); - } else { - return "#000000"; - } -} - -function hexToRgb(hex: string): - | { - r: number; - g: number; - b: number; - } - | undefined { - if (hex.length !== 4 && hex.length !== 7 && !hex.startsWith("#")) { - return undefined; - } - let r: number; - let g: number; - let b: number; - if (hex.length === 4) { - r = ("0x" + hex[1] + hex[1]) as unknown as number; - g = ("0x" + hex[2] + hex[2]) as unknown as number; - b = ("0x" + hex[3] + hex[3]) as unknown as number; - } else if (hex.length === 7) { - r = ("0x" + hex[1] + hex[2]) as unknown as number; - g = ("0x" + hex[3] + hex[4]) as unknown as number; - b = ("0x" + hex[5] + hex[6]) as unknown as number; - } else { - return undefined; - } - - return { r, g, b }; -} - -function rgbToHex(r: number, g: number, b: number): string { - return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); -} - -function hexToHSL(hex: string): { - hue: number; - sat: number; - lgt: number; - string: string; -} { - // Convert hex to RGB first - let r: number; - let g: number; - let b: number; - if (hex.length === 4) { - r = ("0x" + hex[1] + hex[1]) as unknown as number; - g = ("0x" + hex[2] + hex[2]) as unknown as number; - b = ("0x" + hex[3] + hex[3]) as unknown as number; - } else if (hex.length === 7) { - r = ("0x" + hex[1] + hex[2]) as unknown as number; - g = ("0x" + hex[3] + hex[4]) as unknown as number; - b = ("0x" + hex[5] + hex[6]) as unknown as number; - } else { - r = 0x00; - g = 0x00; - b = 0x00; - } - // Then to HSL - r /= 255; - g /= 255; - b /= 255; - const cmin = Math.min(r, g, b); - const cmax = Math.max(r, g, b); - const delta = cmax - cmin; - let h = 0; - let s = 0; - let l = 0; - - if (delta === 0) h = 0; - else if (cmax === r) h = ((g - b) / delta) % 6; - else if (cmax === g) h = (b - r) / delta + 2; - else h = (r - g) / delta + 4; - - h = Math.round(h * 60); - - if (h < 0) h += 360; - - l = (cmax + cmin) / 2; - s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); - s = +(s * 100).toFixed(1); - l = +(l * 100).toFixed(1); - - return { - hue: h, - sat: s, - lgt: l, - string: "hsl(" + h + "," + s + "%," + l + "%)", - }; -} - -export function isColorLight(hex: string): boolean { - const hsl = hexToHSL(hex); - return hsl.lgt >= 50; -} - -export function isColorDark(hex: string): boolean { - const hsl = hexToHSL(hex); - return hsl.lgt < 50; -} - -export function smooth( - arr: number[], - windowSize: number, - getter = (value: number): number => value -): number[] { - const get = getter; - const result = []; - - for (let i = 0; i < arr.length; i += 1) { - const leftOffeset = i - windowSize; - const from = leftOffeset >= 0 ? leftOffeset : 0; - const to = i + windowSize + 1; - - let count = 0; - let sum = 0; - for (let j = from; j < to && j < arr.length; j += 1) { - sum += get(arr[j] as number); - count += 1; - } - - result[i] = sum / count; - } - - return result; -} - -export function stdDev(array: number[]): number { - try { - const n = array.length; - const mean = array.reduce((a, b) => a + b) / n; - return Math.sqrt( - array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n - ); - } catch (e) { - return 0; - } -} - -export function mean(array: number[]): number { - try { - return ( - array.reduce((previous, current) => (current += previous)) / array.length - ); - } catch (e) { - return 0; - } -} - -//https://www.w3resource.com/javascript-exercises/fundamental/javascript-fundamental-exercise-88.php -export function median(arr: number[]): number { - try { - const mid = Math.floor(arr.length / 2), - nums = [...arr].sort((a, b) => a - b); - return arr.length % 2 !== 0 - ? (nums[mid] as number) - : ((nums[mid - 1] as number) + (nums[mid] as number)) / 2; - } catch (e) { - return 0; - } -} - -export async function getLatestReleaseFromGitHub(): Promise { - type releaseType = { name: string }; - const releases = await cachedFetchJson( - "https://api.github.com/repos/monkeytypegame/monkeytype/releases?per_page=1" - ); - if (releases[0] === undefined || releases[0].name === undefined) { - throw new Error("No release found"); - } - return releases[0].name; -} - -export async function getReleasesFromGitHub(): Promise< - MonkeyTypes.GithubRelease[] -> { - return cachedFetchJson( - "https://api.github.com/repos/monkeytypegame/monkeytype/releases?per_page=5" - ); -} - -// function getPatreonNames() { -// let namesel = $(".pageAbout .section .supporters"); -// firebase -// .functions() -// .httpsCallable("getPatreons")() -// .then((data) => { -// let names = data.data; -// names.forEach((name) => { -// namesel.append(`
${name}
`); -// }); -// }); -// } - -export function getLastChar(word: string): string { - try { - return word.charAt(word.length - 1); - } catch { - return ""; - } -} - -export function capitalizeFirstLetterOfEachWord(str: string): string { - return str - .split(/ +/) - .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) - .join(" "); -} - -export function capitalizeFirstLetter(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); -} - -export function isASCIILetter(c: string): boolean { - return c.length === 1 && /[a-z]/i.test(c); -} +import { lastElementFromArray } from "./arrays"; +import * as JSONData from "./json-data"; export function kogasa(cov: number): number { return ( @@ -496,180 +16,6 @@ export function whorf(speed: number, wordlen: number): number { ); } -export function roundTo2(num: number): number { - return Math.round((num + Number.EPSILON) * 100) / 100; -} - -export function findLineByLeastSquares( - values_y: number[] -): [[number, number], [number, number]] | null { - let sum_x = 0; - let sum_y = 0; - let sum_xy = 0; - let sum_xx = 0; - let count = 0; - - /* - * We'll use those letiables for faster read/write access. - */ - let x = 0; - let y = 0; - const values_length = values_y.length; - - /* - * Nothing to do. - */ - if (values_length === 0) { - return null; - } - - /* - * Calculate the sum for each of the parts necessary. - */ - for (let v = 0; v < values_length; v++) { - x = v + 1; - y = values_y[v] as number; - sum_x += x; - sum_y += y; - sum_xx += x * x; - sum_xy += x * y; - count++; - } - - /* - * Calculate m and b for the formular: - * y = x * m + b - */ - const m = (count * sum_xy - sum_x * sum_y) / (count * sum_xx - sum_x * sum_x); - const b = sum_y / count - (m * sum_x) / count; - - const returnpoint1 = [1, 1 * m + b] as [number, number]; - const returnpoint2 = [values_length, values_length * m + b] as [ - number, - number - ]; - return [returnpoint1, returnpoint2]; -} - -export function getGibberish(): string { - const randLen = randomIntFromRange(1, 7); - let ret = ""; - for (let i = 0; i < randLen; i++) { - ret += String.fromCharCode(97 + randomIntFromRange(0, 25)); - } - return ret; -} - -export function secondsToString( - sec: number, - alwaysShowMinutes = false, - alwaysShowHours = false, - delimiter: ":" | "text" = ":", - showSeconds = true, - showDays = false -): string { - sec = Math.abs(sec); - let days = 0; - let hours; - if (showDays) { - days = Math.floor(sec / 86400); - hours = Math.floor((sec % 86400) / 3600); - } else { - hours = Math.floor(sec / 3600); - } - const minutes = Math.floor((sec % 3600) / 60); - const seconds = roundTo2((sec % 3600) % 60); - - let daysString; - let hoursString; - let minutesString; - let secondsString; - - if (showDays) { - days < 10 && delimiter !== "text" - ? (daysString = "0" + days) - : (daysString = days); - } - hours < 10 && delimiter !== "text" - ? (hoursString = "0" + hours) - : (hoursString = hours); - minutes < 10 && delimiter !== "text" - ? (minutesString = "0" + minutes) - : (minutesString = minutes); - seconds < 10 && - (minutes > 0 || hours > 0 || alwaysShowMinutes) && - delimiter !== "text" - ? (secondsString = "0" + seconds) - : (secondsString = seconds); - - let ret = ""; - if (days > 0 && showDays) { - ret += daysString; - if (delimiter === "text") { - if (days === 1) { - ret += " day "; - } else { - ret += " days "; - } - } else { - ret += delimiter; - } - } - if (hours > 0 || alwaysShowHours) { - ret += hoursString; - if (delimiter === "text") { - if (hours === 1) { - ret += " hour "; - } else { - ret += " hours "; - } - } else { - ret += delimiter; - } - } - if (minutes > 0 || hours > 0 || alwaysShowMinutes) { - ret += minutesString; - if (delimiter === "text") { - if (minutes === 1) { - ret += " minute "; - } else { - ret += " minutes "; - } - } else if (showSeconds) { - ret += delimiter; - } - } - if (showSeconds) { - ret += secondsString; - if (delimiter === "text") { - if (seconds === 1) { - ret += " second"; - } else { - ret += " seconds"; - } - } - } - if (hours === 0 && minutes === 0 && !showSeconds && delimiter === "text") { - ret = "less than 1 minute"; - } - return ret.trim(); -} - -export function getNumbers(len: number): string { - const randLen = randomIntFromRange(1, len); - let ret = ""; - for (let i = 0; i < randLen; i++) { - let randomNum; - if (i === 0) { - randomNum = randomIntFromRange(1, 9); - } else { - randomNum = randomIntFromRange(0, 9); - } - ret += randomNum.toString(); - } - return ret; -} - //convert numbers to arabic-indic export function convertNumberToArabic(numString: string): string { const arabicIndic = "٠١٢٣٤٥٦٧٨٩"; @@ -689,119 +35,6 @@ export function convertNumberToNepali(numString: string): string { return ret; } -export function getSpecials(): string { - const randLen = randomIntFromRange(1, 7); - let ret = ""; - const specials = [ - "`", - "~", - "!", - "@", - "#", - "$", - "%", - "^", - "&", - "*", - "(", - ")", - "-", - "_", - "=", - "+", - "{", - "}", - "[", - "]", - "'", - '"', - "/", - "\\", - "|", - "?", - ";", - ":", - ">", - "<", - ]; - for (let i = 0; i < randLen; i++) { - ret += randomElementFromArray(specials); - } - return ret; -} - -export function getASCII(): string { - const randLen = randomIntFromRange(1, 10); - let ret = ""; - for (let i = 0; i < randLen; i++) { - const ran = 33 + randomIntFromRange(0, 93); - ret += String.fromCharCode(ran); - } - return ret; -} - -// code for "generateStep" is from Mirin's "Queue" modfile, -// converted from lua to typescript by Spax -// lineout: https://youtu.be/LnnArS9yrSs -let footTrack = false; -let currFacing = 0; -let facingCount = 0; -let lastLeftStep = 0, - lastRightStep = 3, - leftStepCount = 0, - rightStepCount = 0; -function generateStep(leftRightOverride: boolean): number { - facingCount--; - let randomStep = Math.round(Math.random()); - let stepValue = Math.round(Math.random() * 5 - 0.5); - if (leftRightOverride) { - footTrack = Boolean(Math.round(Math.random())); - if (footTrack) stepValue = 3; - else stepValue = 0; - } else { - //right foot - if (footTrack) { - if (lastLeftStep === randomStep) leftStepCount++; - else leftStepCount = 0; - if (leftStepCount > 1 || (rightStepCount > 0 && leftStepCount > 0)) { - randomStep = 1 - randomStep; - leftStepCount = 0; - } - lastLeftStep = randomStep; - stepValue = randomStep * (currFacing + 1); - //left foot - } else { - if (lastRightStep === randomStep) rightStepCount++; - else rightStepCount = 0; - if (rightStepCount > 1 || (rightStepCount > 0 && leftStepCount > 0)) { - randomStep = 1 - randomStep; - rightStepCount = 0; - } - lastRightStep = randomStep; - stepValue = 3 - randomStep * (currFacing + 1); - } - //alternation - footTrack = !footTrack; - - if (facingCount < 0 && randomStep === 0) { - currFacing = 1 - currFacing; - facingCount = Math.floor(Math.random() * 3) + 3; - } - } - - return stepValue; -} - -export function chart2Word(first: boolean): string { - const arrowArray = ["←", "↓", "↑", "→"]; - let measure = ""; - for (let i = 0; i < 4; i++) { - measure += arrowArray[generateStep(i === 0 && first)]; - } - - return measure; -} - export function findGetParameter( parameterName: string, getOverride?: string @@ -913,26 +146,6 @@ export function toggleFullscreen(): void { } } -export function getWords(): string { - const words = [...document.querySelectorAll("#words .word")] - .map((word) => { - return [...word.querySelectorAll("letter")] - .map((letter) => letter.textContent) - .join(""); - }) - .join(" "); - - return words; -} - -//credit: https://www.w3resource.com/javascript-exercises/javascript-string-exercise-32.php -export function remove_non_ascii(str: string): string { - if (str === null || str === "") return ""; - else str = str.toString(); - - return str.replace(/[^\x20-\x7E]/g, ""); -} - export function escapeRegExp(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } @@ -1045,12 +258,6 @@ export function clearTimeouts(timeouts: (number | NodeJS.Timeout)[]): void { }); } -//https://stackoverflow.com/questions/1431094/how-do-i-replace-a-character-at-a-particular-index-in-javascript -export function setCharAt(str: string, index: number, chr: string): string { - if (index > str.length - 1) return str; - return str.substring(0, index) + chr + str.substring(index + 1); -} - //https://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expr export function regexIndexOf( string: string, @@ -1061,26 +268,6 @@ export function regexIndexOf( return indexOf >= 0 ? indexOf + (startpos || 0) : indexOf; } -export function convertRGBtoHEX(rgb: string): string | undefined { - const match: RegExpMatchArray | null = rgb.match( - /^rgb\((\d+), \s*(\d+), \s*(\d+)\)$/ - ); - if (match === null) return; - if (match.length < 3) return; - function hexCode(i: string): string { - // Take the last 2 characters and convert - // them to Hexadecimal. - - return ("0" + parseInt(i).toString(16)).slice(-2); - } - return ( - "#" + - hexCode(match[1] as string) + - hexCode(match[2] as string) + - hexCode(match[3] as string) - ); -} - type LastIndex = { lastIndexOfRegex(regex: RegExp): number; } & string; @@ -1089,14 +276,39 @@ type LastIndex = { regex: RegExp ): number { const match = this.match(regex); - return match ? this.lastIndexOf(match[match.length - 1] as string) : -1; + return match ? this.lastIndexOf(lastElementFromArray(match) as string) : -1; }; export const trailingComposeChars = /[\u02B0-\u02FF`´^¨~]+$|⎄.*$/; -//https://stackoverflow.com/questions/36532307/rem-px-in-javascript -export function convertRemToPixels(rem: number): number { - return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); +export async function getDiscordAvatarUrl( + discordId?: string, + discordAvatar?: string, + discordAvatarSize = 32 +): Promise { + if ( + discordId === undefined || + discordId === "" || + discordAvatar === undefined || + discordAvatar === "" + ) { + return null; + } + // An invalid request to this URL will return a 404. + try { + const avatarUrl = `https://cdn.discordapp.com/avatars/${discordId}/${discordAvatar}.png?size=${discordAvatarSize}`; + + const response = await fetch(avatarUrl, { + method: "HEAD", + }); + if (!response.ok) { + return null; + } + + return avatarUrl; + } catch (error) {} + + return null; } export async function swapElements( @@ -1267,50 +479,6 @@ export async function downloadResultsCSV( Loader.hide(); } -/** - * Gets an integer between min and max, both are inclusive. - * @param min - * @param max - * @returns Random integer betwen min and max. - */ -export function randomIntFromRange(min: number, max: number): number { - const minNorm = Math.ceil(min); - const maxNorm = Math.floor(max); - return Math.floor(Math.random() * (maxNorm - minNorm + 1) + minNorm); -} - -/** - * Shuffle an array of elements using the Fisher–Yates algorithm. - * This function mutates the input array. - * @param elements - */ -export function shuffle(elements: T[]): void { - for (let i = elements.length - 1; i > 0; --i) { - const j = randomIntFromRange(0, i); - const temp = elements[j]; - elements[j] = elements[i] as T; - elements[i] = temp as T; - } -} - -export function randomElementFromArray(array: T[]): T { - return array[randomIntFromRange(0, array.length - 1)] as T; -} - -export function nthElementFromArray( - array: T[], - index: number -): T | undefined { - index = index < 0 ? array.length + index : index; - return array[index]; -} - -export function randomElementFromObject( - object: T -): T[keyof T] { - return randomElementFromArray(Object.values(object)); -} - export function createErrorMessage(error: unknown, message: string): string { if (error instanceof Error) { return `${message}: ${error.message}`; @@ -1352,44 +520,6 @@ export function isAnyPopupVisible(): boolean { return popupVisible; } -export async function getDiscordAvatarUrl( - discordId?: string, - discordAvatar?: string, - discordAvatarSize = 32 -): Promise { - if ( - discordId === undefined || - discordId === "" || - discordAvatar === undefined || - discordAvatar === "" - ) { - return null; - } - // An invalid request to this URL will return a 404. - try { - const avatarUrl = `https://cdn.discordapp.com/avatars/${discordId}/${discordAvatar}.png?size=${discordAvatarSize}`; - - const response = await fetch(avatarUrl, { - method: "HEAD", - }); - if (!response.ok) { - return null; - } - - return avatarUrl; - } catch (error) {} - - return null; -} - -export function getLevel(xp: number): number { - return (1 / 98) * (-151 + Math.sqrt(392 * xp + 22801)) + 1; -} - -export function getXpForLevel(level: number): number { - return 49 * (level - 1) + 100; -} - export async function promiseAnimation( el: JQuery, animation: Record, @@ -1401,45 +531,10 @@ export async function promiseAnimation( }); } -//abbreviateNumber -export function abbreviateNumber(num: number, decimalPoints = 1): string { - if (num < 1000) { - return num.toString(); - } - - const exp = Math.floor(Math.log(num) / Math.log(1000)); - const pre = "kmbtqQsSond".charAt(exp - 1); - return (num / Math.pow(1000, exp)).toFixed(decimalPoints) + pre; -} - export async function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -export function memoizeAsync(...args: P[]) => Promise>( - fn: T, - getKey?: (...args: Parameters) => P -): T { - const cache = new Map>>(); - - return (async (...args: Parameters): Promise> => { - const key = getKey ? getKey.apply(args) : (args[0] as P); - - if (cache.has(key)) { - const ret = await cache.get(key); - if (ret !== undefined) { - return ret as ReturnType; - } - } - - // eslint-disable-next-line prefer-spread - const result = fn.apply(null, args) as Promise>; - cache.set(key, result); - - return result; - }) as T; -} - export class Section { public title: string; public author: string; @@ -1460,14 +555,6 @@ export function isPasswordStrong(password: string): boolean { return hasCapital && hasNumber && hasSpecial && isLong && isShort; } -export function areUnsortedArraysEqual(a: unknown[], b: unknown[]): boolean { - return a.length === b.length && a.every((v) => b.includes(v)); -} - -export function areSortedArraysEqual(a: unknown[], b: unknown[]): boolean { - return a.length === b.length && a.every((v, i) => v === b[i]); -} - export function intersect(a: T[], b: T[], removeDuplicates = false): T[] { let t; if (b.length > a.length) (t = b), (b = a), (a = t); // indexOf to loop over shorter @@ -1483,13 +570,6 @@ export function htmlToText(html: string): string { return (el.textContent as string) || el.innerText || ""; } -export function camelCaseToWords(str: string): string { - return str - .replace(/([A-Z])/g, " $1") - .trim() - .toLowerCase(); -} - export function loadCSS(href: string, prepend = false): void { const link = document.createElement("link"); link.type = "text/css"; @@ -1513,11 +593,6 @@ export function isDevEnvironment(): boolean { return envConfig.isDevelopment; } -export function getBinary(): string { - const ret = Math.floor(Math.random() * 256).toString(2); - return ret.padStart(8, "0"); -} - export function dreymarIndex(arrayLength: number): number { const n = arrayLength; const g = 0.5772156649; @@ -1531,47 +606,12 @@ export function dreymarIndex(arrayLength: number): number { export async function checkIfLanguageSupportsZipf( language: string ): Promise<"yes" | "no" | "unknown"> { - const lang = await getLanguage(language); + const lang = await JSONData.getLanguage(language); if (lang.orderedByFrequency === true) return "yes"; if (lang.orderedByFrequency === false) return "no"; return "unknown"; } -export function getCurrentDayTimestamp(hourOffset = 0): number { - const offsetMilis = hourOffset * MILISECONDS_IN_HOUR; - const currentTime = Date.now(); - return getStartOfDayTimestamp(currentTime, offsetMilis); -} - -const MILISECONDS_IN_HOUR = 3600000; -const MILLISECONDS_IN_DAY = 86400000; - -export function getStartOfDayTimestamp( - timestamp: number, - offsetMilis = 0 -): number { - return timestamp - ((timestamp - offsetMilis) % MILLISECONDS_IN_DAY); -} - -export function isYesterday(timestamp: number, hourOffset = 0): boolean { - const offsetMilis = hourOffset * MILISECONDS_IN_HOUR; - const yesterday = getStartOfDayTimestamp( - Date.now() - MILLISECONDS_IN_DAY, - offsetMilis - ); - const date = getStartOfDayTimestamp(timestamp, offsetMilis); - - return yesterday === date; -} - -export function isToday(timestamp: number, hourOffset = 0): boolean { - const offsetMilis = hourOffset * MILISECONDS_IN_HOUR; - const today = getStartOfDayTimestamp(Date.now(), offsetMilis); - const date = getStartOfDayTimestamp(timestamp, offsetMilis); - - return today === date; -} - // Function to get the bounding rectangle of a collection of elements export function getBoundingRectOfElements(elements: HTMLElement[]): DOMRect { let minX = Infinity, @@ -1612,73 +652,6 @@ export function getBoundingRectOfElements(elements: HTMLElement[]): DOMRect { }, }; } -export function convertToMorse(word: string): string { - const morseCode: Record = { - a: ".-", - b: "-...", - c: "-.-.", - d: "-..", - e: ".", - f: "..-.", - g: "--.", - h: "....", - i: "..", - j: ".---", - k: "-.-", - l: ".-..", - m: "--", - n: "-.", - o: "---", - p: ".--.", - q: "--.-", - r: ".-.", - s: "...", - t: "-", - u: "..-", - v: "...-", - w: ".--", - x: "-..-", - y: "-.--", - z: "--..", - "0": "-----", - "1": ".----", - "2": "..---", - "3": "...--", - "4": "....-", - "5": ".....", - "6": "-....", - "7": "--...", - "8": "---..", - "9": "----.", - ".": ".-.-.-", - ",": "--..--", - "?": "..--..", - "'": ".----.", - "/": "-..-.", - "(": "-.--.", - ")": "-.--.-", - "&": ".-...", - ":": "---...", - ";": "-.-.-.", - "=": "-...-", - "+": ".-.-.", - "-": "-....-", - _: "..--.-", - '"': ".-..-.", - $: "...-..-", - "!": "-.-.--", - "@": ".--.-.", - }; - - let morseWord = ""; - - const deAccentedWord = replaceSpecialChars(word); - for (let i = 0; i < deAccentedWord.length; i++) { - const letter = morseCode[deAccentedWord.toLowerCase()[i] as string]; - morseWord += letter !== undefined ? letter + "/" : ""; - } - return morseWord; -} export function typedKeys( obj: T @@ -1686,11 +659,6 @@ export function typedKeys( return Object.keys(obj) as unknown as T extends T ? (keyof T)[] : never; } -//https://ricardometring.com/javascript-replace-special-characters -export function replaceSpecialChars(str: string): string { - return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // Remove accents -} - export function reloadAfter(seconds: number): void { setTimeout(() => { window.location.reload(); @@ -1708,65 +676,4 @@ export function updateTitle(title?: string): void { } } -export function getNumberWithMagnitude(num: number): { - rounded: number; - roundedTo2: number; - orderOfMagnitude: string; -} { - const units = [ - "", - "thousand", - "million", - "billion", - "trillion", - "quadrillion", - "quintillion", - "sextillion", - "septillion", - "octillion", - "nonillion", - "decillion", - ]; - let unitIndex = 0; - let roundedNum = num; - - while (roundedNum >= 1000) { - roundedNum /= 1000; - unitIndex++; - } - - const unit = units[unitIndex] ?? "unknown"; - - return { - rounded: Math.round(roundedNum), - roundedTo2: roundTo2(roundedNum), - orderOfMagnitude: unit, - }; -} - -export function numberWithSpaces(x: number): string { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); -} - -export function lastElementFromArray(array: T[]): T | undefined { - return array[array.length - 1]; -} - -export function getLanguageDisplayString( - language: string, - noSizeString = false -): string { - let out = ""; - if (noSizeString) { - out = removeLanguageSize(language); - } else { - out = language; - } - return out.replace(/_/g, " "); -} - -export function removeLanguageSize(language: string): string { - return language.replace(/_\d*k$/g, ""); -} - // DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES diff --git a/frontend/src/ts/utils/numbers.ts b/frontend/src/ts/utils/numbers.ts new file mode 100644 index 000000000000..793a291dc4b6 --- /dev/null +++ b/frontend/src/ts/utils/numbers.ts @@ -0,0 +1,203 @@ +/** + * Calculates the standard deviation of an array of numbers. + * @param array An array of numbers. + * @returns The standard deviation of the input array. + */ +export function stdDev(array: number[]): number { + try { + const n = array.length; + const mean = array.reduce((a, b) => a + b) / n; + return Math.sqrt( + array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n + ); + } catch (e) { + return 0; + } +} + +/** + * Calculates the mean (average) of an array of numbers. + * @param array An array of numbers. + * @returns The mean of the input array. + */ +export function mean(array: number[]): number { + try { + return ( + array.reduce((previous, current) => (current += previous)) / array.length + ); + } catch (e) { + return 0; + } +} + +/** + * Calculates the median of an array of numbers. + * https://www.w3resource.com/javascript-exercises/fundamental/javascript-fundamental-exercise-88.php + * @param arr An array of numbers. + * @returns The median of the input array. + */ +export function median(arr: number[]): number { + try { + const mid = Math.floor(arr.length / 2), + nums = [...arr].sort((a, b) => a - b); + return arr.length % 2 !== 0 + ? (nums[mid] as number) + : ((nums[mid - 1] as number) + (nums[mid] as number)) / 2; + } catch (e) { + return 0; + } +} + +/** + * Rounds a number to two decimal places. + * @param num The number to round. + * @returns The input number rounded to two decimal places. + */ +export function roundTo2(num: number): number { + return Math.round((num + Number.EPSILON) * 100) / 100; +} +/** + * Converts a value in rem units to pixels based on the root element's font size. + * https://stackoverflow.com/questions/36532307/rem-px-in-javascript + * @param rem The value in rem units to convert to pixels. + * @returns The equivalent value in pixels. + */ +export function convertRemToPixels(rem: number): number { + return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); +} + +/** + * Formats a number with spaces for thousands separator. + * @param x The number to format. + * @returns The formatted number as a string with spaces for thousands separator. + */ +export function numberWithSpaces(x: number): string { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); +} + +/** + * Gets an integer between min and max, both are inclusive. + * @param min + * @param max + * @returns Random integer betwen min and max. + */ +export function randomIntFromRange(min: number, max: number): number { + const minNorm = Math.ceil(min); + const maxNorm = Math.floor(max); + return Math.floor(Math.random() * (maxNorm - minNorm + 1) + minNorm); +} + +/** + * Converts a number into a rounded form with its order of magnitude. + * @param num The number to convert. + * @returns An object containing the rounded number, rounded to 2 decimal places, + * and the order of magnitude (e.g., thousand, million, billion). + */ +export function getNumberWithMagnitude(num: number): { + rounded: number; + roundedTo2: number; + orderOfMagnitude: string; +} { + const units = [ + "", + "thousand", + "million", + "billion", + "trillion", + "quadrillion", + "quintillion", + "sextillion", + "septillion", + "octillion", + "nonillion", + "decillion", + ]; + let unitIndex = 0; + let roundedNum = num; + + while (roundedNum >= 1000) { + roundedNum /= 1000; + unitIndex++; + } + + const unit = units[unitIndex] ?? "unknown"; + + return { + rounded: Math.round(roundedNum), + roundedTo2: roundTo2(roundedNum), + orderOfMagnitude: unit, + }; +} + +/** + * Abbreviates a large number with a suffix (k, m, b, etc.) representing its order of magnitude. + * @param num The number to abbreviate. + * @param decimalPoints The number of decimal points to include in the result. Default is 1. + * @returns The abbreviated number as a string with the appropriate suffix. + */ +export function abbreviateNumber(num: number, decimalPoints = 1): string { + if (num < 1000) { + return num.toString(); + } + + const exp = Math.floor(Math.log(num) / Math.log(1000)); + const pre = "kmbtqQsSond".charAt(exp - 1); + return (num / Math.pow(1000, exp)).toFixed(decimalPoints) + pre; +} + +/** + * Finds the line of best fit (least squares regression line) for a set of y-values. + * @param values_y An array of y-values. + * @returns An array of two points representing the line (start and end points), + * or null if the array is empty. + */ +export function findLineByLeastSquares( + values_y: number[] +): [[number, number], [number, number]] | null { + let sum_x = 0; + let sum_y = 0; + let sum_xy = 0; + let sum_xx = 0; + let count = 0; + + /* + * We'll use those letiables for faster read/write access. + */ + let x = 0; + let y = 0; + const values_length = values_y.length; + + /* + * Nothing to do. + */ + if (values_length === 0) { + return null; + } + + /* + * Calculate the sum for each of the parts necessary. + */ + for (let v = 0; v < values_length; v++) { + x = v + 1; + y = values_y[v] as number; + sum_x += x; + sum_y += y; + sum_xx += x * x; + sum_xy += x * y; + count++; + } + + /* + * Calculate m and b for the formula: + * y = x * m + b + */ + const m = (count * sum_xy - sum_x * sum_y) / (count * sum_xx - sum_x * sum_x); + const b = sum_y / count - (m * sum_x) / count; + + const returnpoint1 = [1, 1 * m + b] as [number, number]; + const returnpoint2 = [values_length, values_length * m + b] as [ + number, + number + ]; + return [returnpoint1, returnpoint2]; +} diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts index 3867b89b08a3..efe6c8bb5649 100644 --- a/frontend/src/ts/utils/strings.ts +++ b/frontend/src/ts/utils/strings.ts @@ -1,5 +1,72 @@ /** - * + * Removes accents from a string. + * https://ricardometring.com/javascript-replace-special-characters + * @param str The input string. + * @returns A new string with accents removed. + */ +export function replaceSpecialChars(str: string): string { + return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // Remove accents +} + +/** + * Converts a camelCase string to words separated by spaces. + * @param str The camelCase string to convert. + * @returns The string with spaces inserted before capital letters and converted to lowercase. + */ +export function camelCaseToWords(str: string): string { + return str + .replace(/([A-Z])/g, " $1") + .trim() + .toLowerCase(); +} + +/** + * Returns the last character of a string. + * @param word The input string. + * @returns The last character of the input string, or an empty string if the input is empty. + */ +export function getLastChar(word: string): string { + try { + return word.charAt(word.length - 1); + } catch { + return ""; + } +} + +/** + * Replaces a character at a specific index in a string. + * @param str The input string. + * @param index The index at which to replace the character. + * @param chr The character to insert at the specified index. + * @returns A new string with the character at the specified index replaced. + */ +export function replaceCharAt(str: string, index: number, chr: string): string { + if (index > str.length - 1) return str; + return str.substring(0, index) + chr + str.substring(index + 1); +} + +/** + * Capitalizes the first letter of each word in a string. + * @param str The input string. + * @returns A new string with the first letter of each word capitalized. + */ +export function capitalizeFirstLetterOfEachWord(str: string): string { + return str + .split(/ +/) + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) + .join(" "); +} + +/** + * Capitalizes the first letter of a string. + * @param str The input string. + * @returns A new string with the first letter capitalized. + */ +export function capitalizeFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +/** * @param text String to split * @param delimiters Single character delimiters. */ @@ -26,3 +93,31 @@ export function splitByAndKeep(text: string, delimiters: string[]): string[] { return splitString; } + +/** + * Returns a display string for the given language, optionally removing the size indicator. + * @param language The language string. + * @param noSizeString Whether to remove the size indicator from the language string. Default is false. + * @returns A display string for the language. + */ +export function getLanguageDisplayString( + language: string, + noSizeString = false +): string { + let out = ""; + if (noSizeString) { + out = removeLanguageSize(language); + } else { + out = language; + } + return out.replace(/_/g, " "); +} + +/** + * Removes the size indicator from a language string. + * @param language The language string. + * @returns The language string with the size indicator removed. + */ +export function removeLanguageSize(language: string): string { + return language.replace(/_\d*k$/g, ""); +}