mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-03 05:27:36 +00:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7384a33f3a | ||
|
|
0064d15844 | ||
|
|
e061aca54c | ||
|
|
1eb85ee2fb | ||
|
|
cac2f744e4 | ||
|
|
9e90cfb729 | ||
|
|
dc15237381 | ||
|
|
af7063b71f | ||
|
|
c5f3c95fcc | ||
|
|
fe2a7bb4e9 | ||
|
|
8689075ac9 | ||
|
|
9b5879b6eb | ||
|
|
53136b076c | ||
|
|
95f10980d4 | ||
|
|
c6ecb85507 | ||
|
|
2a69f36e6a | ||
|
|
5e4628b20f | ||
|
|
72a8399e6d | ||
|
|
6ebca68ff8 | ||
|
|
957ddf9849 | ||
|
|
e67cc79f39 | ||
|
|
f086c2b39a |
No files matched your search
@@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
name: campaign-issues
|
||||||
|
description: Create GitHub issues for this LeetCode campaign — individual problem issues and the topic issues that own them — using the repo's exact title, label, milestone, body, and sub-issue conventions. Use when asked to add/create an issue for a LeetCode problem or a topic, attach a problem to its topic, or backfill the issue catalog.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Campaign issues
|
||||||
|
|
||||||
|
The GitHub issue tracker **is the campaign catalog**: ~24 `topic` issues each own a
|
||||||
|
set of `problem` sub-issues. The Worker reconciles this catalog into D1 nightly
|
||||||
|
(`/admin/reconcile`), so a correctly-labelled issue is all that's needed — the SRS,
|
||||||
|
digest, charts, and Project mirror pick it up automatically. Get the labels/body shape
|
||||||
|
right; everything downstream is reconciliation.
|
||||||
|
|
||||||
|
All commands use `gh` against `prdlk/leetcode` and fall back to `gh auth token`.
|
||||||
|
|
||||||
|
## Problem issue
|
||||||
|
|
||||||
|
One issue per LeetCode problem, attached as a sub-issue of its topic.
|
||||||
|
|
||||||
|
- **Title:** `LC <num> · <Title>` — the LC number and the problem's name, nothing
|
||||||
|
else. Separator is a middle dot `·` (U+00B7) with single spaces. Difficulty and
|
||||||
|
set are **labels only**; never repeat them in the title (they used to be a `·
|
||||||
|
Hard · core` suffix — that is gone, and `catalog.ts` no longer parses it).
|
||||||
|
- **Labels** (four, all required — these are what the Worker reads):
|
||||||
|
- `problem` — marks the sub-issue as curriculum; without it the reconcile skips it.
|
||||||
|
- `diff:easy` | `diff:medium` | `diff:hard` — LeetCode's own rating, do not invent.
|
||||||
|
- `set:core` | `set:optional` | `set:deferred` — see set semantics below.
|
||||||
|
- `phase:N` — the **topic's** phase (same phase label the topic issue carries).
|
||||||
|
- **Milestone:**
|
||||||
|
- `core` / `optional` → the topic's phase milestone.
|
||||||
|
- `deferred` → `Review — Transfer & Mocks` (deferred Hards are parked until week 8).
|
||||||
|
- Pass the milestone **title**, not its number, to `--milestone`.
|
||||||
|
- **Body** (exact shape; `<slug>` is the LeetCode URL slug, `<topic-issue>` the owning topic's number):
|
||||||
|
|
||||||
|
```
|
||||||
|
https://leetcode.com/problems/<slug>/
|
||||||
|
|
||||||
|
- **Topic:** #<topic-issue>
|
||||||
|
- **Set:** <core|optional|deferred>
|
||||||
|
- **Language:** Python
|
||||||
|
|
||||||
|
### Close-out (paste before closing this issue)
|
||||||
|
- Time: O(?)
|
||||||
|
- Space: O(?)
|
||||||
|
- Edge case traced:
|
||||||
|
- First attempt passed: yes / no
|
||||||
|
- Miss note:
|
||||||
|
```
|
||||||
|
|
||||||
|
Create it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gh issue create --repo prdlk/leetcode \
|
||||||
|
--title "LC 42 · Trapping Rain Water" \
|
||||||
|
--label problem --label set:core --label diff:hard --label phase:1 \
|
||||||
|
--milestone "Phase I — Linear Structures" \
|
||||||
|
--body "https://leetcode.com/problems/trapping-rain-water/
|
||||||
|
|
||||||
|
- **Topic:** #9
|
||||||
|
- **Set:** core
|
||||||
|
- **Language:** Python
|
||||||
|
|
||||||
|
### Close-out (paste before closing this issue)
|
||||||
|
- Time: O(?)
|
||||||
|
- Space: O(?)
|
||||||
|
- Edge case traced:
|
||||||
|
- First attempt passed: yes / no
|
||||||
|
- Miss note:"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then **attach it as a sub-issue of the topic** (this is a separate API call — creating
|
||||||
|
the issue does not link it). The sub-issues API wants the child's internal **id**, not
|
||||||
|
its issue number, and the field is integer-typed, so use `-F` (not `-f`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
child_id=$(gh api repos/prdlk/leetcode/issues/211 --jq '.id')
|
||||||
|
gh api --method POST repos/prdlk/leetcode/issues/9/sub_issues -F sub_issue_id=$child_id
|
||||||
|
# verify
|
||||||
|
gh api repos/prdlk/leetcode/issues/9/sub_issues --jq '.[] | "\(.number) \(.title)"'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Topic issue
|
||||||
|
|
||||||
|
One issue per curriculum topic; owns the problem sub-issues.
|
||||||
|
|
||||||
|
- **Title:** `Topic NN — <Name>` (zero-padded number, em dash, e.g. `Topic 07 — Monotonic Stack`).
|
||||||
|
- **Labels:** `topic`, `phase:N`.
|
||||||
|
- **Milestone:** that phase's milestone (title, not number).
|
||||||
|
- **Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
**Scheduled:** <Weekday Mon DD, YYYY>
|
||||||
|
**Phase:** <Roman> — <Phase name> (Week N)
|
||||||
|
**Builds on:** Topic <n>
|
||||||
|
|
||||||
|
### Trigger
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Invariant
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Trap
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Close-out ritual
|
||||||
|
Before every submit, out loud:
|
||||||
|
1. State time and space complexity.
|
||||||
|
2. Trace one edge case.
|
||||||
|
|
||||||
|
### Day rules
|
||||||
|
- 90-minute coding cap. Stop at the cap, even mid-problem.
|
||||||
|
- Core problems first. Optional only if the core is done.
|
||||||
|
- Finished early? Do this topic's optional set. Do not start tomorrow.
|
||||||
|
```
|
||||||
|
|
||||||
|
`Scheduled` should come from the calendar of record — `apps/api/data/schedule.json`
|
||||||
|
(day → topic issue) — not guessed.
|
||||||
|
|
||||||
|
## Reference tables
|
||||||
|
|
||||||
|
Set semantics (from the label descriptions):
|
||||||
|
|
||||||
|
| set | meaning | milestone |
|
||||||
|
|---|---|---|
|
||||||
|
| `core` | must solve on the topic day | topic's phase milestone |
|
||||||
|
| `optional` | solve only if the clock allows | topic's phase milestone |
|
||||||
|
| `deferred` | a Hard, parked until week 8 | `Review — Transfer & Mocks` |
|
||||||
|
|
||||||
|
Milestones (`--milestone` takes the **title**):
|
||||||
|
|
||||||
|
| # | title |
|
||||||
|
|---|---|
|
||||||
|
| 1 | Phase I — Linear Structures |
|
||||||
|
| 2 | Phase II — Nodal & Grid |
|
||||||
|
| 3 | Phase III — Hierarchical |
|
||||||
|
| 4 | Phase IV — Relational |
|
||||||
|
| 5 | Phase V — Decision Space |
|
||||||
|
| 6 | Review — Transfer & Mocks |
|
||||||
|
|
||||||
|
Find a topic issue and confirm its phase before creating problems under it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gh issue list --repo prdlk/leetcode --label topic --state all --limit 30 \
|
||||||
|
--json number,title,labels,milestone
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Check the LC number is not already in the curriculum BEFORE creating.** D1 keys
|
||||||
|
`problems` on `lc_number` (`ON CONFLICT(lc_number)`), so two issues for one LC number
|
||||||
|
silently fight over a single row — whichever the reconcile walks last wins, and the
|
||||||
|
loser's issue number ends up orphaned in D1. A problem can legitimately move topic or
|
||||||
|
set; that is a relabel/re-parent of the *existing* issue, never a second issue:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gh issue list --repo prdlk/leetcode --label problem --state all --limit 300 \
|
||||||
|
--search "LC 42" --json number,title,labels,state
|
||||||
|
```
|
||||||
|
|
||||||
|
If a row already exists, change the existing issue instead: swap its `set:*` label,
|
||||||
|
move the sub-issue to the new topic, and fix its milestone. Then reconcile the board
|
||||||
|
(`project-fields` skill). If a duplicate did get created and one is deleted, the
|
||||||
|
surviving issue number and set only reach D1 on the next catalog reconcile — SRS
|
||||||
|
columns (`stage`, `next_review`) are preserved, since they are the real attempt history.
|
||||||
|
|
||||||
|
- **`-F` vs `-f`:** `sub_issue_id` must be an integer; `-f` sends a string and the API
|
||||||
|
rejects it with `is not of type integer`.
|
||||||
|
- **id, not number:** the sub-issue payload wants the child's `.id` (a large opaque
|
||||||
|
integer), never its display `#number`.
|
||||||
|
- **`--milestone` wants the title string**, not the numeric id (`'1' not found` otherwise).
|
||||||
|
- **Reconcile, don't react:** don't edit D1 or the Project by hand — a correct issue is
|
||||||
|
reconciled into SRS state on the nightly cron. Solutions landing in `work/` are what
|
||||||
|
close problem issues (via `close-solved.yml`), which then roll up to close topics.
|
||||||
|
- **Difficulty is LeetCode's**, `set` is our scheduling choice — they're independent. A
|
||||||
|
Hard can be `core` if we deliberately schedule it (vs the usual `deferred`).
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
name: project-fields
|
||||||
|
description: Reconcile the "Interview Prep" GitHub Project (user project #2) fields — Set and Difficulty projected from issue labels — and understand which fields the Worker mirrors vs which live only in D1. Use when asked to sync/fix/backfill Project fields, when Project columns look empty or stale, or when adding a field to the project.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Interview Prep Project fields
|
||||||
|
|
||||||
|
The user-level Project **"Interview Prep"** (`prdlk` project #2, id `PVT_kwHOADoNjs4BhV1a`)
|
||||||
|
is a *board over* the issue catalog, never a source of truth. Every field is a projection
|
||||||
|
of something owned elsewhere.
|
||||||
|
|
||||||
|
## Field ownership — the rule that matters
|
||||||
|
|
||||||
|
| Field | Owner | Written by |
|
||||||
|
|---|---|---|
|
||||||
|
| `Set` (Core/Optional/Deferred) | issue label `set:*` | `sync-project-fields`, last step of `close-solved.yml` |
|
||||||
|
| `Difficulty` (Easy/Medium/Hard) | issue label `diff:*` | `sync-project-fields`, last step of `close-solved.yml` |
|
||||||
|
| `Target Date` | D1 `next_review` | Worker mirror, per log (`apps/api/src/mirror.ts`) |
|
||||||
|
| `Status`, `Labels`, `Milestone`, `Parent issue`, … | GitHub built-ins | GitHub |
|
||||||
|
|
||||||
|
**SRS stage and first-attempt result are deliberately NOT Project fields.** They live only
|
||||||
|
in D1; the charts, digest and `/api/stats` read them from there. Duplicating them into
|
||||||
|
Project single-selects bought nothing but drift and two extra writes per log. Do not re-add
|
||||||
|
them to the project, and do not re-add `setStage`/`setFirstAttempt` to `mirror.ts`.
|
||||||
|
|
||||||
|
## Reconciling Set / Difficulty
|
||||||
|
|
||||||
|
`apps/cli/sync-project-fields.ts` recomputes every desired value from the issue labels and
|
||||||
|
writes only what differs — same reconcile-don't-react shape as `close-solved`.
|
||||||
|
|
||||||
|
**In CI it runs itself.** It is the final step of `.github/workflows/close-solved.yml`, so
|
||||||
|
any push under `work/` reconciles the board along with the issues and D1. The step is
|
||||||
|
`continue-on-error: true` and skips cleanly when `PROJECT_PAT` is unset — D1 and the issues
|
||||||
|
are the truth, so a Project failure must never fail a run that already closed issues and
|
||||||
|
logged the solve. It honours the workflow's `dry_run` input via `DRY_RUN=true`.
|
||||||
|
|
||||||
|
Run it by hand when issues changed without a `work/` push (e.g. you just filed a problem
|
||||||
|
issue, or relabelled one):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# needs a token with the `project` scope — neither `gh auth token` nor GITHUB_TOKEN has it.
|
||||||
|
export GH_TOKEN=$(grep '^GH_PAT=' apps/api/.dev.vars | cut -d= -f2-)
|
||||||
|
|
||||||
|
bun run sync-project-fields -- --dry-run # or DRY_RUN=1|true; prints the table, writes nothing
|
||||||
|
bun run sync-project-fields # apply
|
||||||
|
bun run sync-project-fields # re-run: must report 0 writes
|
||||||
|
```
|
||||||
|
|
||||||
|
Invariants the script holds — preserve them in any edit:
|
||||||
|
|
||||||
|
- **Only `problem`-labelled items are touched.** Topic, cadence and review issues have no
|
||||||
|
set/difficulty; they are skipped, never written and never cleared.
|
||||||
|
- **Idempotent.** A second run reports `0 field value(s)`. That re-run *is* the verification.
|
||||||
|
- **Labels are truth.** Never "fix" a field by hand in the UI — fix the label, then reconcile.
|
||||||
|
A hand-edited field is silently reverted on the next run.
|
||||||
|
- **Fields and options resolve by NAME**, so re-creating a field in the UI doesn't strand a
|
||||||
|
dead id. A missing field or option is a hard error, not a silent skip.
|
||||||
|
- **A problem missing a `set:`/`diff:` label** warns and is left untouched (it means the
|
||||||
|
issue is mislabelled — fix it with the `campaign-issues` skill).
|
||||||
|
|
||||||
|
## Auth gotcha
|
||||||
|
|
||||||
|
Project v2 needs the `project` scope. The local `gh` login has only
|
||||||
|
`gist, read:org, repo, workflow`, so **any** `gh project`/GraphQL project call fails with
|
||||||
|
the default token. Use the Worker's `GH_PAT` from `apps/api/.dev.vars` via `GH_TOKEN`
|
||||||
|
(never echo it). Symptom of the wrong token: `Resource not accessible by personal access
|
||||||
|
token` or an empty `projectsV2.nodes`.
|
||||||
|
|
||||||
|
## Inspecting the project
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export GH_TOKEN=$(grep '^GH_PAT=' apps/api/.dev.vars | cut -d= -f2-)
|
||||||
|
|
||||||
|
# fields + single-select options
|
||||||
|
gh api graphql -f owner=prdlk -f title="Interview Prep" -f query='
|
||||||
|
query($owner: String!, $title: String!) {
|
||||||
|
user(login: $owner) { projectsV2(first: 10, query: $title) { nodes { id title number
|
||||||
|
items(first: 1) { totalCount }
|
||||||
|
fields(first: 30) { nodes {
|
||||||
|
... on ProjectV2FieldCommon { id name dataType }
|
||||||
|
... on ProjectV2SingleSelectField { options { id name } } } } } } }
|
||||||
|
}' --jq '.data.user.projectsV2.nodes[] | select(.title=="Interview Prep")
|
||||||
|
| .fields.nodes[] | "\(.name)\t\(.dataType)\t\((.options // []) | map(.name) | join(","))"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes use `updateProjectV2ItemFieldValue` with
|
||||||
|
`value: { singleSelectOptionId: … }` (dates use `{ date: "YYYY-MM-DD" }`); clearing uses
|
||||||
|
`clearProjectV2ItemFieldValue`. An item id is *not* an issue number — resolve it from the
|
||||||
|
project's `items` connection (or the issue's `projectItems`), as `mirror.ts` does.
|
||||||
|
|
||||||
|
## Adding a new projected field
|
||||||
|
|
||||||
|
1. Create the field + options in the Project UI.
|
||||||
|
2. Add it to the `FIELDS` table in `apps/cli/sync-project-fields.ts` — one entry mapping
|
||||||
|
label → option name. Nothing else needs to change; resolution, diffing, dry-run and
|
||||||
|
reporting are generic over that table.
|
||||||
|
3. Dry-run, apply, re-run to prove idempotency.
|
||||||
|
|
||||||
|
If the new field's truth lives in **D1** rather than a label, it belongs in the Worker
|
||||||
|
mirror instead (`apps/api/src/mirror.ts`) — but read the ownership rule above first and
|
||||||
|
default to *not* mirroring.
|
||||||
|
|
||||||
|
## Known trap
|
||||||
|
|
||||||
|
`resolve()` in `mirror.ts` throws if a field it asks for is absent, *before* caching
|
||||||
|
`info` — so one missing field breaks **every** mirrored write, including `Target Date`,
|
||||||
|
and only as a `console.warn`. If Target Date silently stops updating, check that every
|
||||||
|
field `resolve()` looks up still exists in the project.
|
||||||
@@ -39,3 +39,25 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
DRY_RUN: ${{ inputs.dry_run }}
|
DRY_RUN: ${{ inputs.dry_run }}
|
||||||
SRS_ADMIN_KEY: ${{ secrets.SRS_ADMIN_KEY }}
|
SRS_ADMIN_KEY: ${{ secrets.SRS_ADMIN_KEY }}
|
||||||
|
|
||||||
|
# Project fields are a projection of the issue labels, so the same push
|
||||||
|
# that adds a solution (and any issue filed since the last run) can leave
|
||||||
|
# the board's Set/Difficulty empty. Reconciling here keeps the one
|
||||||
|
# "solution landed" entry point responsible for the whole fan-out; the
|
||||||
|
# script is idempotent, so a run with nothing to do writes nothing.
|
||||||
|
#
|
||||||
|
# Needs the `project` scope, which GITHUB_TOKEN does not have — hence
|
||||||
|
# PROJECT_PAT. Unset = skip, matching SRS_ADMIN_KEY above. Best-effort by
|
||||||
|
# design: D1 and the issues are the truth, so a Project failure must not
|
||||||
|
# fail a run that already closed issues and logged the solve.
|
||||||
|
- name: Reconcile Project Set/Difficulty
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.PROJECT_PAT }}
|
||||||
|
DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$GH_TOKEN" ]; then
|
||||||
|
echo "PROJECT_PAT unset — skipping the Project field reconcile"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
bun apps/cli/sync-project-fields.ts
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
LeetCode interview-prep campaign (8 weeks, 2026-08-17 → Oct 11) run as a repo: solution files under `work/`, a Blume docs site under `apps/docs/`, GitHub Actions that reconcile issues and publish docs, and a Cloudflare Worker (`apps/api/`) that runs the spaced-repetition system (SRS) — daily digest email, one-tap logging, weekly review issues, live charts. Issues model the curriculum: ~24 `topic` issues own ~161 `problem` sub-issues (`set:core|optional|deferred`, `diff:*`); milestones are phases; a user-level GitHub Project ("Interview Prep", #2) mirrors review state.
|
LeetCode interview-prep campaign (8 weeks, 2026-08-17 → Oct 11) run as a repo: solution files under `work/`, a Blume docs site under `apps/docs/`, GitHub Actions that reconcile issues and publish docs, and a Cloudflare Worker (`apps/api/`) that runs the spaced-repetition system (SRS) — daily digest email, one-tap logging, weekly review issues, live charts. Issues model the curriculum: ~24 `topic` issues (`Topic NN — Name`) own ~161 `problem` sub-issues titled `LC <num> · <Name>`, with difficulty and set carried **only** by labels (`set:core|optional|deferred`, `diff:*`) — never by the title; milestones are phases; a user-level GitHub Project ("Interview Prep", #2) mirrors review state.
|
||||||
|
|
||||||
## Architecture & Data Flow
|
## Architecture & Data Flow
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@ flowchart LR
|
|||||||
D1 -->|8 AM ET cron| Mail[digest email + one-tap links]
|
D1 -->|8 AM ET cron| Mail[digest email + one-tap links]
|
||||||
Mail -->|GET /log| D1
|
Mail -->|GET /log| D1
|
||||||
PI -->|/done webhook| D1
|
PI -->|/done webhook| D1
|
||||||
D1 -->|best-effort mirror| Proj[GitHub Project fields]
|
D1 -->|best-effort mirror: Target Date only| Proj[GitHub Project fields]
|
||||||
|
GH -->|close-solved.yml sync-project-fields| Proj
|
||||||
D1 -->|Sat midnight ET| Review[Review — Week N issue]
|
D1 -->|Sat midnight ET| Review[Review — Week N issue]
|
||||||
D1 --> Charts[SVG charts + /api/stats]
|
D1 --> Charts[SVG charts + /api/stats]
|
||||||
```
|
```
|
||||||
@@ -26,7 +27,7 @@ flowchart LR
|
|||||||
- **Reconcile, don't react.** `close-solved.ts`, `close-topics.ts`, and the Worker's catalog sync recompute desired state from scratch each run: re-runs are no-ops, backfills need no special casing, closing is one-directional. `close-solved` posts its *whole* implemented set to `/admin/solved`, not just the issues it closed this run.
|
- **Reconcile, don't react.** `close-solved.ts`, `close-topics.ts`, and the Worker's catalog sync recompute desired state from scratch each run: re-runs are no-ops, backfills need no special casing, closing is one-directional. `close-solved` posts its *whole* implemented set to `/admin/solved`, not just the issues it closed this run.
|
||||||
- **Solving has three doors, one write path.** A digest one-tap, a `/done` comment, and a solution landing in `work/` all end in `logAttempt()`. Closing an issue is *not* one of them: the catalog reconcile never reads issue state, so a `work/` push that skipped the email would otherwise leave `stage='new'` — invisible to the charts, the digest's solved ticks, and the drill/gate pools. Hence `/admin/solved` (`source='commit'`, first solve only, no index needed for idempotency: it refuses any problem past stage `new`). `close-topics` needs no such call — D1 stores no topic completion, only problem rows.
|
- **Solving has three doors, one write path.** A digest one-tap, a `/done` comment, and a solution landing in `work/` all end in `logAttempt()`. Closing an issue is *not* one of them: the catalog reconcile never reads issue state, so a `work/` push that skipped the email would otherwise leave `stage='new'` — invisible to the charts, the digest's solved ticks, and the drill/gate pools. Hence `/admin/solved` (`source='commit'`, first solve only, no index needed for idempotency: it refuses any problem past stage `new`). `close-topics` needs no such call — D1 stores no topic completion, only problem rows.
|
||||||
- **The README charts are live, never committed.** They are Worker endpoints reading D1 per request (`Cache-Control: max-age=300`, honored by GitHub Camo), so a solved push moves them within ~5 minutes with no commit and no workflow of their own.
|
- **The README charts are live, never committed.** They are Worker endpoints reading D1 per request (`Cache-Control: max-age=300`, honored by GitHub Camo), so a solved push moves them within ~5 minutes with no commit and no workflow of their own.
|
||||||
- **D1 owns SRS state; GitHub issues own the catalog; `apps/api/data/schedule.json` owns the calendar.** The catalog reconcile never invents rows and never overwrites SRS columns (`stage`, `next_review`). Project fields (`Target Date`, `SRS Stage`, `First Attempt`) are a best-effort mirror (`apps/api/src/mirror.ts`): failures are warnings, never lost D1 writes; topic rows' `Target Date` is never written.
|
- **D1 owns SRS state; GitHub issues own the catalog; `apps/api/data/schedule.json` owns the calendar.** The catalog reconcile never invents rows and never overwrites SRS columns (`stage`, `next_review`). `Target Date` is the *only* Project field the Worker mirrors (`apps/api/src/mirror.ts`): failures are warnings, never lost D1 writes; topic rows' `Target Date` is never written. SRS stage and first-attempt result live **only** in D1 — do not re-add them as Project fields. The Project's `Set`/`Difficulty` single-selects are a projection of the issue labels, reconciled by `bun run sync-project-fields` — a best-effort final step of `close-solved.yml`, never the Worker.
|
||||||
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `apps/api/src/srs.ts`, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date in `email_log`; one-tap links are unique on (problem, date, kind).
|
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `apps/api/src/srs.ts`, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date in `email_log`; one-tap links are unique on (problem, date, kind).
|
||||||
- **DST-proof crons:** each event has two UTC crons; code fires only on the computed ET hour (`etHour`), unit-checked in `apps/api/src/srs.test.ts`.
|
- **DST-proof crons:** each event has two UTC crons; code fires only on the computed ET hour (`etHour`), unit-checked in `apps/api/src/srs.test.ts`.
|
||||||
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
|
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
|
||||||
@@ -39,7 +40,7 @@ flowchart LR
|
|||||||
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
|
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
|
||||||
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `islands/`, `public/`). `content/(<category>)/<num>-<slug>.mdx` generated by sync |
|
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `islands/`, `public/`). `content/(<category>)/<num>-<slug>.mdx` generated by sync |
|
||||||
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (day → topic issue, human-edited, bundled at deploy), `migrations/`, `scripts/import-srs.ts` |
|
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (day → topic issue, human-edited, bundled at deploy), `migrations/`, `scripts/import-srs.ts` |
|
||||||
| `.github/workflows/` | deploy (docs → Pages, Worker → Cloudflare, on every main push), close-solved (issues + `/admin/solved`, needs `SRS_ADMIN_KEY`), close-topics, sync-d1 (issue edits → `/admin/reconcile`) |
|
| `.github/workflows/` | deploy (docs → Pages, Worker → Cloudflare, on every main push), close-solved (issues + `/admin/solved` + Project `Set`/`Difficulty`, needs `SRS_ADMIN_KEY` and `PROJECT_PAT`), close-topics, sync-d1 (issue edits → `/admin/reconcile`) |
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
@@ -49,8 +50,10 @@ All from the repo root (a Bun workspace over `apps/*`):
|
|||||||
bun run pick # scaffold a solution via leetcode-cli (fuzzy picker; hides problems
|
bun run pick # scaffold a solution via leetcode-cli (fuzzy picker; hides problems
|
||||||
# already under work/, caches index+descriptions in
|
# already under work/, caches index+descriptions in
|
||||||
# ~/.local/share/leetcode/data.db — see apps/cli/db.ts)
|
# ~/.local/share/leetcode/data.db — see apps/cli/db.ts)
|
||||||
bun run test # run ONE solution against LeetCode's judge (not a test suite)
|
bun run test # run ONE solution against LeetCode's judge (not a test suite;
|
||||||
bun run submit # submit ONE solution to LeetCode (same picker as test)
|
# most recently tested first, stamped in the db on exit 0)
|
||||||
|
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing
|
||||||
|
# hidden; last_submitted stamped in the db on exit 0)
|
||||||
bun run sync # work/ → apps/docs/content/ pages
|
bun run sync # work/ → apps/docs/content/ pages
|
||||||
bun run dev|build # Blume docs site (runs in apps/docs/)
|
bun run dev|build # Blume docs site (runs in apps/docs/)
|
||||||
bun run close-solved -- --dry-run # issue reconcilers (also DRY_RUN=1)
|
bun run close-solved -- --dry-run # issue reconcilers (also DRY_RUN=1)
|
||||||
@@ -58,6 +61,9 @@ bun run close-topics -- --dry-run
|
|||||||
# close-solved also pushes the solved set to the Worker; point it at a local
|
# close-solved also pushes the solved set to the Worker; point it at a local
|
||||||
# one and nothing production is touched:
|
# one and nothing production is touched:
|
||||||
SRS_API=http://localhost:8787 SRS_ADMIN_KEY=$LINK_KEY bun run close-solved -- --dry-run
|
SRS_API=http://localhost:8787 SRS_ADMIN_KEY=$LINK_KEY bun run close-solved -- --dry-run
|
||||||
|
# Project Set/Difficulty from the issue labels (needs a `project`-scoped token;
|
||||||
|
# runs as the last step of close-solved.yml in CI):
|
||||||
|
GH_TOKEN=$PROJECT_PAT bun run sync-project-fields -- --dry-run
|
||||||
bun run api:dev # wrangler dev on :8787 (local D1); api:test = DST guard tests
|
bun run api:dev # wrangler dev on :8787 (local D1); api:test = DST guard tests
|
||||||
bun run api:deploy # deploy the Worker by hand (CI also deploys on main pushes)
|
bun run api:deploy # deploy the Worker by hand (CI also deploys on main pushes)
|
||||||
curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
||||||
|
|||||||
+7
-3
@@ -16,9 +16,13 @@ Live at `https://srs-api.prdlk.workers.dev`.
|
|||||||
never overwrites SRS-owned columns (`stage`, `next_review`).
|
never overwrites SRS-owned columns (`stage`, `next_review`).
|
||||||
- **The repo owns the schedule** — `data/schedule.json`, human-edited,
|
- **The repo owns the schedule** — `data/schedule.json`, human-edited,
|
||||||
bundled at deploy. Git history is its audit log.
|
bundled at deploy. Git history is its audit log.
|
||||||
- The GitHub Project mirror (`Target Date`, `SRS Stage`, `First Attempt`)
|
- The GitHub Project mirror writes `Target Date` and nothing else: it is
|
||||||
is best-effort: failures log warnings, never block a D1 write. Topic rows
|
best-effort, so failures log warnings and never block a D1 write, and topic
|
||||||
are never written.
|
rows are never written. SRS stage and first-attempt result stay in D1 only —
|
||||||
|
the charts, digest and `/api/stats` read them from there, so mirroring them
|
||||||
|
into Project single-selects bought only drift. Do not re-add them.
|
||||||
|
`Set`/`Difficulty` are a projection of the issue labels, reconciled by
|
||||||
|
`bun run sync-project-fields` at the repo root, not by this Worker.
|
||||||
|
|
||||||
## The ladder
|
## The ladder
|
||||||
|
|
||||||
|
|||||||
+34
-4
@@ -4,11 +4,19 @@
|
|||||||
* rows from scratch on every run — re-runs are no-ops. SRS-owned columns
|
* rows from scratch on every run — re-runs are no-ops. SRS-owned columns
|
||||||
* (stage, next_review) are NEVER overwritten; defer_until is set once, on
|
* (stage, next_review) are NEVER overwritten; defer_until is set once, on
|
||||||
* first sight of a deferred problem. The Worker never invents catalog rows.
|
* first sight of a deferred problem. The Worker never invents catalog rows.
|
||||||
|
*
|
||||||
|
* Difficulty and set come from the `diff:*` / `set:*` LABELS, never from the
|
||||||
|
* issue title — the title carries only the LC number and the problem's name,
|
||||||
|
* so relabelling a problem is enough to move it and titles stay readable.
|
||||||
|
* The `problem` label is what marks a sub-issue as curriculum.
|
||||||
*/
|
*/
|
||||||
import { addDays } from "./srs.ts";
|
import { addDays } from "./srs.ts";
|
||||||
import type { GitHub } from "./github.ts";
|
import type { GitHub } from "./github.ts";
|
||||||
|
|
||||||
const TITLE_RE = /^LC (\d+) · (.+) · (Easy|Medium|Hard) · (core|optional|deferred)$/;
|
const TITLE_RE = /^LC (\d+) · (.+)$/;
|
||||||
|
|
||||||
|
const DIFFICULTIES = ["easy", "medium", "hard"] as const;
|
||||||
|
const SETS = ["core", "optional", "deferred"] as const;
|
||||||
|
|
||||||
/** Deferred Hards enter the queue from this date, two per day. */
|
/** Deferred Hards enter the queue from this date, two per day. */
|
||||||
const DEFER_FROM = "2026-09-28";
|
const DEFER_FROM = "2026-09-28";
|
||||||
@@ -18,6 +26,18 @@ export interface ReconcileReport {
|
|||||||
problems: number;
|
problems: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Label names on one issue payload, narrowed with guards rather than cast. */
|
||||||
|
function labelNames(value: object): string[] {
|
||||||
|
if (!("labels" in value) || !Array.isArray(value.labels)) return [];
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const label of value.labels) {
|
||||||
|
if (label && typeof label === "object" && "name" in label && typeof label.name === "string") {
|
||||||
|
names.push(label.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
export async function reconcileCatalog(db: D1Database, gh: GitHub): Promise<ReconcileReport> {
|
export async function reconcileCatalog(db: D1Database, gh: GitHub): Promise<ReconcileReport> {
|
||||||
interface TopicIssue {
|
interface TopicIssue {
|
||||||
number: number;
|
number: number;
|
||||||
@@ -62,10 +82,20 @@ export async function reconcileCatalog(db: D1Database, gh: GitHub): Promise<Reco
|
|||||||
if (!raw || typeof raw !== "object") continue;
|
if (!raw || typeof raw !== "object") continue;
|
||||||
if (!("number" in raw) || typeof raw.number !== "number") continue;
|
if (!("number" in raw) || typeof raw.number !== "number") continue;
|
||||||
if (!("title" in raw) || typeof raw.title !== "string") continue;
|
if (!("title" in raw) || typeof raw.title !== "string") continue;
|
||||||
|
const labels = labelNames(raw);
|
||||||
|
if (!labels.includes("problem")) continue; // non-curriculum sub-issue
|
||||||
const m = raw.title.match(TITLE_RE);
|
const m = raw.title.match(TITLE_RE);
|
||||||
if (!m) continue; // non-curriculum sub-issue
|
if (!m) {
|
||||||
|
console.warn(`catalog: #${raw.number} is a problem but not titled "LC <n> · <name>"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const difficulty = DIFFICULTIES.find((d) => labels.includes(`diff:${d}`));
|
||||||
|
const set = SETS.find((s) => labels.includes(`set:${s}`));
|
||||||
|
if (!difficulty || !set) {
|
||||||
|
console.warn(`catalog: #${raw.number} is missing a diff:/set: label — skipped`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const lc = Number(m[1]);
|
const lc = Number(m[1]);
|
||||||
const set = m[4]!;
|
|
||||||
// Two deferred Hards per day from DEFER_FROM, in catalog walk order —
|
// Two deferred Hards per day from DEFER_FROM, in catalog walk order —
|
||||||
// applied only when the row is first created (SRS owns it afterwards).
|
// applied only when the row is first created (SRS owns it afterwards).
|
||||||
const defer = set === "deferred" ? addDays(DEFER_FROM, Math.floor(deferredSeen / 2)) : null;
|
const defer = set === "deferred" ? addDays(DEFER_FROM, Math.floor(deferredSeen / 2)) : null;
|
||||||
@@ -78,7 +108,7 @@ export async function reconcileCatalog(db: D1Database, gh: GitHub): Promise<Reco
|
|||||||
ON CONFLICT(lc_number) DO UPDATE SET
|
ON CONFLICT(lc_number) DO UPDATE SET
|
||||||
issue = ?2, topic_issue = ?3, title = ?4, difficulty = ?5, set_label = ?6`,
|
issue = ?2, topic_issue = ?3, title = ?4, difficulty = ?5, set_label = ?6`,
|
||||||
)
|
)
|
||||||
.bind(lc, raw.number, t.number, m[2]!, m[3]!.toLowerCase(), set, defer),
|
.bind(lc, raw.number, t.number, m[2]!, difficulty, set, defer),
|
||||||
);
|
);
|
||||||
problems++;
|
problems++;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-10
@@ -30,22 +30,16 @@ import { buildStats } from "./stats.ts";
|
|||||||
|
|
||||||
// ── shared side effects after a state write ──────────────────────
|
// ── shared side effects after a state write ──────────────────────
|
||||||
|
|
||||||
/** Project-field half of a mirror; shared by every write path. */
|
|
||||||
async function mirrorFields(mirror: Mirror, outcome: LogOutcome): Promise<void> {
|
|
||||||
await mirror.setStage(outcome.issue, outcome.stage);
|
|
||||||
await mirror.setTargetDate(outcome.issue, outcome.next_review);
|
|
||||||
if (outcome.first) await mirror.setFirstAttempt(outcome.issue, outcome.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirror one logged attempt into GitHub: Project fields always; on a
|
* Mirror one logged attempt into GitHub: the Project's Target Date always; on a
|
||||||
* first-ever pass also close the problem's sub-issue (comment first — repo
|
* first-ever pass also close the problem's sub-issue (comment first — repo
|
||||||
* convention). Runs inside ctx.waitUntil; failures are warnings.
|
* convention). Runs inside ctx.waitUntil; failures are warnings.
|
||||||
*/
|
*/
|
||||||
async function mirrorOutcome(env: Env, outcome: LogOutcome, source: string): Promise<void> {
|
async function mirrorOutcome(env: Env, outcome: LogOutcome, source: string): Promise<void> {
|
||||||
if (outcome.error || outcome.duplicate) return;
|
if (outcome.error || outcome.duplicate) return;
|
||||||
const gh = github(env.GH_PAT, env.REPO);
|
const gh = github(env.GH_PAT, env.REPO);
|
||||||
await mirrorFields(projectMirror(gh, env.REPO.split("/")[0]!), outcome);
|
const mirror = projectMirror(gh, env.REPO.split("/")[0]!);
|
||||||
|
await mirror.setTargetDate(outcome.issue, outcome.next_review);
|
||||||
if (outcome.first && outcome.result === "pass") {
|
if (outcome.first && outcome.result === "pass") {
|
||||||
try {
|
try {
|
||||||
await gh.closeIssue(
|
await gh.closeIssue(
|
||||||
@@ -248,7 +242,7 @@ async function handleSolved(request: Request, env: Env, date: string): Promise<R
|
|||||||
logged.push(outcomeLine(outcome));
|
logged.push(outcomeLine(outcome));
|
||||||
// One mirror for the batch: field IDs resolve once, not per problem.
|
// One mirror for the batch: field IDs resolve once, not per problem.
|
||||||
mirror ??= projectMirror(github(env.GH_PAT, env.REPO), env.REPO.split("/")[0]!);
|
mirror ??= projectMirror(github(env.GH_PAT, env.REPO), env.REPO.split("/")[0]!);
|
||||||
await mirrorFields(mirror, outcome);
|
await mirror.setTargetDate(outcome.issue, outcome.next_review);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Response.json({ date, logged, skipped, warnings: mirror?.warnings ?? [] });
|
return Response.json({ date, logged, skipped, warnings: mirror?.warnings ?? [] });
|
||||||
|
|||||||
+10
-39
@@ -4,30 +4,26 @@
|
|||||||
* console warning and never blocks a state write. Only problem issues are
|
* console warning and never blocks a state write. Only problem issues are
|
||||||
* ever passed in, so topic rows' Target Date is never written.
|
* ever passed in, so topic rows' Target Date is never written.
|
||||||
*
|
*
|
||||||
* Field/option IDs are resolved by NAME per invocation (a Worker isolate is
|
* Target Date is the ONLY mirrored column. SRS stage and first-attempt result
|
||||||
* short-lived; the two extra queries per mirror burst are irrelevant at this
|
* live exclusively in D1 — they are read from there (charts, digest, /api/stats)
|
||||||
* volume and survive field re-creation).
|
* and duplicating them into Project single-selects bought nothing but drift and
|
||||||
|
* two extra writes per log. Do not re-add them here.
|
||||||
|
*
|
||||||
|
* Field IDs are resolved by NAME per invocation (a Worker isolate is
|
||||||
|
* short-lived; the extra query per mirror burst is irrelevant at this
|
||||||
|
* volume and survives field re-creation).
|
||||||
*/
|
*/
|
||||||
import type { GitHub } from "./github.ts";
|
import type { GitHub } from "./github.ts";
|
||||||
|
|
||||||
const PROJECT_TITLE = "Interview Prep";
|
const PROJECT_TITLE = "Interview Prep";
|
||||||
|
|
||||||
interface SelectField {
|
|
||||||
id: string;
|
|
||||||
options: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProjectInfo {
|
interface ProjectInfo {
|
||||||
id: string;
|
id: string;
|
||||||
targetDate: string;
|
targetDate: string;
|
||||||
srsStage: SelectField;
|
|
||||||
firstAttempt: SelectField;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Mirror {
|
export interface Mirror {
|
||||||
setTargetDate(issue: number, date: string | null): Promise<void>;
|
setTargetDate(issue: number, date: string | null): Promise<void>;
|
||||||
setStage(issue: number, stage: string): Promise<void>;
|
|
||||||
setFirstAttempt(issue: number, result: string): Promise<void>;
|
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +43,6 @@ export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|||||||
fields(first: 30) {
|
fields(first: 30) {
|
||||||
nodes {
|
nodes {
|
||||||
... on ProjectV2FieldCommon { id name dataType }
|
... on ProjectV2FieldCommon { id name dataType }
|
||||||
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +57,7 @@ export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
fields: {
|
fields: {
|
||||||
nodes: { id: string; name: string; options?: { id: string; name: string }[] }[];
|
nodes: { id: string; name: string }[];
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
@@ -70,19 +65,9 @@ export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|||||||
};
|
};
|
||||||
const node = data.user.projectsV2.nodes.find((n) => n.title === PROJECT_TITLE);
|
const node = data.user.projectsV2.nodes.find((n) => n.title === PROJECT_TITLE);
|
||||||
if (!node) throw new Error(`project "${PROJECT_TITLE}" not found for @${owner}`);
|
if (!node) throw new Error(`project "${PROJECT_TITLE}" not found for @${owner}`);
|
||||||
const select = (name: string): SelectField => {
|
|
||||||
const f = node.fields.nodes.find((n) => n.name === name);
|
|
||||||
if (!f?.options) throw new Error(`single-select field "${name}" missing`);
|
|
||||||
return { id: f.id, options: Object.fromEntries(f.options.map((o) => [o.name, o.id])) };
|
|
||||||
};
|
|
||||||
const date = node.fields.nodes.find((n) => n.name === "Target Date");
|
const date = node.fields.nodes.find((n) => n.name === "Target Date");
|
||||||
if (!date) throw new Error(`date field "Target Date" missing`);
|
if (!date) throw new Error(`date field "Target Date" missing`);
|
||||||
info = {
|
info = { id: node.id, targetDate: date.id };
|
||||||
id: node.id,
|
|
||||||
targetDate: date.id,
|
|
||||||
srsStage: select("SRS Stage"),
|
|
||||||
firstAttempt: select("First Attempt"),
|
|
||||||
};
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,19 +139,5 @@ export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|||||||
await setField(issue, project.targetDate, { date });
|
await setField(issue, project.targetDate, { date });
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
setStage: (issue, stage) =>
|
|
||||||
attempt(`SRS Stage #${issue}`, async () => {
|
|
||||||
const project = await resolve();
|
|
||||||
const option = project.srsStage.options[stage];
|
|
||||||
if (!option) throw new Error(`no option "${stage}"`);
|
|
||||||
await setField(issue, project.srsStage.id, { singleSelectOptionId: option });
|
|
||||||
}),
|
|
||||||
setFirstAttempt: (issue, result) =>
|
|
||||||
attempt(`First Attempt #${issue}`, async () => {
|
|
||||||
const project = await resolve();
|
|
||||||
const option = project.firstAttempt.options[result];
|
|
||||||
if (!option) throw new Error(`no option "${result}"`);
|
|
||||||
await setField(issue, project.firstAttempt.id, { singleSelectOptionId: option });
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
addDays,
|
addDays,
|
||||||
campaignDay,
|
campaignDay,
|
||||||
campaignWeek,
|
campaignWeek,
|
||||||
|
isoWeek,
|
||||||
streak,
|
streak,
|
||||||
} from "./srs.ts";
|
} from "./srs.ts";
|
||||||
|
|
||||||
@@ -61,9 +62,14 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
|||||||
const ladder: Record<string, number> = { new: 0, "+2": 0, "+5": 0, "+10": 0, retired: 0 };
|
const ladder: Record<string, number> = { new: 0, "+2": 0, "+5": 0, "+10": 0, retired: 0 };
|
||||||
for (const row of ladderRows) ladder[row.stage] = row.n;
|
for (const row of ladderRows) ladder[row.stage] = row.n;
|
||||||
|
|
||||||
const { results: gates } = await db
|
// gates rows are keyed by ISO week (that's the digest/gate contract), but
|
||||||
|
// the page speaks campaign weeks. The campaign never crosses a year
|
||||||
|
// boundary, so a plain offset converts safely.
|
||||||
|
const isoWeekOffset = isoWeek(CAMPAIGN_START) - 1;
|
||||||
|
const { results: gateRows } = await db
|
||||||
.prepare("SELECT week, issue, pass_rate, closed_on FROM gates ORDER BY week")
|
.prepare("SELECT week, issue, pass_rate, closed_on FROM gates ORDER BY week")
|
||||||
.all<{ week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }>();
|
.all<{ week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }>();
|
||||||
|
const gates = gateRows.map((g) => ({ ...g, week: g.week - isoWeekOffset }));
|
||||||
|
|
||||||
// Review-queue depth for the next 14 days: everything due by that day.
|
// Review-queue depth for the next 14 days: everything due by that day.
|
||||||
const queue: { date: string; due: number }[] = [];
|
const queue: { date: string; due: number }[] = [];
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
* are ever fetched, and paid-only problems are skipped because GraphQL
|
* are ever fetched, and paid-only problems are skipped because GraphQL
|
||||||
* returns null content for them.
|
* returns null content for them.
|
||||||
*
|
*
|
||||||
|
* A `solutions` table tracks the lifecycle of each work/ file by LC number:
|
||||||
|
* `created_at` (backfilled from the file's birthtime on every picker run),
|
||||||
|
* `last_tested` and `last_submitted` (stamped on exit 0 of leetcode-cli).
|
||||||
|
* The test picker sorts by last_tested, the submit picker by created_at, and
|
||||||
|
* submit hides anything with a last_submitted.
|
||||||
|
*
|
||||||
* Library: side-effect-free on import — nothing is opened or fetched until
|
* Library: side-effect-free on import — nothing is opened or fetched until
|
||||||
* problemDb() is called. Index upserts never touch the description column, so
|
* problemDb() is called. Index upserts never touch the description column, so
|
||||||
* a daily refresh cannot evict cached statements.
|
* a daily refresh cannot evict cached statements.
|
||||||
@@ -27,6 +33,13 @@ export interface Problem {
|
|||||||
paidOnly: boolean;
|
paidOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lifecycle of one work/ solution, keyed by LC number. ISO timestamps. */
|
||||||
|
export interface SolutionActivity {
|
||||||
|
createdAt: string;
|
||||||
|
lastTested: string | null;
|
||||||
|
lastSubmitted: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
const DIFFICULTY = ["", "Easy", "Medium", "Hard"] as const;
|
const DIFFICULTY = ["", "Easy", "Medium", "Hard"] as const;
|
||||||
const INDEX_TTL_MS = 24 * 60 * 60 * 1000;
|
const INDEX_TTL_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
@@ -109,6 +122,13 @@ export function problemDb(path = DB_PATH) {
|
|||||||
paid_only INTEGER NOT NULL,
|
paid_only INTEGER NOT NULL,
|
||||||
description TEXT
|
description TEXT
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS solutions (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
last_tested TEXT,
|
||||||
|
last_submitted TEXT
|
||||||
|
);
|
||||||
|
DROP TABLE IF EXISTS submissions;
|
||||||
CREATE TABLE IF NOT EXISTS meta (
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
@@ -177,6 +197,37 @@ export function problemDb(path = DB_PATH) {
|
|||||||
db.query("UPDATE problems SET description = ?2 WHERE slug = ?1").run(slug, html);
|
db.query("UPDATE problems SET description = ?2 WHERE slug = ?1").run(slug, html);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Per-solution lifecycle rows keyed by LC number. */
|
||||||
|
solutionActivity(): Map<number, SolutionActivity> {
|
||||||
|
const rows = db
|
||||||
|
.query<{ id: number; created_at: string; last_tested: string | null; last_submitted: string | null }, []>(
|
||||||
|
"SELECT id, created_at, last_tested, last_submitted FROM solutions",
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
return new Map(
|
||||||
|
rows.map((r) => [r.id, { createdAt: r.created_at, lastTested: r.last_tested, lastSubmitted: r.last_submitted }]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Register a solution's creation time; existing rows are left alone. */
|
||||||
|
ensureSolution(id: number, createdAt: string): void {
|
||||||
|
db.query("INSERT INTO solutions (id, created_at) VALUES (?1, ?2) ON CONFLICT(id) DO NOTHING").run(id, createdAt);
|
||||||
|
},
|
||||||
|
|
||||||
|
markTested(id: number): void {
|
||||||
|
db.query(
|
||||||
|
`INSERT INTO solutions (id, created_at, last_tested) VALUES (?1, ?2, ?2)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET last_tested = excluded.last_tested`,
|
||||||
|
).run(id, new Date().toISOString());
|
||||||
|
},
|
||||||
|
|
||||||
|
markSubmitted(id: number): void {
|
||||||
|
db.query(
|
||||||
|
`INSERT INTO solutions (id, created_at, last_submitted) VALUES (?1, ?2, ?2)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET last_submitted = excluded.last_submitted`,
|
||||||
|
).run(id, new Date().toISOString());
|
||||||
|
},
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
db.close();
|
db.close();
|
||||||
},
|
},
|
||||||
|
|||||||
+23
-3
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* GitHub REST plumbing shared by the issue reconcilers (close-solved,
|
* GitHub REST + GraphQL plumbing shared by the issue reconcilers (close-solved,
|
||||||
* close-topics).
|
* close-topics) and the Project field reconciler (sync-project-fields).
|
||||||
*
|
*
|
||||||
* Importing this module is side-effect free — nothing resolves credentials or
|
* Importing this module is side-effect free — nothing resolves credentials or
|
||||||
* touches the network until github() is awaited — so a script can import it
|
* touches the network until github() is awaited — so a script can import it
|
||||||
@@ -22,6 +22,8 @@ export interface GitHub {
|
|||||||
repo: string;
|
repo: string;
|
||||||
/** One authenticated request against api.github.com; throws on non-2xx. */
|
/** One authenticated request against api.github.com; throws on non-2xx. */
|
||||||
api(path: string, init?: RequestInit): Promise<unknown>;
|
api(path: string, init?: RequestInit): Promise<unknown>;
|
||||||
|
/** One GraphQL call (Projects v2 lives here); throws on transport or query errors. */
|
||||||
|
graphql(query: string, variables?: Record<string, unknown>): Promise<unknown>;
|
||||||
/** Every page of a list endpoint, flattened into one stream of elements. */
|
/** Every page of a list endpoint, flattened into one stream of elements. */
|
||||||
list(path: string): AsyncGenerator<unknown, void, void>;
|
list(path: string): AsyncGenerator<unknown, void, void>;
|
||||||
/** Comment on an issue, then close it as completed. */
|
/** Comment on an issue, then close it as completed. */
|
||||||
@@ -74,6 +76,24 @@ export async function github(): Promise<GitHub> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function graphql(
|
||||||
|
query: string,
|
||||||
|
variables: Record<string, unknown> = {},
|
||||||
|
): Promise<unknown> {
|
||||||
|
const res = await fetch("https://api.github.com/graphql", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${token}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query, variables }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`graphql -> ${res.status} ${await res.text()}`);
|
||||||
|
const payload = (await res.json()) as { data?: unknown; errors?: { message: string }[] };
|
||||||
|
if (payload.errors?.length) throw new Error(payload.errors.map((e) => e.message).join("; "));
|
||||||
|
return payload.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comment before closing: if the PATCH fails, the issue still carries a
|
* Comment before closing: if the PATCH fails, the issue still carries a
|
||||||
* visible note of what the automation decided, instead of failing silently.
|
* visible note of what the automation decided, instead of failing silently.
|
||||||
@@ -89,5 +109,5 @@ export async function github(): Promise<GitHub> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { repo, api, list, closeIssue };
|
return { repo, api, graphql, list, closeIssue };
|
||||||
}
|
}
|
||||||
+59
-6
@@ -1,18 +1,64 @@
|
|||||||
/**
|
/**
|
||||||
* Shared work/ solution picker for the test and submit entries: fuzzy-select
|
* Shared work/ solution picker for the test and submit entries: fuzzy-select
|
||||||
* a solution file, then hand it to leetcode-cli. Library: side-effect-free on
|
* a solution file, then hand it to leetcode-cli.
|
||||||
* import — nothing scans the filesystem until runOnSolution() is called.
|
*
|
||||||
|
* Every run backfills the db's solutions table with created_at (from the
|
||||||
|
* file's birthtime, mtime when the filesystem has none) so ordering works for
|
||||||
|
* files that predate the db. Ordering is per command — test lists the most
|
||||||
|
* recently tested first (never-tested after, by name); submit lists the most
|
||||||
|
* recently created first. Nothing is ever hidden. Exit 0 of leetcode-cli
|
||||||
|
* stamps last_tested / last_submitted.
|
||||||
|
*
|
||||||
|
* Library: side-effect-free on import — nothing scans the filesystem until
|
||||||
|
* runOnSolution() is called.
|
||||||
*/
|
*/
|
||||||
import { autocomplete, cancel, isCancel, log } from "@clack/prompts";
|
import { autocomplete, cancel, isCancel, log } from "@clack/prompts";
|
||||||
import { join, relative } from "node:path";
|
import { statSync } from "node:fs";
|
||||||
|
import { basename, join, relative } from "node:path";
|
||||||
|
import { problemDb } from "./db";
|
||||||
|
|
||||||
const ROOT = join(import.meta.dir, "..", "..");
|
const ROOT = join(import.meta.dir, "..", "..");
|
||||||
const WORK_DIR = join(ROOT, "work");
|
const WORK_DIR = join(ROOT, "work");
|
||||||
|
|
||||||
|
/** Leading LC number of a work/ file, or undefined for unnumbered names. */
|
||||||
|
function lcNumber(rel: string): number | undefined {
|
||||||
|
const m = basename(rel).match(/^(\d+)\./);
|
||||||
|
return m ? Number(m[1]) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** Fuzzy-pick a solution under work/ and run `leetcode <command>` on it. */
|
/** Fuzzy-pick a solution under work/ and run `leetcode <command>` on it. */
|
||||||
export async function runOnSolution(command: "test" | "submit", message: string): Promise<never> {
|
export async function runOnSolution(command: "test" | "submit", message: string): Promise<never> {
|
||||||
const files = [...new Bun.Glob("**/*.{js,ts,py,java,c,cpp,go,rs,rb,swift,kt,cs}").scanSync({ cwd: WORK_DIR })]
|
const db = problemDb();
|
||||||
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
||||||
|
const scanned = [...new Bun.Glob("**/*.{js,ts,py,java,c,cpp,go,rs,rb,swift,kt,cs}").scanSync({ cwd: WORK_DIR })];
|
||||||
|
|
||||||
|
// register creation times so files older than the db still sort correctly
|
||||||
|
for (const f of scanned) {
|
||||||
|
const num = lcNumber(f);
|
||||||
|
if (num === undefined) continue;
|
||||||
|
const stat = statSync(join(WORK_DIR, f));
|
||||||
|
db.ensureSolution(num, new Date(stat.birthtimeMs || stat.mtimeMs).toISOString());
|
||||||
|
}
|
||||||
|
const activity = db.solutionActivity();
|
||||||
|
|
||||||
|
// test → most recently tested first; submit → most recently created first
|
||||||
|
const sortKey = (f: string) => {
|
||||||
|
const num = lcNumber(f);
|
||||||
|
const row = num === undefined ? undefined : activity.get(num);
|
||||||
|
return (command === "test" ? row?.lastTested : row?.createdAt) ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const files = scanned
|
||||||
|
.sort((a, b) => {
|
||||||
|
const ka = sortKey(a);
|
||||||
|
const kb = sortKey(b);
|
||||||
|
if (ka !== kb) {
|
||||||
|
if (ka === undefined) return 1;
|
||||||
|
if (kb === undefined) return -1;
|
||||||
|
return kb.localeCompare(ka); // ISO timestamps: lexicographic = chronological
|
||||||
|
}
|
||||||
|
return a.localeCompare(b, undefined, { numeric: true });
|
||||||
|
});
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
log.error(`No solution files found in ${relative(process.cwd(), WORK_DIR)}/`);
|
log.error(`No solution files found in ${relative(process.cwd(), WORK_DIR)}/`);
|
||||||
@@ -55,5 +101,12 @@ export async function runOnSolution(command: "test" | "submit", message: string)
|
|||||||
],
|
],
|
||||||
{ cwd: ROOT, stdio: ["inherit", "inherit", "inherit"] },
|
{ cwd: ROOT, stdio: ["inherit", "inherit", "inherit"] },
|
||||||
);
|
);
|
||||||
process.exit(await proc.exited);
|
const code = await proc.exited;
|
||||||
|
const num = lcNumber(picked);
|
||||||
|
if (code === 0 && num !== undefined) {
|
||||||
|
if (command === "test") db.markTested(num);
|
||||||
|
else db.markSubmitted(num);
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
process.exit(code);
|
||||||
}
|
}
|
||||||
Executable
+252
@@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Reconcile the "Interview Prep" Project's Set and Difficulty fields from the
|
||||||
|
* labels on each problem issue.
|
||||||
|
*
|
||||||
|
* The issue labels are the truth: `set:core|optional|deferred` and
|
||||||
|
* `diff:easy|medium|hard` are fixed when a problem issue is filed, and the
|
||||||
|
* Project's single-selects are a projection of them so the board can group and
|
||||||
|
* filter. This recomputes every desired value from scratch and writes only the
|
||||||
|
* ones that differ, so re-runs are no-ops and a backfill needs no special
|
||||||
|
* casing — same reconcile-don't-react shape as close-solved / close-topics.
|
||||||
|
*
|
||||||
|
* Scope guard: only items whose issue carries the `problem` label are touched.
|
||||||
|
* Topic, cadence and review issues have no set or difficulty, so they are
|
||||||
|
* skipped outright — never written, never cleared.
|
||||||
|
*
|
||||||
|
* Deliberately NOT mirrored: SRS stage and first-attempt result. Those live
|
||||||
|
* only in D1 (apps/api owns them; the charts, digest and /api/stats read them
|
||||||
|
* from there). The Project mirror writes Target Date and nothing else.
|
||||||
|
*
|
||||||
|
* Auth: needs a token carrying the `project` scope — neither the default
|
||||||
|
* `gh auth token` nor Actions' GITHUB_TOKEN has it. Pass one in GH_TOKEN: the
|
||||||
|
* PROJECT_PAT secret in CI, or the Worker's GH_PAT locally:
|
||||||
|
* GH_TOKEN=$GH_PAT bun run sync-project-fields -- --dry-run
|
||||||
|
*/
|
||||||
|
import { github, type GitHub } from "./github.ts";
|
||||||
|
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
||||||
|
|
||||||
|
const PROJECT_TITLE = "Interview Prep";
|
||||||
|
|
||||||
|
/** Label → single-select option name, per field. Static tables, so Record. */
|
||||||
|
const FIELDS: Record<string, Record<string, string>> = {
|
||||||
|
Set: { "set:core": "Core", "set:optional": "Optional", "set:deferred": "Deferred" },
|
||||||
|
Difficulty: { "diff:easy": "Easy", "diff:medium": "Medium", "diff:hard": "Hard" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const DRY =
|
||||||
|
process.argv.includes("--dry-run") ||
|
||||||
|
process.env.DRY_RUN === "1" ||
|
||||||
|
process.env.DRY_RUN === "true";
|
||||||
|
|
||||||
|
// ── project + field resolution ───────────────────────────────────
|
||||||
|
|
||||||
|
interface SelectField {
|
||||||
|
id: string;
|
||||||
|
/** option name → option id */
|
||||||
|
options: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
id: string;
|
||||||
|
fields: Record<string, SelectField>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field and option IDs are resolved by NAME, so re-creating a field in the UI
|
||||||
|
* does not strand this script on a dead id.
|
||||||
|
*/
|
||||||
|
async function readProject(gh: GitHub, owner: string): Promise<Project> {
|
||||||
|
const data = (await gh.graphql(
|
||||||
|
`query($owner: String!, $title: String!) {
|
||||||
|
user(login: $owner) {
|
||||||
|
projectsV2(first: 10, query: $title) {
|
||||||
|
nodes {
|
||||||
|
id title
|
||||||
|
fields(first: 30) {
|
||||||
|
nodes {
|
||||||
|
... on ProjectV2SingleSelectField { id name options { id name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ owner, title: PROJECT_TITLE },
|
||||||
|
)) as {
|
||||||
|
user: {
|
||||||
|
projectsV2: {
|
||||||
|
nodes: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
fields: { nodes: ({ id: string; name: string; options: { id: string; name: string }[] } | Record<string, never>)[] };
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const node = data.user.projectsV2.nodes.find((n) => n.title === PROJECT_TITLE);
|
||||||
|
if (!node) throw new Error(`project "${PROJECT_TITLE}" not found for @${owner}`);
|
||||||
|
|
||||||
|
const fields: Record<string, SelectField> = {};
|
||||||
|
for (const name of Object.keys(FIELDS)) {
|
||||||
|
const field = node.fields.nodes.find((f) => "name" in f && f.name === name);
|
||||||
|
if (!field || !("options" in field)) {
|
||||||
|
throw new Error(`single-select field "${name}" missing from "${PROJECT_TITLE}"`);
|
||||||
|
}
|
||||||
|
fields[name] = {
|
||||||
|
id: field.id,
|
||||||
|
options: Object.fromEntries(field.options.map((o) => [o.name, o.id])),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { id: node.id, fields };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── items ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
id: string;
|
||||||
|
issue: number;
|
||||||
|
title: string;
|
||||||
|
labels: string[];
|
||||||
|
/** field name → current option name */
|
||||||
|
current: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* readItems(gh: GitHub, project: string): AsyncGenerator<Item, void, void> {
|
||||||
|
let cursor: string | null = null;
|
||||||
|
for (;;) {
|
||||||
|
const data = (await gh.graphql(
|
||||||
|
`query($id: ID!, $after: String) {
|
||||||
|
node(id: $id) {
|
||||||
|
... on ProjectV2 {
|
||||||
|
items(first: 100, after: $after) {
|
||||||
|
pageInfo { hasNextPage endCursor }
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
fieldValues(first: 25) {
|
||||||
|
nodes {
|
||||||
|
... on ProjectV2ItemFieldSingleSelectValue {
|
||||||
|
name field { ... on ProjectV2FieldCommon { name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content {
|
||||||
|
... on Issue { number title labels(first: 25) { nodes { name } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ id: project, after: cursor },
|
||||||
|
)) as {
|
||||||
|
node: {
|
||||||
|
items: {
|
||||||
|
pageInfo: { hasNextPage: boolean; endCursor: string };
|
||||||
|
nodes: {
|
||||||
|
id: string;
|
||||||
|
fieldValues: { nodes: ({ name: string; field: { name: string } } | Record<string, never>)[] };
|
||||||
|
content: { number: number; title: string; labels: { nodes: { name: string }[] } } | Record<string, never>;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const node of data.node.items.nodes) {
|
||||||
|
const content = node.content;
|
||||||
|
// Draft items and PRs have no issue number; nothing to reconcile.
|
||||||
|
if (!("number" in content) || typeof content.number !== "number") continue;
|
||||||
|
const current: Record<string, string> = {};
|
||||||
|
for (const value of node.fieldValues.nodes) {
|
||||||
|
if ("name" in value && value.field?.name) current[value.field.name] = value.name;
|
||||||
|
}
|
||||||
|
yield {
|
||||||
|
id: node.id,
|
||||||
|
issue: content.number,
|
||||||
|
title: content.title,
|
||||||
|
labels: content.labels.nodes.map((l) => l.name),
|
||||||
|
current,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = data.node.items.pageInfo;
|
||||||
|
if (!page.hasNextPage) return;
|
||||||
|
cursor = page.endCursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── reconcile ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface Write {
|
||||||
|
item: string;
|
||||||
|
issue: number;
|
||||||
|
title: string;
|
||||||
|
field: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setField(gh: GitHub, project: string, w: Write, option: string): Promise<void> {
|
||||||
|
await gh.graphql(
|
||||||
|
`mutation($project: ID!, $item: ID!, $field: ID!, $option: String!) {
|
||||||
|
updateProjectV2ItemFieldValue(
|
||||||
|
input: {
|
||||||
|
projectId: $project, itemId: $item, fieldId: $field
|
||||||
|
value: { singleSelectOptionId: $option }
|
||||||
|
}
|
||||||
|
) { projectV2Item { id } }
|
||||||
|
}`,
|
||||||
|
{ project, item: w.item, field: w.field, option },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gh = await github();
|
||||||
|
const owner = gh.repo.split("/")[0]!;
|
||||||
|
const project = await readProject(gh, owner);
|
||||||
|
|
||||||
|
const writes: Write[] = [];
|
||||||
|
let problems = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for await (const item of readItems(gh, project.id)) {
|
||||||
|
if (!item.labels.includes("problem")) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
problems++;
|
||||||
|
for (const [field, mapping] of Object.entries(FIELDS)) {
|
||||||
|
const want = item.labels.map((l) => mapping[l]).find(Boolean);
|
||||||
|
if (!want) {
|
||||||
|
console.warn(`#${item.issue} has no ${field.toLowerCase()} label — left untouched`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const from = item.current[field];
|
||||||
|
if (from === want) continue;
|
||||||
|
if (!project.fields[field]!.options[want]) {
|
||||||
|
throw new Error(`field "${field}" has no option "${want}"`);
|
||||||
|
}
|
||||||
|
writes.push({
|
||||||
|
item: item.id,
|
||||||
|
issue: item.issue,
|
||||||
|
title: item.title,
|
||||||
|
field: project.fields[field]!.id,
|
||||||
|
from: from ?? "(empty)",
|
||||||
|
to: want,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: string[][] = [["ISSUE", "PROBLEM", "FIELD", "FROM", "TO"]];
|
||||||
|
for (const w of writes) {
|
||||||
|
const name = Object.entries(project.fields).find(([, f]) => f.id === w.field)![0];
|
||||||
|
rows.push([`#${w.issue}`, w.title.slice(0, 46), name, w.from, w.to]);
|
||||||
|
if (DRY) continue;
|
||||||
|
await setField(gh, project.id, w, project.fields[name]!.options[w.to]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writes.length) printTable(rows);
|
||||||
|
console.log(
|
||||||
|
`\n${DRY ? "would update" : "updated"} ${writes.length} field value(s) across ${problems} problem item(s); ${skipped} non-problem item(s) skipped`,
|
||||||
|
);
|
||||||
|
if (writes.length) await writeStepSummary(markdownTable(rows));
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
title: '150. Evaluate Reverse Polish Notation'
|
||||||
|
description: You are given an array of strings tokens that represents an arithmetic expression in a Reverse Polish Notation
|
||||||
|
sidebar:
|
||||||
|
label: 'Evaluate Reverse Polish Notation'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Stack</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `tokens = ["2","1","+","3","*"]`
|
||||||
|
- Output: `9`
|
||||||
|
- Explanation: `((2 + 1`) * `3`) = `9`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `tokens = ["4","13","5","/","+"]`
|
||||||
|
- Output: `6`
|
||||||
|
- Explanation: `(4 + (13 / 5`)) = `6`
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]`
|
||||||
|
- Output: `22`
|
||||||
|
- Explanation: `((10 * (6 / ((9 + 3`) * `-11`))) + `17`) + `5 = ((10 * (6 / (12 * -11`))) + `17`) + `5 = ((10 * (6 / -132`)) + `17`) + `5 = ((10 * 0`) + `17`) + `5 = (0 + 17`) + `5 = 17 + 5 = 22`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `1 <= tokens.length <= 10^4`
|
||||||
|
- `tokens[i]` is either an operator: "+", "-", "*", or "/", or an integer in the range [-200, 200].
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def evalRPN(self, tokens: List[str]) -> int:
|
||||||
|
stack = []
|
||||||
|
operations = {
|
||||||
|
"+": lambda x, y: int(x + y),
|
||||||
|
"-": lambda x, y: int(x - y),
|
||||||
|
"*": lambda x, y: int(x * y),
|
||||||
|
"/": lambda x, y: int(x / y),
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in tokens:
|
||||||
|
if c in operations:
|
||||||
|
y = stack.pop()
|
||||||
|
x = stack.pop()
|
||||||
|
calc_func = operations[c]
|
||||||
|
result = calc_func(x, y)
|
||||||
|
stack.append(result)
|
||||||
|
else:
|
||||||
|
stack.append(int(c))
|
||||||
|
|
||||||
|
return stack[0]
|
||||||
|
```
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
title: '153. Find Minimum in Rotated Sorted Array'
|
||||||
|
description: 'Suppose an array of length n sorted in ascending order is rotated between 1 and n times. For example, the array nums = [0,1,2,4,5,6,7] might become:'
|
||||||
|
sidebar:
|
||||||
|
label: 'Find Minimum in Rotated Sorted Array'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Binary Search</Badge>
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
Notice that rotating an array [a[0], a[1], a[2], ..., a[n-1]] 1 time results in the array [a[n-1], a[0], a[1], a[2], ..., a[n-2]].
|
||||||
|
::::
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `nums = [3,4,5,1,2]`
|
||||||
|
- Output: `1`
|
||||||
|
- Explanation: The original array was `[1,2,3,4,5]` rotated `3` times.
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `nums = [4,5,6,7,0,1,2]`
|
||||||
|
- Output: `0`
|
||||||
|
- Explanation: The original array was `[0,1,2,4,5,6,7]` and it was rotated `4` times.
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `nums = [11,13,15,17]`
|
||||||
|
- Output: `11`
|
||||||
|
- Explanation: The original array was `[11,13,15,17]` and it was rotated `4` times.
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `n == nums.length`
|
||||||
|
- `1 <= n <= 5000`
|
||||||
|
- `-5000 <= nums[i] <= 5000`
|
||||||
|
- All the integers of `nums` are unique.
|
||||||
|
- `nums` is sorted and rotated between 1 and `n` times.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def findMin(self, nums: List[int]) -> int:
|
||||||
|
l, r = 0, len(nums) - 1
|
||||||
|
lowest_index = -1
|
||||||
|
|
||||||
|
while l <= r:
|
||||||
|
m = (l + r) // 2
|
||||||
|
if nums[m] <= nums[-1]:
|
||||||
|
lowest_index = m
|
||||||
|
r = m - 1
|
||||||
|
else:
|
||||||
|
l = m + 1
|
||||||
|
|
||||||
|
return nums[lowest_index]
|
||||||
|
```
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
title: '189. Rotate Array'
|
|
||||||
description: Given an integer array nums, rotate the array to the right by k steps, where k is non-negative
|
|
||||||
sidebar:
|
|
||||||
label: 'Rotate Array'
|
|
||||||
badge: 'Medium'
|
|
||||||
---
|
|
||||||
|
|
||||||
<Badge variant="accent">Two Pointers</Badge>
|
|
||||||
|
|
||||||
::::warning
|
|
||||||
Try to come up with as many solutions as you can. There are at least three different ways to solve this problem. Could you do it in-place with `O(1)` extra space?
|
|
||||||
::::
|
|
||||||
|
|
||||||
### Example 1:
|
|
||||||
- Input: `nums = [1,2,3,4,5,6,7], k = 3`
|
|
||||||
- Output: `[5,6,7,1,2,3,4]`
|
|
||||||
- Explanation: rotate 1 steps to the right: `[7,1,2,3,4,5,6]` rotate 2 steps to the right: `[6,7,1,2,3,4,5]` rotate 3 steps to the right: `[5,6,7,1,2,3,4]`
|
|
||||||
|
|
||||||
### Example 2:
|
|
||||||
- Input: `nums = [-1,-100,3,99], k = 2`
|
|
||||||
- Output: `[3,99,-1,-100]`
|
|
||||||
- Explanation: rotate 1 steps to the right: `[99,-1,-100,3]` rotate 2 steps to the right: `[3,99,-1,-100]`
|
|
||||||
|
|
||||||
### Constraints:
|
|
||||||
|
|
||||||
- `1 <= nums.length <= 10^5`
|
|
||||||
- `-2^31 <= nums[i] <= 2^31 - 1`
|
|
||||||
- `0 <= k <= 10^5`
|
|
||||||
|
|
||||||
## Solution
|
|
||||||
|
|
||||||
```js
|
|
||||||
/**
|
|
||||||
* @param {number[]} nums
|
|
||||||
* @param {number} k
|
|
||||||
* @return {void} Do not return anything, modify nums in-place instead.
|
|
||||||
*/
|
|
||||||
var rotate = function(nums, k) {
|
|
||||||
|
|
||||||
};
|
|
||||||
```
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
---
|
|
||||||
title: '2090. K Radius Subarray Averages'
|
|
||||||
description: You are given a 0-indexed array nums of n integers, and an integer k
|
|
||||||
sidebar:
|
|
||||||
label: 'K Radius Subarray Averages'
|
|
||||||
badge: 'Medium'
|
|
||||||
---
|
|
||||||
|
|
||||||
<Badge variant="accent">Array</Badge>
|
|
||||||
|
|
||||||
### Example 1:
|
|
||||||
- Input: `nums = [7,4,3,9,1,8,5,2,6], k = 3`
|
|
||||||
- Output: `[-1,-1,-1,5,4,4,-1,-1,-1]`
|
|
||||||
- Explanation:
|
|
||||||
- `avg[0]`, `avg[1]`, and `avg[2]` are `-1` because there are less than `k` elements before each index.
|
|
||||||
- The sum of the subarray centered at index `3` with radius `3` is: `7 + 4 + 3 + 9 + 1 + 8 + 5 = 37`. Using integer division, `avg[3] = 37 / 7 = 5`.
|
|
||||||
- For the subarray centered at index `4`, `avg[4] = (4 + 3 + 9 + 1 + 8 + 5 + 2) / 7 = 4`.
|
|
||||||
- For the subarray centered at index `5`, `avg[5] = (3 + 9 + 1 + 8 + 5 + 2 + 6) / 7 = 4`.
|
|
||||||
- `avg[6]`, `avg[7]`, and `avg[8]` are `-1` because there are less than `k` elements after each index.
|
|
||||||
|
|
||||||
### Example 2:
|
|
||||||
- Input: `nums = [100000], k = 0`
|
|
||||||
- Output: `[100000]`
|
|
||||||
- Explanation:
|
|
||||||
- The sum of the subarray centered at index `0` with radius `0` is: `100000`. `avg[0] = 100000 / 1 = 100000`.
|
|
||||||
|
|
||||||
### Example 3:
|
|
||||||
- Input: `nums = [8], k = 100000`
|
|
||||||
- Output: `[-1]`
|
|
||||||
- Explanation:
|
|
||||||
- `avg[0]` is `-1` because there are less than `k` elements before and after index `0`.
|
|
||||||
|
|
||||||
### Constraints:
|
|
||||||
|
|
||||||
- `n == nums.length`
|
|
||||||
- `1 <= n <= 10^5`
|
|
||||||
- `0 <= nums[i], k <= 10^5`
|
|
||||||
|
|
||||||
## Solution
|
|
||||||
|
|
||||||
```js
|
|
||||||
/**
|
|
||||||
* @param {number[]} nums
|
|
||||||
* @param {number} k
|
|
||||||
* @return {number[]}
|
|
||||||
*/
|
|
||||||
var getAverages = function(nums, k) {
|
|
||||||
|
|
||||||
};
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: '35. Search Insert Position'
|
||||||
|
description: Given a sorted array of distinct integers and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order
|
||||||
|
sidebar:
|
||||||
|
label: 'Search Insert Position'
|
||||||
|
badge: 'Easy'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Binary Search</Badge>
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
You must write an algorithm with O(log n) runtime complexity.
|
||||||
|
::::
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `nums = [1,3,5,6], target = 5`
|
||||||
|
- Output: `2`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `nums = [1,3,5,6], target = 2`
|
||||||
|
- Output: `1`
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `nums = [1,3,5,6], target = 7`
|
||||||
|
- Output: `4`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `1 <= nums.length <= 10^4`
|
||||||
|
- `-10^4 <= nums[i] <= 10^4`
|
||||||
|
- `nums` contains distinct values sorted in ascending order.
|
||||||
|
- `-10^4 <= target <= 10^4`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def searchInsert(self, nums: List[int], target: int) -> int:
|
||||||
|
l, r = 0, len(nums) - 1
|
||||||
|
|
||||||
|
while l <= r:
|
||||||
|
m = (l + r) // 2
|
||||||
|
if nums[m] == target:
|
||||||
|
return m
|
||||||
|
elif nums[m] < target:
|
||||||
|
l = m + 1
|
||||||
|
elif nums[m] > target:
|
||||||
|
r = m - 1
|
||||||
|
|
||||||
|
return (l + r) // 2 + 1
|
||||||
|
```
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
title: '42. Trapping Rain Water'
|
|
||||||
description: Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining
|
|
||||||
sidebar:
|
|
||||||
label: 'Trapping Rain Water'
|
|
||||||
badge: 'Hard'
|
|
||||||
---
|
|
||||||
|
|
||||||
<Badge variant="accent">Two Pointers</Badge>
|
|
||||||
|
|
||||||
### Example 1:
|
|
||||||
- Input: `height = [0,1,0,2,1,0,1,3,2,1,2,1]`
|
|
||||||
- Output: `6`
|
|
||||||
- Explanation: The above elevation map (black section) is represented by array `[0,1,0,2,1,0,1,3,2,1,2,1]`. In this case, `6` units of rain water (blue section) are being trapped.
|
|
||||||
|
|
||||||
### Example 2:
|
|
||||||
- Input: `height = [4,2,0,3,2,5]`
|
|
||||||
- Output: `9`
|
|
||||||
|
|
||||||
### Constraints:
|
|
||||||
|
|
||||||
- `n == height.length`
|
|
||||||
- `1 <= n <= 2 * 10^4`
|
|
||||||
- `0 <= height[i] <= 10^5`
|
|
||||||
|
|
||||||
## Solution
|
|
||||||
|
|
||||||
```js
|
|
||||||
/**
|
|
||||||
* @param {number[]} height
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
var trap = function(height) {
|
|
||||||
|
|
||||||
};
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
title: '739. Daily Temperatures'
|
||||||
|
description: Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the i^th day to get a warmer temperature. If there is no future day for which this is possible, keep answer[i] == 0 instead
|
||||||
|
sidebar:
|
||||||
|
label: 'Daily Temperatures'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Monotonic Stack</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `temperatures = [73,74,75,71,69,72,76,73]`
|
||||||
|
- Output: `[1,1,4,2,1,1,0,0]`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `temperatures = [30,40,50,60]`
|
||||||
|
- Output: `[1,1,1,0]`
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `temperatures = [30,60,90]`
|
||||||
|
- Output: `[1,1,0]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `1 <= temperatures.length <= 10^5`
|
||||||
|
- `30 <= temperatures[i] <= 100`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
|
||||||
|
ans = [0] * len(temperatures)
|
||||||
|
stack = []
|
||||||
|
for i, temp in enumerate(temperatures):
|
||||||
|
while stack and temperatures[stack[-1]] < temp:
|
||||||
|
prev = stack.pop()
|
||||||
|
ans[prev] = i - prev
|
||||||
|
stack.append(i)
|
||||||
|
return ans
|
||||||
|
```
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: '853. Car Fleet'
|
||||||
|
description: There are n cars at given miles away from the starting mile 0, traveling to reach the mile target
|
||||||
|
sidebar:
|
||||||
|
label: 'Car Fleet'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Monotonic Stack</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]`
|
||||||
|
- Output: `3`
|
||||||
|
- Explanation: • The cars starting at `10 (speed 2`) and `8 (speed 4`) become a fleet, meeting each other at `12`. The fleet forms at `target`. • The car starting at `0 (speed 1`) does not catch up to any other car, so it is a fleet by itself. • The cars starting at `5 (speed 1`) and `3 (speed 3`) become a fleet, meeting each other at `6`. The fleet moves at `speed 1` until it reaches `target`.
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `target = 10, position = [3], speed = [3]`
|
||||||
|
- Output: `1`
|
||||||
|
- Explanation: There is only one car, hence there is only one fleet.
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `target = 100, position = [0,2,4], speed = [4,2,1]`
|
||||||
|
- Output: `1`
|
||||||
|
- Explanation: • The cars starting at `0 (speed 4`) and `2 (speed 2`) become a fleet, meeting each other at `4`. The car starting at `4 (speed 1`) travels to `5`. • Then, the fleet at `4 (speed 2`) and the car at `position 5 (speed 1`) become one fleet, meeting each other at `6`. The fleet moves at `speed 1` until it reaches `target`.
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `n == position.length == speed.length`
|
||||||
|
- `1 <= n <= 10^5`
|
||||||
|
- `0 < target <= 10^6`
|
||||||
|
- `0 <= position[i] < target`
|
||||||
|
- All the values of `position` are unique.
|
||||||
|
- `0 < speed[i] <= 10^6`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
|
||||||
|
stack = []
|
||||||
|
for pos, spd in sorted(zip(position, speed), reverse=True):
|
||||||
|
time = (target - pos) / spd
|
||||||
|
if not stack or time > stack[-1]:
|
||||||
|
stack.append(time)
|
||||||
|
|
||||||
|
return len(stack)
|
||||||
|
```
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: '155. Min Stack'
|
||||||
|
description: Design a stack that supports push, pop, top, and retrieving the minimum element in constant time
|
||||||
|
sidebar:
|
||||||
|
label: 'Min Stack'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Stack</Badge>
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
You must implement a solution with O(1) time complexity for each function.
|
||||||
|
::::
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: ``
|
||||||
|
- Output: ``
|
||||||
|
- Explanation: Input `["MinStack","push","push","push","getMin","pop","top","getMin"]` [[],[-2],[0],[-3],[],[],[],[]] Output `[null,null,null,null,-3,null,0,-2]` Explanation MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); // return `-3` minStack.pop(); minStack.top(); // return `0` minStack.getMin(); // return `-2`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `-2^31 <= val <= 2^31 - 1`
|
||||||
|
- Methods pop, top and getMin operations will always be called on non-empty stacks.
|
||||||
|
- At most 3 * 10^4 calls will be made to push, pop, top, and getMin.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MinStack:
|
||||||
|
def __init__(self):
|
||||||
|
self.stack = []
|
||||||
|
self.minStack = []
|
||||||
|
|
||||||
|
def push(self, value: int) -> None:
|
||||||
|
self.stack.append(value)
|
||||||
|
value = min(value, self.minStack[-1] if self.minStack else value)
|
||||||
|
self.minStack.append(value)
|
||||||
|
|
||||||
|
def pop(self) -> None:
|
||||||
|
self.stack.pop()
|
||||||
|
self.minStack.pop()
|
||||||
|
|
||||||
|
def top(self) -> int:
|
||||||
|
return self.stack[-1]
|
||||||
|
|
||||||
|
def getMin(self) -> int:
|
||||||
|
return self.minStack[-1]
|
||||||
|
|
||||||
|
|
||||||
|
# Your MinStack object will be instantiated and called as such:
|
||||||
|
# obj = MinStack()
|
||||||
|
# obj.push(value)
|
||||||
|
# obj.pop()
|
||||||
|
# param_3 = obj.top()
|
||||||
|
# param_4 = obj.getMin()
|
||||||
|
```
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: '20. Valid Parentheses'
|
||||||
|
description: Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid
|
||||||
|
sidebar:
|
||||||
|
label: 'Valid Parentheses'
|
||||||
|
badge: 'Easy'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Stack</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `s = "()"`
|
||||||
|
- Output: `true`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `s = "()[]{}"`
|
||||||
|
- Output: `true`
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `s = "(]"`
|
||||||
|
- Output: `false`
|
||||||
|
|
||||||
|
### Example 4:
|
||||||
|
- Input: `s = "([])"`
|
||||||
|
- Output: `true`
|
||||||
|
|
||||||
|
### Example 5:
|
||||||
|
- Input: `s = "([)]"`
|
||||||
|
- Output: `false`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `1 <= s.length <= 10^4`
|
||||||
|
- `s` consists of parentheses only '()[]{}'.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Solution:
|
||||||
|
def isValid(self, s: str) -> bool:
|
||||||
|
stack = []
|
||||||
|
pairs = {")": "(", "]": "[", "}": "{"}
|
||||||
|
|
||||||
|
for c in s:
|
||||||
|
if c in pairs:
|
||||||
|
if stack and pairs[c] == stack[-1]:
|
||||||
|
stack.pop()
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
stack.append(c)
|
||||||
|
|
||||||
|
return True if not stack else False
|
||||||
|
```
|
||||||
@@ -35,6 +35,7 @@ interface Stats {
|
|||||||
deferred_total: number;
|
deferred_total: number;
|
||||||
}[];
|
}[];
|
||||||
ladder: Record<Stage, number>;
|
ladder: Record<Stage, number>;
|
||||||
|
/** week is the 1-based campaign week — the Worker converts from ISO. */
|
||||||
gates: {
|
gates: {
|
||||||
week: number;
|
week: number;
|
||||||
issue: number | null;
|
issue: number | null;
|
||||||
@@ -76,7 +77,12 @@ function dayLabel(iso: string): string {
|
|||||||
return `${weekday} ${iso.slice(8)}`;
|
return `${weekday} ${iso.slice(8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── shared styles (theme tokens only — no hand-rolled palette) ───
|
// ── shared styles ────────────────────────────────────────────────
|
||||||
|
// Theme tokens only, with two deliberate exceptions: the phase-bar sets get
|
||||||
|
// fixed hues (accent-blue core, yellow optional, red deferred — they must be
|
||||||
|
// distinguishable, not shades of one colour), and the last-7-days heatmap
|
||||||
|
// reuses the GREENS ramp from apps/api/src/charts.ts so the page matches the
|
||||||
|
// README/email heatmaps (level 0 stays a theme token so light mode works).
|
||||||
|
|
||||||
const card: CSSProperties = {
|
const card: CSSProperties = {
|
||||||
border: "1px solid var(--blume-border)",
|
border: "1px solid var(--blume-border)",
|
||||||
@@ -142,32 +148,78 @@ function Header({ stats }: { stats: Stats }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One stacked bar per phase: solved core / optional / deferred segments over
|
||||||
|
// a muted track sized by the phase's total problem count, so both phase size
|
||||||
|
// and progress read at a glance.
|
||||||
|
const SETS = [
|
||||||
|
["core", "var(--blume-accent)"],
|
||||||
|
["optional", "#d29922"],
|
||||||
|
["deferred", "#e5534b"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
function Phases({ stats }: { stats: Stats }) {
|
function Phases({ stats }: { stats: Stats }) {
|
||||||
|
const maxTotal = Math.max(
|
||||||
|
1,
|
||||||
|
...stats.phases.map((p) => p.core_total + p.optional_total + p.deferred_total),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div style={card}>
|
<div style={card}>
|
||||||
<div style={heading}>Phases</div>
|
<div style={heading}>Phases</div>
|
||||||
<table style={{ borderCollapse: "collapse", width: "100%" }}>
|
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap", marginBottom: "0.6rem" }}>
|
||||||
<tbody>
|
{SETS.map(([set, color]) => (
|
||||||
{stats.phases.map((p) => (
|
<span key={set} style={muted}>
|
||||||
<tr key={p.milestone}>
|
<span
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>{p.name}</td>
|
style={{
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>
|
display: "inline-block",
|
||||||
<Bar done={p.core_done} total={p.core_total} />{" "}
|
width: "0.65rem",
|
||||||
<span style={muted}>
|
height: "0.65rem",
|
||||||
core {p.core_done}/{p.core_total}
|
borderRadius: "2px",
|
||||||
</span>
|
background: color,
|
||||||
</td>
|
marginRight: "0.35rem",
|
||||||
<td style={{ padding: "0.2rem 0" }}>
|
}}
|
||||||
<span style={muted}>
|
/>
|
||||||
optional {p.optional_done}/{p.optional_total}
|
{set}
|
||||||
{p.deferred_total > 0 &&
|
</span>
|
||||||
` · deferred ${p.deferred_done}/${p.deferred_total}`}
|
))}
|
||||||
</span>
|
</div>
|
||||||
</td>
|
{stats.phases.map((p) => {
|
||||||
</tr>
|
const total = p.core_total + p.optional_total + p.deferred_total;
|
||||||
))}
|
const done = p.core_done + p.optional_done + p.deferred_done;
|
||||||
</tbody>
|
return (
|
||||||
</table>
|
<div
|
||||||
|
key={p.milestone}
|
||||||
|
title={`core ${p.core_done}/${p.core_total} · optional ${p.optional_done}/${p.optional_total} · deferred ${p.deferred_done}/${p.deferred_total}`}
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: "0.75rem", padding: "0.25rem 0" }}
|
||||||
|
>
|
||||||
|
<span style={{ width: "9.5rem", flexShrink: 0 }}>{p.name}</span>
|
||||||
|
<span style={{ flex: 1 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
width: `${(total / maxTotal) * 100}%`,
|
||||||
|
height: "0.65rem",
|
||||||
|
borderRadius: "var(--blume-radius)",
|
||||||
|
background: "var(--blume-muted)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{SETS.map(([set, color]) => {
|
||||||
|
const solved = p[`${set}_done`];
|
||||||
|
return solved > 0 ? (
|
||||||
|
<span
|
||||||
|
key={set}
|
||||||
|
style={{ width: `${(solved / total) * 100}%`, background: color }}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span style={{ ...muted, width: "3.5rem", textAlign: "right", flexShrink: 0 }}>
|
||||||
|
{done}/{total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -224,76 +276,95 @@ function Gates({ stats }: { stats: Stats }) {
|
|||||||
<div style={card}>
|
<div style={card}>
|
||||||
<div style={heading}>Gate log</div>
|
<div style={heading}>Gate log</div>
|
||||||
{stats.gates.length === 0 ? (
|
{stats.gates.length === 0 ? (
|
||||||
<span style={muted}>No gates yet.</span>
|
<span style={muted}>No gates yet — the first weekly review opens Saturday night.</span>
|
||||||
) : (
|
) : (
|
||||||
<table style={{ borderCollapse: "collapse", width: "100%" }}>
|
stats.gates.map((g) => (
|
||||||
<thead>
|
<div
|
||||||
<tr>
|
key={g.week}
|
||||||
{["Week", "Issue", "Pass rate", "Closed"].map((h) => (
|
style={{
|
||||||
<th key={h} style={{ ...muted, textAlign: "left", padding: "0.2rem 1rem 0.2rem 0" }}>
|
display: "flex",
|
||||||
{h}
|
gap: "0.75rem",
|
||||||
</th>
|
alignItems: "baseline",
|
||||||
))}
|
flexWrap: "wrap",
|
||||||
</tr>
|
padding: "0.15rem 0",
|
||||||
</thead>
|
}}
|
||||||
<tbody>
|
>
|
||||||
{stats.gates.map((g) => (
|
<span style={{ width: "4.5rem", flexShrink: 0 }}>Week {g.week}</span>
|
||||||
<tr key={g.week}>
|
{g.issue === null ? (
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>{g.week}</td>
|
<span style={muted}>no review issue</span>
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>
|
) : (
|
||||||
{g.issue === null ? (
|
<a
|
||||||
<span style={muted}>—</span>
|
href={`https://github.com/prdlk/leetcode/issues/${g.issue}`}
|
||||||
) : (
|
target="_blank"
|
||||||
<a
|
rel="noreferrer"
|
||||||
href={`https://github.com/prdlk/leetcode/issues/${g.issue}`}
|
>
|
||||||
target="_blank"
|
#{g.issue}
|
||||||
rel="noreferrer"
|
</a>
|
||||||
>
|
)}
|
||||||
#{g.issue}
|
{g.pass_rate === null ? (
|
||||||
</a>
|
<span style={muted}>in progress</span>
|
||||||
)}
|
) : (
|
||||||
</td>
|
<span>
|
||||||
<td style={{ padding: "0.2rem 1rem 0.2rem 0" }}>
|
{percent(g.pass_rate)} passed
|
||||||
{g.pass_rate === null ? (
|
{g.closed_on && <span style={muted}> · closed {g.closed_on}</span>}
|
||||||
<span style={muted}>open</span>
|
</span>
|
||||||
) : (
|
)}
|
||||||
percent(g.pass_rate)
|
</div>
|
||||||
)}
|
))
|
||||||
</td>
|
|
||||||
<td style={{ padding: "0.2rem 0" }}>
|
|
||||||
{g.closed_on ?? <span style={muted}>—</span>}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
)}
|
||||||
|
<div style={{ ...muted, marginTop: "0.4rem" }}>
|
||||||
|
One gate per campaign week — scored when its review issue closes.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GREENS ramp from apps/api/src/charts.ts; level 0 swapped for the theme's
|
||||||
|
// muted token so an empty day looks right in both colour schemes.
|
||||||
|
const GREENS = [
|
||||||
|
"var(--blume-muted)",
|
||||||
|
"#0e4429",
|
||||||
|
"#006d32",
|
||||||
|
"#26a641",
|
||||||
|
"#39d353",
|
||||||
|
];
|
||||||
|
|
||||||
function Recent({ stats }: { stats: Stats }) {
|
function Recent({ stats }: { stats: Stats }) {
|
||||||
return (
|
return (
|
||||||
<div style={card}>
|
<div style={card}>
|
||||||
<div style={heading}>Last 7 days</div>
|
<div style={heading}>Last 7 days</div>
|
||||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
|
||||||
{stats.recent.map((r) => (
|
{stats.recent.map((r) => (
|
||||||
<span
|
<span
|
||||||
key={r.date}
|
key={r.date}
|
||||||
style={{
|
title={`${r.date}: ${r.passes}/${r.attempts} passed`}
|
||||||
border: "1px solid var(--blume-border)",
|
style={{ textAlign: "center" }}
|
||||||
borderRadius: "var(--blume-radius)",
|
|
||||||
padding: "0.35rem 0.6rem",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span style={{ ...muted, display: "block" }}>{dayLabel(r.date)}</span>
|
<span
|
||||||
<strong>{r.passes}</strong>
|
style={{
|
||||||
<span style={muted}>/{r.attempts}</span>
|
display: "block",
|
||||||
|
width: "2.6rem",
|
||||||
|
height: "2rem",
|
||||||
|
borderRadius: "var(--blume-radius)",
|
||||||
|
background: GREENS[Math.min(r.attempts, 4)],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ ...muted, display: "block", marginTop: "0.25rem", fontSize: "0.75em" }}>
|
||||||
|
{dayLabel(r.date)}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ ...muted, marginTop: "0.4rem" }}>passes / attempts</div>
|
<div style={{ ...muted, marginTop: "0.5rem", display: "flex", alignItems: "center", gap: "0.25rem" }}>
|
||||||
|
<span>Less</span>
|
||||||
|
{GREENS.map((color) => (
|
||||||
|
<span
|
||||||
|
key={color}
|
||||||
|
style={{ display: "inline-block", width: "0.65rem", height: "0.65rem", borderRadius: "2px", background: color }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span>More · colour = attempts, hover for passes</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"doctor": "cd apps/docs && bun run doctor",
|
"doctor": "cd apps/docs && bun run doctor",
|
||||||
"close-solved": "bun apps/cli/close-solved.ts",
|
"close-solved": "bun apps/cli/close-solved.ts",
|
||||||
"close-topics": "bun apps/cli/close-topics.ts",
|
"close-topics": "bun apps/cli/close-topics.ts",
|
||||||
|
"sync-project-fields": "bun apps/cli/sync-project-fields.ts",
|
||||||
"api:dev": "cd apps/api && bun run dev",
|
"api:dev": "cd apps/api && bun run dev",
|
||||||
"api:deploy": "cd apps/api && bun run deploy",
|
"api:deploy": "cd apps/api && bun run deploy",
|
||||||
"api:test": "cd apps/api && bun test src",
|
"api:test": "cd apps/api && bun test src",
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""
|
||||||
|
35. Search Insert Position
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/search-insert-position/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given a sorted array of distinct integers and a target value, return
|
||||||
|
the index if the target is found. If not, return the index where it
|
||||||
|
would be if it were inserted in order.
|
||||||
|
|
||||||
|
You must write an algorithm with O(log n) runtime complexity.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: nums = [1,3,5,6], target = 5
|
||||||
|
Output: 2
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: nums = [1,3,5,6], target = 2
|
||||||
|
Output: 1
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: nums = [1,3,5,6], target = 7
|
||||||
|
Output: 4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= nums.length <= 10^4
|
||||||
|
|
||||||
|
• -10^4 <= nums[i] <= 10^4
|
||||||
|
|
||||||
|
• nums contains distinct values sorted in ascending order.
|
||||||
|
|
||||||
|
• -10^4 <= target <= 10^4
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def searchInsert(self, nums: List[int], target: int) -> int:
|
||||||
|
l, r = 0, len(nums) - 1
|
||||||
|
|
||||||
|
while l <= r:
|
||||||
|
m = (l + r) // 2
|
||||||
|
if nums[m] == target:
|
||||||
|
return m
|
||||||
|
elif nums[m] < target:
|
||||||
|
l = m + 1
|
||||||
|
elif nums[m] > target:
|
||||||
|
r = m - 1
|
||||||
|
|
||||||
|
return (l + r) // 2 + 1
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
20. Valid Parentheses
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/valid-parentheses/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given a string s containing just the characters '(', ')', '{', '}',
|
||||||
|
'[' and ']', determine if the input string is valid.
|
||||||
|
|
||||||
|
An input string is valid if:
|
||||||
|
|
||||||
|
• Open brackets must be closed by the same type of brackets.
|
||||||
|
|
||||||
|
• Open brackets must be closed in the correct order.
|
||||||
|
|
||||||
|
• Every close bracket has a corresponding open bracket of the same
|
||||||
|
type.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: s = "()"
|
||||||
|
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: s = "()[]{}"
|
||||||
|
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: s = "(]"
|
||||||
|
|
||||||
|
Output: false
|
||||||
|
|
||||||
|
Example 4:
|
||||||
|
|
||||||
|
Input: s = "([])"
|
||||||
|
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
Example 5:
|
||||||
|
|
||||||
|
Input: s = "([)]"
|
||||||
|
|
||||||
|
Output: false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= s.length <= 10^4
|
||||||
|
|
||||||
|
• s consists of parentheses only '()[]{}'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def isValid(self, s: str) -> bool:
|
||||||
|
stack = []
|
||||||
|
pairs = {")": "(", "]": "[", "}": "{"}
|
||||||
|
|
||||||
|
for c in s:
|
||||||
|
if c in pairs:
|
||||||
|
if stack and pairs[c] == stack[-1]:
|
||||||
|
stack.pop()
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
stack.append(c)
|
||||||
|
|
||||||
|
return True if not stack else False
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* 42. Trapping Rain Water
|
|
||||||
* Difficulty: Hard
|
|
||||||
* https://leetcode.com/problems/trapping-rain-water/
|
|
||||||
*
|
|
||||||
* ──────────────────────────────────────────────────
|
|
||||||
*
|
|
||||||
* Given n non-negative integers representing an elevation map where the
|
|
||||||
* width of each bar is 1, compute how much water it can trap after
|
|
||||||
* raining.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Example 1:
|
|
||||||
*
|
|
||||||
* Input: height = [0,1,0,2,1,0,1,3,2,1,2,1]
|
|
||||||
* Output: 6
|
|
||||||
* Explanation: The above elevation map (black section) is represented
|
|
||||||
* by array [0,1,0,2,1,0,1,3,2,1,2,1]. In this case, 6 units of rain
|
|
||||||
* water (blue section) are being trapped.
|
|
||||||
*
|
|
||||||
* Example 2:
|
|
||||||
*
|
|
||||||
* Input: height = [4,2,0,3,2,5]
|
|
||||||
* Output: 9
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Constraints:
|
|
||||||
*
|
|
||||||
* • n == height.length
|
|
||||||
*
|
|
||||||
* • 1 <= n <= 2 * 10^4
|
|
||||||
*
|
|
||||||
* • 0 <= height[i] <= 10^5
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number[]} height
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
var trap = function(height) {
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
150. Evaluate Reverse Polish Notation
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/evaluate-reverse-polish-notation/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
You are given an array of strings tokens that represents an
|
||||||
|
arithmetic expression in a Reverse Polish Notation.
|
||||||
|
|
||||||
|
Evaluate the expression. Return an integer that represents the value
|
||||||
|
of the expression.
|
||||||
|
|
||||||
|
Note that:
|
||||||
|
|
||||||
|
• The valid operators are '+', '-', '*', and '/'.
|
||||||
|
|
||||||
|
• Each operand may be an integer or another expression.
|
||||||
|
|
||||||
|
• The division between two integers always truncates toward zero.
|
||||||
|
|
||||||
|
• There will not be any division by zero.
|
||||||
|
|
||||||
|
• The input represents a valid arithmetic expression in a reverse
|
||||||
|
polish notation.
|
||||||
|
|
||||||
|
• The answer and all the intermediate calculations can be
|
||||||
|
represented in a 32-bit integer.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: tokens = ["2","1","+","3","*"]
|
||||||
|
Output: 9
|
||||||
|
Explanation: ((2 + 1) * 3) = 9
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: tokens = ["4","13","5","/","+"]
|
||||||
|
Output: 6
|
||||||
|
Explanation: (4 + (13 / 5)) = 6
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: tokens =
|
||||||
|
["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
|
||||||
|
Output: 22
|
||||||
|
Explanation: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
|
||||||
|
= ((10 * (6 / (12 * -11))) + 17) + 5
|
||||||
|
= ((10 * (6 / -132)) + 17) + 5
|
||||||
|
= ((10 * 0) + 17) + 5
|
||||||
|
= (0 + 17) + 5
|
||||||
|
= 17 + 5
|
||||||
|
= 22
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= tokens.length <= 10^4
|
||||||
|
|
||||||
|
• tokens[i] is either an operator: "+", "-", "*", or "/", or an
|
||||||
|
integer in the range [-200, 200].
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def evalRPN(self, tokens: List[str]) -> int:
|
||||||
|
stack = []
|
||||||
|
operations = {
|
||||||
|
"+": lambda x, y: int(x + y),
|
||||||
|
"-": lambda x, y: int(x - y),
|
||||||
|
"*": lambda x, y: int(x * y),
|
||||||
|
"/": lambda x, y: int(x / y),
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in tokens:
|
||||||
|
if c in operations:
|
||||||
|
y = stack.pop()
|
||||||
|
x = stack.pop()
|
||||||
|
calc_func = operations[c]
|
||||||
|
result = calc_func(x, y)
|
||||||
|
stack.append(result)
|
||||||
|
else:
|
||||||
|
stack.append(int(c))
|
||||||
|
|
||||||
|
return stack[0]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
739. Daily Temperatures
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/daily-temperatures/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an array of integers temperatures represents the daily
|
||||||
|
temperatures, return an array answer such that answer[i] is the number
|
||||||
|
of days you have to wait after the i^th day to get a warmer
|
||||||
|
temperature. If there is no future day for which this is possible,
|
||||||
|
keep answer[i] == 0 instead.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: temperatures = [73,74,75,71,69,72,76,73]
|
||||||
|
Output: [1,1,4,2,1,1,0,0]
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: temperatures = [30,40,50,60]
|
||||||
|
Output: [1,1,1,0]
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: temperatures = [30,60,90]
|
||||||
|
Output: [1,1,0]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= temperatures.length <= 10^5
|
||||||
|
|
||||||
|
• 30 <= temperatures[i] <= 100
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
|
||||||
|
ans = [0] * len(temperatures)
|
||||||
|
stack = []
|
||||||
|
for i, temp in enumerate(temperatures):
|
||||||
|
while stack and temperatures[stack[-1]] < temp:
|
||||||
|
prev = stack.pop()
|
||||||
|
ans[prev] = i - prev
|
||||||
|
stack.append(i)
|
||||||
|
return ans
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
853. Car Fleet
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/car-fleet/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
There are n cars at given miles away from the starting mile 0,
|
||||||
|
traveling to reach the mile target.
|
||||||
|
|
||||||
|
You are given two integer arrays position and speed, both of length
|
||||||
|
n, where position[i] is the starting mile of the i^th car and speed[i]
|
||||||
|
is the speed of the i^th car in miles per hour.
|
||||||
|
|
||||||
|
A car cannot pass another car, but it can catch up and then travel
|
||||||
|
next to it at the speed of the slower car.
|
||||||
|
|
||||||
|
A car fleet is a single car or a group of cars driving next to each
|
||||||
|
other. The speed of the car fleet is the minimum speed of any car in
|
||||||
|
the fleet.
|
||||||
|
|
||||||
|
If a car catches up to a car fleet at the mile target, it will still
|
||||||
|
be considered as part of the car fleet.
|
||||||
|
|
||||||
|
Return the number of car fleets that will arrive at the destination.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]
|
||||||
|
|
||||||
|
Output: 3
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
• The cars starting at 10 (speed 2) and 8 (speed 4) become a fleet,
|
||||||
|
meeting each other at 12. The fleet forms at target.
|
||||||
|
|
||||||
|
• The car starting at 0 (speed 1) does not catch up to any other
|
||||||
|
car, so it is a fleet by itself.
|
||||||
|
|
||||||
|
• The cars starting at 5 (speed 1) and 3 (speed 3) become a fleet,
|
||||||
|
meeting each other at 6. The fleet moves at speed 1 until it reaches
|
||||||
|
target.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: target = 10, position = [3], speed = [3]
|
||||||
|
|
||||||
|
Output: 1
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
There is only one car, hence there is only one fleet.
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: target = 100, position = [0,2,4], speed = [4,2,1]
|
||||||
|
|
||||||
|
Output: 1
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
• The cars starting at 0 (speed 4) and 2 (speed 2) become a fleet,
|
||||||
|
meeting each other at 4. The car starting at 4 (speed 1) travels to 5.
|
||||||
|
|
||||||
|
• Then, the fleet at 4 (speed 2) and the car at position 5 (speed 1)
|
||||||
|
become one fleet, meeting each other at 6. The fleet moves at speed 1
|
||||||
|
until it reaches target.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• n == position.length == speed.length
|
||||||
|
|
||||||
|
• 1 <= n <= 10^5
|
||||||
|
|
||||||
|
• 0 < target <= 10^6
|
||||||
|
|
||||||
|
• 0 <= position[i] < target
|
||||||
|
|
||||||
|
• All the values of position are unique.
|
||||||
|
|
||||||
|
• 0 < speed[i] <= 10^6
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
|
||||||
|
stack = []
|
||||||
|
for pos, spd in sorted(zip(position, speed), reverse=True):
|
||||||
|
time = (target - pos) / spd
|
||||||
|
if not stack or time > stack[-1]:
|
||||||
|
stack.append(time)
|
||||||
|
|
||||||
|
return len(stack)
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
155. Min Stack
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/min-stack/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Design a stack that supports push, pop, top, and retrieving the
|
||||||
|
minimum element in constant time.
|
||||||
|
|
||||||
|
Implement the MinStack class:
|
||||||
|
|
||||||
|
• MinStack() initializes the stack object.
|
||||||
|
|
||||||
|
• void push(int value) pushes the element value onto the stack.
|
||||||
|
|
||||||
|
• void pop() removes the element on the top of the stack.
|
||||||
|
|
||||||
|
• int top() gets the top element of the stack.
|
||||||
|
|
||||||
|
• int getMin() retrieves the minimum element in the stack.
|
||||||
|
|
||||||
|
You must implement a solution with O(1) time complexity for each
|
||||||
|
function.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input
|
||||||
|
["MinStack","push","push","push","getMin","pop","top","getMin"]
|
||||||
|
[[],[-2],[0],[-3],[],[],[],[]]
|
||||||
|
|
||||||
|
Output
|
||||||
|
[null,null,null,null,-3,null,0,-2]
|
||||||
|
|
||||||
|
Explanation
|
||||||
|
MinStack minStack = new MinStack();
|
||||||
|
minStack.push(-2);
|
||||||
|
minStack.push(0);
|
||||||
|
minStack.push(-3);
|
||||||
|
minStack.getMin(); // return -3
|
||||||
|
minStack.pop();
|
||||||
|
minStack.top(); // return 0
|
||||||
|
minStack.getMin(); // return -2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• -2^31 <= val <= 2^31 - 1
|
||||||
|
|
||||||
|
• Methods pop, top and getMin operations will always be called on
|
||||||
|
non-empty stacks.
|
||||||
|
|
||||||
|
• At most 3 * 10^4 calls will be made to push, pop, top, and getMin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MinStack:
|
||||||
|
def __init__(self):
|
||||||
|
self.stack = []
|
||||||
|
self.minStack = []
|
||||||
|
|
||||||
|
def push(self, value: int) -> None:
|
||||||
|
self.stack.append(value)
|
||||||
|
value = min(value, self.minStack[-1] if self.minStack else value)
|
||||||
|
self.minStack.append(value)
|
||||||
|
|
||||||
|
def pop(self) -> None:
|
||||||
|
self.stack.pop()
|
||||||
|
self.minStack.pop()
|
||||||
|
|
||||||
|
def top(self) -> int:
|
||||||
|
return self.stack[-1]
|
||||||
|
|
||||||
|
def getMin(self) -> int:
|
||||||
|
return self.minStack[-1]
|
||||||
|
|
||||||
|
|
||||||
|
# Your MinStack object will be instantiated and called as such:
|
||||||
|
# obj = MinStack()
|
||||||
|
# obj.push(value)
|
||||||
|
# obj.pop()
|
||||||
|
# param_3 = obj.top()
|
||||||
|
# param_4 = obj.getMin()
|
||||||
Reference in new issue
Block a user