mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
feat(api): add recent solves, yearly stats, and miss count to API
This commit is contained in:
1 parent
f2a2d2d7de
commit
f4fc0fd0e7
3 files changed
+87
-5
No files matched your search
@@ -23,6 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
CAMPAIGN_DAYS,
|
CAMPAIGN_DAYS,
|
||||||
|
MISS_WHERE,
|
||||||
type ProblemRow,
|
type ProblemRow,
|
||||||
RESERVED,
|
RESERVED,
|
||||||
REVIEW_CAP,
|
REVIEW_CAP,
|
||||||
@@ -84,8 +85,7 @@ async function openMisses(db: D1Database): Promise<ProblemRow[]> {
|
|||||||
.prepare(
|
.prepare(
|
||||||
`SELECT p.* FROM problems p
|
`SELECT p.* FROM problems p
|
||||||
JOIN attempts a ON a.lc_number = p.lc_number
|
JOIN attempts a ON a.lc_number = p.lc_number
|
||||||
WHERE a.id = (SELECT MAX(id) FROM attempts WHERE lc_number = p.lc_number)
|
WHERE ${MISS_WHERE}
|
||||||
AND a.result = 'fail' AND p.stage != 'retired'
|
|
||||||
ORDER BY a.date, p.lc_number`,
|
ORDER BY a.date, p.lc_number`,
|
||||||
)
|
)
|
||||||
.all<ProblemRow>();
|
.all<ProblemRow>();
|
||||||
|
|||||||
@@ -310,6 +310,15 @@ export async function lookupProblems(
|
|||||||
export const DUE_WHERE = `stage != 'retired' AND next_review IS NOT NULL AND next_review <= ?1
|
export const DUE_WHERE = `stage != 'retired' AND next_review IS NOT NULL AND next_review <= ?1
|
||||||
AND (defer_until IS NULL OR defer_until <= ?1)`;
|
AND (defer_until IS NULL OR defer_until <= ?1)`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What an open miss means, in one place: a problem `p` whose most recent
|
||||||
|
* attempt (highest id) failed and that has not retired. The sweep day works
|
||||||
|
* these and the stats' "attempting" count is how many there are, so the two
|
||||||
|
* cannot disagree. Expects `problems p` joined to `attempts a` on lc_number.
|
||||||
|
*/
|
||||||
|
export const MISS_WHERE = `a.id = (SELECT MAX(id) FROM attempts WHERE lc_number = p.lc_number)
|
||||||
|
AND a.result = 'fail' AND p.stage != 'retired'`;
|
||||||
|
|
||||||
/** Reviews due on/before `date`, oldest first — the overflow carry order. */
|
/** Reviews due on/before `date`, oldest first — the overflow carry order. */
|
||||||
export async function dueReviews(db: D1Database, date: string): Promise<ProblemRow[]> {
|
export async function dueReviews(db: D1Database, date: string): Promise<ProblemRow[]> {
|
||||||
const { results } = await db
|
const { results } = await db
|
||||||
|
|||||||
+76
-3
@@ -8,12 +8,17 @@
|
|||||||
* srs.ts turns a topic's days-since-last-attempt into a band, so "which
|
* srs.ts turns a topic's days-since-last-attempt into a band, so "which
|
||||||
* concepts have gone cold" has exactly one definition and the dashboard is
|
* concepts have gone cold" has exactly one definition and the dashboard is
|
||||||
* free to colour it however it likes.
|
* free to colour it however it likes.
|
||||||
|
*
|
||||||
|
* `recent`, `attempting` and `year` feed the docs page's LeetCode-style cards
|
||||||
|
* (recently solved, the solved ring, the year heatmap). They are additive: the
|
||||||
|
* terminal dashboard ignores them.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
CAMPAIGN_DAYS,
|
CAMPAIGN_DAYS,
|
||||||
CAMPAIGN_START,
|
CAMPAIGN_START,
|
||||||
DIFFICULTIES,
|
DIFFICULTIES,
|
||||||
DUE_WHERE,
|
DUE_WHERE,
|
||||||
|
MISS_WHERE,
|
||||||
addDays,
|
addDays,
|
||||||
campaignDay,
|
campaignDay,
|
||||||
campaignWeek,
|
campaignWeek,
|
||||||
@@ -54,6 +59,12 @@ interface TopicStat {
|
|||||||
temperature: Temperature;
|
temperature: Temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Days in the year heatmap: the 365 ending today, like LeetCode's "past one year". */
|
||||||
|
const YEAR_DAYS = 365;
|
||||||
|
|
||||||
|
/** How many problems `recent` lists. */
|
||||||
|
const RECENT_LIMIT = 10;
|
||||||
|
|
||||||
async function buildTopics(db: D1Database, today: string): Promise<TopicStat[]> {
|
async function buildTopics(db: D1Database, today: string): Promise<TopicStat[]> {
|
||||||
// Three shapes, three statements: batch() carries one row type, and casting
|
// Three shapes, three statements: batch() carries one row type, and casting
|
||||||
// its results apart would cost more than the round trips it saves.
|
// its results apart would cost more than the round trips it saves.
|
||||||
@@ -195,9 +206,9 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
|||||||
);
|
);
|
||||||
const queue = dates.map((date, i) => ({ date, due: queueRows[i]?.results[0]?.n ?? 0 }));
|
const queue = dates.map((date, i) => ({ date, due: queueRows[i]?.results[0]?.n ?? 0 }));
|
||||||
|
|
||||||
// Attempts per campaign day, every day present. This is the heatmap's data
|
// Attempts per campaign day, every day present. This is the README heatmap's
|
||||||
// model — the docs page reads the tail of it for "last 7 days" rather than
|
// data model — the terminal dashboard lays it out by campaign week and reads
|
||||||
// asking a second question that could answer differently.
|
// its tail for "last 14 days" rather than asking a second question.
|
||||||
const end = addDays(CAMPAIGN_START, CAMPAIGN_DAYS - 1);
|
const end = addDays(CAMPAIGN_START, CAMPAIGN_DAYS - 1);
|
||||||
const { results: heatRows } = await db
|
const { results: heatRows } = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -215,16 +226,78 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
|||||||
return { date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 };
|
return { date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The ten problems most recently solved, dated by each one's FIRST passing
|
||||||
|
// attempt — when it was completed, not when it was last re-solved (LeetCode's
|
||||||
|
// "Recent AC"). Same-day ties fall back to log order.
|
||||||
|
const { results: recentRows } = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT p.lc_number AS lc, p.title AS title, p.difficulty AS difficulty,
|
||||||
|
MIN(a.date) AS date, MIN(a.id) AS seq
|
||||||
|
FROM attempts a JOIN problems p ON p.lc_number = a.lc_number
|
||||||
|
WHERE a.result = 'pass'
|
||||||
|
GROUP BY p.lc_number
|
||||||
|
ORDER BY date DESC, seq DESC
|
||||||
|
LIMIT ?1`,
|
||||||
|
)
|
||||||
|
.bind(RECENT_LIMIT)
|
||||||
|
.all<{ lc: number; title: string; difficulty: string; date: string; seq: number }>();
|
||||||
|
const recent = recentRows.map(({ lc, title, difficulty, date }) => ({ lc, title, difficulty, date }));
|
||||||
|
|
||||||
|
// Problems still standing on a failure — the sweep day's miss log, counted.
|
||||||
|
const missRow = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT COUNT(*) AS n FROM problems p
|
||||||
|
JOIN attempts a ON a.lc_number = p.lc_number
|
||||||
|
WHERE ${MISS_WHERE}`,
|
||||||
|
)
|
||||||
|
.first<{ n: number }>();
|
||||||
|
|
||||||
|
// Attempts per day over the past year, every day present, plus the three
|
||||||
|
// figures LeetCode prints above its heatmap. `heat` stays campaign-scoped
|
||||||
|
// (the terminal dashboard lays it out by campaign week); this is the
|
||||||
|
// calendar-scoped view of the same table.
|
||||||
|
const yearStart = addDays(today, -(YEAR_DAYS - 1));
|
||||||
|
const { results: yearRows } = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT date,
|
||||||
|
COUNT(*) AS attempts,
|
||||||
|
SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) AS passes
|
||||||
|
FROM attempts WHERE date BETWEEN ?1 AND ?2 GROUP BY date`,
|
||||||
|
)
|
||||||
|
.bind(yearStart, today)
|
||||||
|
.all<{ date: string; attempts: number; passes: number | null }>();
|
||||||
|
const yearByDate = new Map(yearRows.map((r) => [r.date, r]));
|
||||||
|
const yearDays = Array.from({ length: YEAR_DAYS }, (_, i) => {
|
||||||
|
const date = addDays(yearStart, i);
|
||||||
|
const row = yearByDate.get(date);
|
||||||
|
return { date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 };
|
||||||
|
});
|
||||||
|
let run = 0;
|
||||||
|
let maxStreak = 0;
|
||||||
|
for (const d of yearDays) {
|
||||||
|
run = d.attempts > 0 ? run + 1 : 0;
|
||||||
|
maxStreak = Math.max(maxStreak, run);
|
||||||
|
}
|
||||||
|
const year = {
|
||||||
|
days: yearDays,
|
||||||
|
attempts: yearDays.reduce((n, d) => n + d.attempts, 0),
|
||||||
|
active_days: yearDays.filter((d) => d.attempts > 0).length,
|
||||||
|
max_streak: maxStreak,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generated: new Date().toISOString(),
|
generated: new Date().toISOString(),
|
||||||
campaign: { day: campaignDay(today), week: campaignWeek(today), start: CAMPAIGN_START, days: CAMPAIGN_DAYS },
|
campaign: { day: campaignDay(today), week: campaignWeek(today), start: CAMPAIGN_START, days: CAMPAIGN_DAYS },
|
||||||
phases: [...phases.values()],
|
phases: [...phases.values()],
|
||||||
ladder,
|
ladder,
|
||||||
difficulty,
|
difficulty,
|
||||||
|
attempting: missRow?.n ?? 0,
|
||||||
topics: await buildTopics(db, today),
|
topics: await buildTopics(db, today),
|
||||||
gates,
|
gates,
|
||||||
streak: await streak(db, today),
|
streak: await streak(db, today),
|
||||||
queue,
|
queue,
|
||||||
heat,
|
heat,
|
||||||
|
recent,
|
||||||
|
year,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user