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
4 changes: 3 additions & 1 deletion frontend/user/modules/opportunity-board.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ function emptyMessageFor(modeId) {
}

function isMissingSortValue(value) {
return value === null || value === undefined || Number.isNaN(value);
if (value === null || value === undefined) return true;
if (typeof value === "number") return !Number.isFinite(value);
return false;
}

export function sortItems(items, sortSpec) {
Expand Down
38 changes: 38 additions & 0 deletions tests/frontend/test_opportunity_board_sort.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,44 @@ test("sortItems: NaN values sort to the end like null (no silent reordering)", (
);
});

test("sortItems: Infinity/-Infinity sort to the end like NaN (bogus upstream math)", () => {
// Infinity escapes Number.isNaN (and Number() coercion) when an upstream
// metric divides by zero (e.g. rent/price=0 → yield=Infinity). The board's
// missing-value sink already catches null/undefined/NaN; ±Infinity belongs
// in the same bucket — otherwise it floats to the top of a desc sort and
// displaces legitimate rows, while formatValue renders the cell as "—".
const items = [
{ id: "pos_inf", yield: Number.POSITIVE_INFINITY },
{ id: "neg_inf", yield: Number.NEGATIVE_INFINITY },
{ id: "low", yield: 1 },
{ id: "high", yield: 9 },
];
const desc = sortItems(items, { key: "yield", direction: "desc" });
assert.deepEqual(
desc.slice(0, 2).map((i) => i.id),
["high", "low"],
"desc: finite numbers come first, ±Infinity at the end",
);
const descTail = desc.slice(2).map((i) => i.id).sort();
assert.deepEqual(
descTail,
["neg_inf", "pos_inf"],
"desc: both ±Infinity belong in the missing-marker bucket at the end",
);
const asc = sortItems(items, { key: "yield", direction: "asc" });
assert.deepEqual(
asc.slice(0, 2).map((i) => i.id),
["low", "high"],
"asc: finite numbers come first, ±Infinity still at the end",
);
const ascTail = asc.slice(2).map((i) => i.id).sort();
assert.deepEqual(
ascTail,
["neg_inf", "pos_inf"],
"asc: ±Infinity stays in the missing-marker bucket regardless of direction",
);
});

test("sortItems: mixed null/NaN/numbers — all missing markers cluster at the end", () => {
const items = [
{ id: "a", score: 50 },
Expand Down