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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions lib/format.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
// Compact number formatting for display (1240 → "1.2k", 248723 → "249k").
export const formatCount = (n: number): string =>
n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(/\.0$/, "") + "k" : String(Math.round(n));
// Compact number formatting for display (1240 → "1.2k", 248723 → "249k", 1250000 → "1.3M").
// One decimal below 10 units ("1.2k"), none above ("249k"), so the string stays short.
const compact = (n: number, unit: number, suffix: string): string => {
const scaled = n / unit;
return scaled.toFixed(scaled >= 10 ? 0 : 1).replace(/\.0$/, "") + suffix;
};

export const formatCount = (n: number): string => {
if (n >= 1_000_000) return compact(n, 1_000_000, "M");
if (n >= 1000) {
const thousands = compact(n, 1000, "k");
// 999_950+ rounds up to "1000k"; roll over to the next unit rather than
// printing a four-digit thousands value.
return thousands === "1000k" ? compact(n, 1_000_000, "M") : thousands;
}
return String(Math.round(n));
};

// Fixed-precision rounding for deterministic geometry / imperative transforms —
// shared so radar, VS burst and tilt all round identically.
Expand Down
51 changes: 51 additions & 0 deletions tests/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { formatCount, round1, round2 } from "@/lib/format";

// formatCount feeds the card, the scout report and the duel bars, so the exact
// strings matter: they have to stay short enough for the fixed-width slots and
// never show a four-digit thousands value like "1200k".

describe("formatCount", () => {
it("prints small counts verbatim", () => {
expect(formatCount(0)).toBe("0");
expect(formatCount(7)).toBe("7");
expect(formatCount(999)).toBe("999");
});

it("switches to thousands at 1000", () => {
expect(formatCount(1000)).toBe("1k");
expect(formatCount(1240)).toBe("1.2k");
expect(formatCount(9999)).toBe("10k");
});

it("drops the decimal once there are ten or more thousands", () => {
expect(formatCount(10000)).toBe("10k");
expect(formatCount(248723)).toBe("249k");
});

it("switches to millions instead of a four-digit thousands value", () => {
// Top accounts clear a million stars once organization-owned repos count,
// and "1200k" is neither compact nor readable.
expect(formatCount(1_000_000)).toBe("1M");
expect(formatCount(1_250_000)).toBe("1.3M");
expect(formatCount(12_300_000)).toBe("12M");
});

it("rolls over rather than rounding up to 1000k", () => {
expect(formatCount(999_999)).toBe("1M");
});
});

describe("round1 / round2", () => {
it("rounds to fixed precision", () => {
expect(round2(1.005)).toBe(1);
expect(round2(1.239)).toBe(1.24);
expect(round1(1.24)).toBe(1.2);
expect(round1(1.25)).toBe(1.3);
});

it("is stable for values already at precision", () => {
expect(round2(2.5)).toBe(2.5);
expect(round1(2.5)).toBe(2.5);
});
});