feat(lens): order the deadlines, not just the closest one
The headline panel asked for one row and got one row, so nothing else could ask this question. `nextToExpire` answers it for any count and `firstToExpire` is now that function asked for one — one definition, so a big countdown and the lines under it can never disagree about which deadline is next. Unannounced ends still sort behind every dated one however long they have been running: a panel of deadlines that leads with "unknown" is not a panel of deadlines. It sorts a copy, because the array it is handed is the one the list on screen is rendering from. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c2179e29e0
commit
6e95e54ec6
+26
-18
@@ -39,28 +39,36 @@ export function outstanding<T extends Row>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The single row closest to expiring.
|
* The rows closest to expiring, soonest first.
|
||||||
*
|
*
|
||||||
* Reads the minimum rather than taking the first row, because the list it is
|
* Ordered here rather than taken off the top of the list, because the list it
|
||||||
* given is sorted by whatever mode the reader chose — under "doing first" the
|
* is given is sorted by whatever mode the reader chose — under "doing first"
|
||||||
* head of the list is what they are partway through, which is not what a panel
|
* the head of the list is what they are partway through, which is not what a
|
||||||
* headed "next to expire" is claiming to show.
|
* panel headed "next to expire" is claiming to show.
|
||||||
*
|
*
|
||||||
* An event with no announced end can only ever be the answer when nothing else
|
* An event with no announced end sorts behind every dated one however long it
|
||||||
* is running: it is real, but it is not a deadline.
|
* has been running: it is real, but it is not a deadline, and it can only
|
||||||
|
* surface here once the deadlines run out.
|
||||||
|
*/
|
||||||
|
export function nextToExpire<T extends Row>(
|
||||||
|
rows: readonly T[],
|
||||||
|
count: number,
|
||||||
|
): T[] {
|
||||||
|
const dated = rows
|
||||||
|
.filter((r) => r.clock.msRemaining !== null)
|
||||||
|
.sort((a, b) => (a.clock.msRemaining ?? 0) - (b.clock.msRemaining ?? 0));
|
||||||
|
const undated = rows.filter((r) => r.clock.msRemaining === null);
|
||||||
|
return [...dated, ...undated].slice(0, Math.max(0, count));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single row closest to expiring — the headline's own event.
|
||||||
|
*
|
||||||
|
* One definition, asked for one row, so the big countdown and the lines under
|
||||||
|
* it can never disagree about which deadline is next.
|
||||||
*/
|
*/
|
||||||
export function firstToExpire<T extends Row>(rows: readonly T[]): T | null {
|
export function firstToExpire<T extends Row>(rows: readonly T[]): T | null {
|
||||||
let best: T | null = null;
|
return nextToExpire(rows, 1)[0] ?? null;
|
||||||
let bestMs = Infinity;
|
|
||||||
for (const row of rows) {
|
|
||||||
const ms = row.clock.msRemaining;
|
|
||||||
if (ms === null) continue;
|
|
||||||
if (ms < bestMs) {
|
|
||||||
best = row;
|
|
||||||
bestMs = ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best ?? rows[0] ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
advanceFocus,
|
advanceFocus,
|
||||||
countByGame,
|
countByGame,
|
||||||
firstToExpire,
|
firstToExpire,
|
||||||
|
nextToExpire,
|
||||||
outstanding,
|
outstanding,
|
||||||
resolveFocus,
|
resolveFocus,
|
||||||
} from "../src/client/state/lens.ts";
|
} from "../src/client/state/lens.ts";
|
||||||
@@ -73,6 +74,60 @@ describe("firstToExpire", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("nextToExpire", () => {
|
||||||
|
test("orders by deadline, whatever order it was handed", () => {
|
||||||
|
// Same argument as firstToExpire, three rows deep: the reader asked for the
|
||||||
|
// three closest deadlines, not the first three rows of a list they had
|
||||||
|
// sorted by what they are partway through.
|
||||||
|
const rows = [
|
||||||
|
row("mid-run", "genshin", 9 * 86_400_000),
|
||||||
|
row("tomorrow", "hsr", 30 * 3_600_000),
|
||||||
|
row("tonight", "zzz", 3 * 3_600_000),
|
||||||
|
];
|
||||||
|
expect(nextToExpire(rows, 3).map((r) => r.event.id)).toEqual([
|
||||||
|
"tonight",
|
||||||
|
"tomorrow",
|
||||||
|
"mid-run",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("takes only as many as asked for", () => {
|
||||||
|
const rows = [
|
||||||
|
row("a", "genshin", 1000),
|
||||||
|
row("b", "hsr", 2000),
|
||||||
|
row("c", "zzz", 3000),
|
||||||
|
];
|
||||||
|
expect(nextToExpire(rows, 2).map((r) => r.event.id)).toEqual(["a", "b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("asking for more than there is returns what there is", () => {
|
||||||
|
expect(nextToExpire([row("a", "genshin", 1000)], 3)).toHaveLength(1);
|
||||||
|
expect(nextToExpire([], 3)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unannounced ends sort behind every real deadline", () => {
|
||||||
|
// A panel of deadlines that leads with "unknown" is not a panel of
|
||||||
|
// deadlines. They still appear once the dated ones run out, because the
|
||||||
|
// event is real — it just is not a countdown.
|
||||||
|
const rows = [
|
||||||
|
row("unknown", "zzz", null),
|
||||||
|
row("late", "wuwa", 90 * 86_400_000),
|
||||||
|
];
|
||||||
|
expect(nextToExpire(rows, 2).map((r) => r.event.id)).toEqual([
|
||||||
|
"late",
|
||||||
|
"unknown",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("leaves the list it was given alone", () => {
|
||||||
|
// It is handed the same array the list on screen is rendering from, and
|
||||||
|
// sorting that in place would reorder the reader's list from under them.
|
||||||
|
const rows = [row("b", "hsr", 2000), row("a", "genshin", 1000)];
|
||||||
|
nextToExpire(rows, 2);
|
||||||
|
expect(rows.map((r) => r.event.id)).toEqual(["b", "a"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("resolveFocus", () => {
|
describe("resolveFocus", () => {
|
||||||
const enabled: GameId[] = ["genshin", "hsr"];
|
const enabled: GameId[] = ["genshin", "hsr"];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user