add flashcard generation tooling and binary search cards
- gen-flashcards.py: auto-generate recognition cards from all problem files - toolkit/gen-problem-cards.org: 199 auto-generated problem cards - 5 binary search tool cards (std::binary_search, std::lower_bound, comparison, two-sum pattern, sorting gotcha) - two-sum.org: add binary search C++ attempt - lc-org.el: add doom emacs localleader keybinding support
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Auto-generate Anki flashcards from problem .org files.
|
||||
|
||||
Generates one recognition card per problem:
|
||||
Front: Problem number + name
|
||||
Back: Problem statement (first paragraph)
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path(__file__).parent
|
||||
DSA = BASE / "dsa"
|
||||
OUTPUT = BASE / "toolkit" / "gen-problem-cards.org"
|
||||
|
||||
TOPIC_DISPLAY = {
|
||||
"1-d-dynamic-programming": "1D DP",
|
||||
"2-d-dynamic-programming": "2D DP",
|
||||
"advanced-graphs": "Advanced Graphs",
|
||||
"arrays-hashing": "Arrays & Hashing",
|
||||
"backtracking": "Backtracking",
|
||||
"binary-search": "Binary Search",
|
||||
"bit-manipulation": "Bit Manipulation",
|
||||
"graphs": "Graphs",
|
||||
"greedy": "Greedy",
|
||||
"heap-priority-queue": "Heap / Priority Queue",
|
||||
"intervals": "Intervals",
|
||||
"linked-list": "Linked List",
|
||||
"math-geometry": "Math & Geometry",
|
||||
"sliding-window": "Sliding Window",
|
||||
"stack": "Stack",
|
||||
"trees": "Trees",
|
||||
"tries": "Tries",
|
||||
"two-pointers": "Two Pointers",
|
||||
}
|
||||
|
||||
|
||||
def parse_file(path: Path) -> dict | None:
|
||||
"""Extract problem metadata and statement from an .org file."""
|
||||
text = path.read_text()
|
||||
|
||||
# Match heading: * TODO 0217. Contains Duplicate :easy:
|
||||
m = re.search(
|
||||
r"^\* (?:TODO|DONE) (\d{4})\. (.+?) :(easy|medium|hard):",
|
||||
text,
|
||||
re.MULTILINE,
|
||||
)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
num = m.group(1)
|
||||
name = m.group(2).strip()
|
||||
difficulty = m.group(3)
|
||||
|
||||
# Topic = parent directory name
|
||||
topic_slug = path.parent.name
|
||||
topic = TOPIC_DISPLAY.get(topic_slug, topic_slug.replace("-", " ").title())
|
||||
|
||||
# Problem statement: first non-empty, non-heading, non-property line after :END:
|
||||
after_props = text[m.end():]
|
||||
lines = after_props.split("\n")
|
||||
statement_lines = []
|
||||
in_block = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped == ":END:":
|
||||
continue
|
||||
if not stripped:
|
||||
if statement_lines:
|
||||
break
|
||||
continue
|
||||
if stripped.startswith("#+") or stripped.startswith("*") or stripped.startswith(":"):
|
||||
if statement_lines:
|
||||
break
|
||||
continue
|
||||
statement_lines.append(stripped)
|
||||
|
||||
statement = " ".join(statement_lines).strip()
|
||||
# Clean up org markup for plain text
|
||||
statement = re.sub(r"[~=*/_]", "", statement)
|
||||
|
||||
return {
|
||||
"num": num,
|
||||
"name": name,
|
||||
"difficulty": difficulty,
|
||||
"topic": topic,
|
||||
"statement": statement,
|
||||
"path": path,
|
||||
}
|
||||
|
||||
|
||||
def generate_cards(problems: list[dict]) -> str:
|
||||
"""Generate org-mode flashcard content."""
|
||||
parts = []
|
||||
parts.append("#+TITLE: Auto-Generated Problem Cards")
|
||||
parts.append("#+ANKI_DECK: study_deck_02")
|
||||
parts.append("")
|
||||
|
||||
for p in sorted(problems, key=lambda x: int(x["num"])):
|
||||
num = p["num"]
|
||||
name = p["name"]
|
||||
diff = p["difficulty"]
|
||||
topic = p["topic"]
|
||||
stmt = p["statement"]
|
||||
|
||||
# Card: given problem name → what does it ask?
|
||||
title = f"LC {num}. {name} — what does it ask?"
|
||||
tags = f":leetcode:{diff}:{topic.replace(' ', '-').lower()}:retrieval::recognition:"
|
||||
|
||||
parts.append(f"* {title} {tags}")
|
||||
parts.append(":PROPERTIES:")
|
||||
parts.append(":ANKI_NOTE_TYPE: Basic")
|
||||
parts.append(":END:")
|
||||
parts.append("** Front")
|
||||
parts.append(f"What does LeetCode {num} *{name}* ask you to do?")
|
||||
parts.append("** Back")
|
||||
parts.append(stmt)
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
|
||||
org_files = sorted(DSA.rglob("*.org"))
|
||||
org_files = [f for f in org_files if f.name != "udfs.org"]
|
||||
print(f"Found {len(org_files)} problem files")
|
||||
|
||||
problems = []
|
||||
skipped = 0
|
||||
for f in org_files:
|
||||
p = parse_file(f)
|
||||
if p:
|
||||
problems.append(p)
|
||||
else:
|
||||
skipped += 1
|
||||
print(f" skipped: {f.name}")
|
||||
|
||||
print(f"Parsed: {len(problems)} problems, {skipped} skipped")
|
||||
|
||||
content = generate_cards(problems)
|
||||
|
||||
if dry_run:
|
||||
print(f"\nWould write {len(problems)} cards to {OUTPUT}")
|
||||
# Show first 3 cards as preview
|
||||
preview = content.split("\n\n\n")[:3]
|
||||
print("\n--- Preview ---")
|
||||
print("\n\n".join(preview))
|
||||
else:
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_text(content)
|
||||
print(f"\nWrote {len(problems)} cards to {OUTPUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user