diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts new file mode 100644 index 0000000..dfcdde1 --- /dev/null +++ b/src/lib/format.test.ts @@ -0,0 +1,42 @@ +import { compareVersions, formatBytes, formatCount, percent } from "./format"; + +function assert(condition: boolean, message: string) { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +function runTests() { + // basic version comparison + assert(compareVersions("1.2.10", "1.2.9") > 0, "1.2.10 > 1.2.9"); + assert(compareVersions("1.2.9", "1.2.10") < 0, "1.2.9 < 1.2.10"); + assert(compareVersions("1.2.3", "1.2.3") === 0, "1.2.3 === 1.2.3"); + + // segment length mismatch + assert(compareVersions("1.2", "1.2.1") < 0, "1.2 < 1.2.1"); + assert(compareVersions("1.2.1", "1.2") > 0, "1.2.1 > 1.2"); + assert(compareVersions("1.2.0", "1.2") === 0, "1.2.0 === 1.2"); + + // multi-digit segments + assert(compareVersions("2.0.15", "2.0.2") > 0, "2.0.15 > 2.0.2"); + assert(compareVersions("100.0.0", "99.99.99") > 0, "100.0.0 > 99.99.99"); + + // formatCount + assert(formatCount(500) === "500", "500 formatCount"); + assert(formatCount(1500) === "2k", "1500 formatCount"); + assert(formatCount(1842000) === "1.8M", "1842000 formatCount"); + + // formatBytes + assert(formatBytes(500) === "500 B", "500 B formatBytes"); + assert(formatBytes(2048) === "2 KB", "2 KB formatBytes"); + assert(formatBytes(2097152) === "2.0 MB", "2.0 MB formatBytes"); + + // percent + assert(percent(50, 100) === 50, "50% percent"); + assert(percent(1, 0) === 0, "0 percent when total 0"); + assert(percent(150, 100) === 100, "100% max percent"); + + console.log("All format tests passed successfully."); +} + +runTests(); diff --git a/src/lib/format.ts b/src/lib/format.ts index 345259d..dd53555 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -19,13 +19,41 @@ export function percent(received: number, total: number): number { return Math.min(100, Math.round((received / total) * 100)); } -/** Compare "1.2.10" vs "1.2.9" numerically per segment. Good enough for mod versions. */ +/** + * Compare "1.2.10" vs "1.2.9" numerically per segment. Good enough for mod versions. + * Single-pass index scanner to avoid array allocations (.split/.map) during list sorting. + */ export function compareVersions(a: string, b: string): number { - const pa = a.split(".").map((s) => parseInt(s, 10) || 0); - const pb = b.split(".").map((s) => parseInt(s, 10) || 0); - for (let i = 0; i < Math.max(pa.length, pb.length); i++) { - const diff = (pa[i] ?? 0) - (pb[i] ?? 0); - if (diff !== 0) return diff; + let i = 0; + let j = 0; + const lenA = a.length; + const lenB = b.length; + + while (i < lenA || j < lenB) { + let numA = 0; + while (i < lenA && a.charCodeAt(i) !== 46 /* '.' */) { + const code = a.charCodeAt(i); + if (code >= 48 && code <= 57) { + numA = numA * 10 + (code - 48); + } + i++; + } + if (i < lenA) i++; // skip '.' + + let numB = 0; + while (j < lenB && b.charCodeAt(j) !== 46 /* '.' */) { + const code = b.charCodeAt(j); + if (code >= 48 && code <= 57) { + numB = numB * 10 + (code - 48); + } + j++; + } + if (j < lenB) j++; // skip '.' + + if (numA !== numB) { + return numA - numB; + } } + return 0; } diff --git a/src/pages/InstalledPage.tsx b/src/pages/InstalledPage.tsx index ef1b9df..c0173f2 100644 --- a/src/pages/InstalledPage.tsx +++ b/src/pages/InstalledPage.tsx @@ -437,6 +437,11 @@ export default function InstalledPage() { const mods = snapshot?.mods ?? []; + const collator = useMemo( + () => new Intl.Collator(undefined, { sensitivity: "base" }), + [], + ); + const { enabledMods, disabledMods } = useMemo(() => { const enabled: InstalledMod[] = []; const disabled: InstalledMod[] = []; @@ -447,13 +452,13 @@ export default function InstalledPage() { disabled.push(m); } } + // Reusing Intl.Collator instance avoids creating collators repeatedly during sorting const cmp = (a: InstalledMod, b: InstalledMod) => - a.name.localeCompare(b.name, undefined, { sensitivity: "base" }) || - a.name.localeCompare(b.name); + collator.compare(a.name, b.name) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0); enabled.sort(cmp); disabled.sort(cmp); return { enabledMods: enabled, disabledMods: disabled }; - }, [mods]); + }, [mods, collator]); useThumbnails(mods.map((m) => m.name));