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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/lib/format.test.ts
Original file line number Diff line number Diff line change
@@ -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();
40 changes: 34 additions & 6 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
11 changes: 8 additions & 3 deletions src/pages/InstalledPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -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));

Expand Down
Loading