Files
tomatocream e10cc4257d 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
2026-06-08 11:38:09 +08:00

159 lines
4.4 KiB
Python

#!/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()