mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
76 lines
3.2 KiB
TypeScript
76 lines
3.2 KiB
TypeScript
/**
|
|
* Topic issues and their problem sub-issues: the ONE reading of "is this topic
|
|
* finished", shared by close-topics.ts (closes the topic issue) and
|
|
* release-topics.ts (publishes the topic's timever release) so the two can
|
|
* never disagree about when a topic was done.
|
|
*
|
|
* Rule: a topic is finished when it has a core set (`set:core` sub-issues) and
|
|
* every one of them is closed. Optional and deferred problems are extra credit
|
|
* and never hold a topic open. A topic with no core sub-issues is never
|
|
* finished — that would be indistinguishable from a mis-labelled problem set.
|
|
*
|
|
* Library: side-effect-free on import; every call takes the caller's github().
|
|
*/
|
|
import type { GitHub } from "./github.ts";
|
|
|
|
/** One problem sub-issue, narrowed from the sub_issues payload. */
|
|
export interface Problem {
|
|
number: number;
|
|
/** `LC <num> · <Name>`. */
|
|
title: string;
|
|
/** ISO instant the issue closed; null while it is open. */
|
|
closedAt: string | null;
|
|
labels: string[];
|
|
}
|
|
|
|
export interface Core {
|
|
/** Core sub-issues, closed and open alike, lowest number first. */
|
|
all: number[];
|
|
/** The core sub-issues still open — non-empty means the topic stays open. */
|
|
pending: number[];
|
|
/** When the last core sub-issue closed; null until the topic is finished. */
|
|
finishedAt: string | null;
|
|
}
|
|
|
|
/**
|
|
* Every sub-issue of a topic. The sub_issues payload carries the full issue
|
|
* objects (state + labels), so no per-problem follow-up request is needed.
|
|
*/
|
|
export async function readProblems(gh: GitHub, topic: number): Promise<Problem[]> {
|
|
const problems: Problem[] = [];
|
|
for await (const raw of gh.list(`/repos/${gh.repo}/issues/${topic}/sub_issues`)) {
|
|
if (!raw || typeof raw !== "object") continue;
|
|
if (!("number" in raw) || typeof raw.number !== "number") continue;
|
|
if (!("title" in raw) || typeof raw.title !== "string") continue;
|
|
if (!("state" in raw) || typeof raw.state !== "string") continue;
|
|
if (!("labels" in raw) || !Array.isArray(raw.labels)) continue;
|
|
const closedAt =
|
|
raw.state === "closed" && "closed_at" in raw && typeof raw.closed_at === "string"
|
|
? raw.closed_at
|
|
: null;
|
|
const labels = raw.labels.flatMap((label: unknown) =>
|
|
label && typeof label === "object" && "name" in label && typeof label.name === "string"
|
|
? [label.name]
|
|
: [],
|
|
);
|
|
problems.push({ number: raw.number, title: raw.title, closedAt, labels });
|
|
}
|
|
return problems.sort((a, b) => a.number - b.number);
|
|
}
|
|
|
|
/** The value of a `prefix:value` label (`set:` → `core`), if the problem has one. */
|
|
export function labelValue(problem: Problem, prefix: string): string | undefined {
|
|
return problem.labels.find((l) => l.startsWith(prefix))?.slice(prefix.length);
|
|
}
|
|
|
|
export function coreSet(problems: Problem[]): Core {
|
|
const core = problems.filter((p) => labelValue(p, "set:") === "core");
|
|
const pending = core.filter((p) => !p.closedAt).map((p) => p.number);
|
|
// ISO instants from the API share one format, so string order is time order.
|
|
const finishedAt =
|
|
core.length && !pending.length
|
|
? core.map((p) => p.closedAt!).reduce((a, b) => (a > b ? a : b))
|
|
: null;
|
|
return { all: core.map((p) => p.number), pending, finishedAt };
|
|
}
|