Skip to content

Commit c8ec4e8

Browse files
committed
feat: dev
1 parent 238ec7b commit c8ec4e8

31 files changed

+2319
-412
lines changed

.amazonq/rules/development-rules.md

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,27 @@
22

33
## Discussion Mode
44

5-
- **Discussion Mode**: Prefix prompt with "D:" to enter read-only discussion mode
6-
- In discussion mode: NO code updates, only read files and provide analysis/suggestions
7-
- Always start responses with "[Discussion Mode]" header when in discussion mode
8-
- Never exit discussion mode automatically - only when user uses "XD:" prefix
9-
- If user seems to want code changes, remind them to use "XD:" to exit discussion mode
10-
- **Exit Discussion**: Use "XD:" prefix to exit discussion mode and resume normal operations
5+
- **Enter**: Prefix with "D:" for read-only analysis mode
6+
- **Exit**: Use "XD:" to resume normal operations
7+
- In discussion mode: NO code updates, only analysis/suggestions
118

129
## Code Standards
1310

14-
- Use snake_case for Python method names (following Python convention)
15-
- Always include type hints for function parameters and return types
16-
- Use PEP 585/604 syntax: `list[str]`, `dict[str, int]`, `Type | None`, etc.
17-
- Add return statements to satisfy type checkers even if unreachable
18-
- Follow the project's linting rules (black, isort, ruff, mypy)
19-
20-
## Template Usage
21-
22-
- **When user copies LeetCode problem**: Use `leetcode/_template/` to structure the question
23-
- Copy template files to new question directory: `leetcode/{question_name}/`
24-
- Replace template placeholders with actual problem details:
25-
- `{method_name}` - snake_case method name (e.g., `two_sum`)
26-
- `{ClassName}` - PascalCase class name (e.g., `TwoSum`)
27-
- `{parameters}` - method parameters with types
28-
- `{return_type}` - return type annotation
29-
- Test case placeholders with actual examples
30-
- **Template Implementation**: Do NOT implement the Solution class - only provide test cases and structure
31-
- **Helper Functions/Classes**: If the question relies on underlying helper functions or classes (e.g., TreeNode, ListNode):
32-
- First check if implementation already exists in `leetcode_py/common/` directory
33-
- If found, import from common module
34-
- If not found, create shared implementation in `leetcode_py/common/` for reusable classes
35-
- For question-specific helpers, implement directly in the solution file
36-
- **Update Makefile**: When adding new question, update the default `QUESTION` value in Makefile to the new question name
37-
- Always use the template structure for consistency
11+
- Use snake_case for Python methods
12+
- Include type hints: `list[str]`, `dict[str, int]`, `Type | None`
13+
- Follow linting rules (black, isort, ruff, mypy)
3814

39-
## File Structure
15+
## Testing
4016

41-
Each LeetCode problem should have:
17+
- Test specific: `make test-question QUESTION=<name>`
18+
- Test all: `make test`
19+
- Beautiful logging with loguru
4220

43-
- `README.md` - Problem description and examples
44-
- `solution.py` - Solution implementation
45-
- `tests.py` - Parametrized pytest tests with loguru logging
46-
- `__init__.py` - Empty file for Python package
21+
## File Structure
4722

48-
## Testing
23+
Each problem has:
4924

50-
- Use `make test-question QUESTION=<question_name>` to run tests
51-
- Use `make test` to run all questions with coverage
52-
- Default question is set to `two_sum` in Makefile
53-
- Tests should cover all provided examples
54-
- Use loguru for beautiful logging in tests
25+
- `README.md` - Problem description
26+
- `solution.py` - Implementation with TODO placeholder
27+
- `tests.py` - Parametrized pytest tests
28+
- `__init__.py` - Empty package file
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Question Creation Guide
2+
3+
## Quick Steps
4+
5+
1. Create JSON: `.templates/leetcode/json/{question_name}.json`
6+
2. Update Makefile: `QUESTION ?= your_new_question`
7+
3. Generate: `make q-gen`
8+
4. Verify: `make lint`
9+
5. **If you edit generated files**: Update JSON template, then `make q-gen FORCE=1` to ensure reproducibility
10+
11+
## JSON Template Rules
12+
13+
- **Copy from reference examples** - don't create from scratch
14+
- **Tree problems**: Use `.templates/leetcode/examples/tree.json5`
15+
- **Basic problems**: Use `.templates/leetcode/examples/basic.json5`
16+
- **Don't add extra fields** - templates are complete
17+
- **If lint fails**: Fix JSON and regenerate, don't edit generated files
18+
- **After any manual edits**: Always update JSON template and verify with `make q-gen FORCE=1`
19+
20+
## Tags (Optional)
21+
22+
```json
23+
"tags": ["grind-75", "blind-75", "neetcode-150", "top-interview"]
24+
```
25+
26+
## Helper Classes
27+
28+
- TreeNode: `from leetcode_py.tree_node import TreeNode`
29+
- ListNode: `from leetcode_py.list_node import ListNode`
30+
- New helpers: Add to `leetcode_py/`

.gitignore

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -529,11 +529,11 @@ terragrunt-debug.tfvars.json
529529

530530
### VisualStudioCode ###
531531
.vscode/*
532-
!.vscode/settings.json
533-
!.vscode/tasks.json
534-
!.vscode/launch.json
535-
!.vscode/extensions.json
536-
!.vscode/*.code-snippets
532+
# !.vscode/settings.json
533+
# !.vscode/tasks.json
534+
# !.vscode/launch.json
535+
# !.vscode/extensions.json
536+
# !.vscode/*.code-snippets
537537

538538
# Local History for Visual Studio Code
539539
.history/
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"question_name": "two_sum",
3+
"class_name": "TwoSum",
4+
"method_name": "two_sum",
5+
"problem_number": "1",
6+
"problem_title": "Two Sum",
7+
"difficulty": "Easy",
8+
"topics": "Array, Hash Table",
9+
"_tags": { "list": ["grind-75"] },
10+
"problem_description": "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.",
11+
"_examples": {
12+
"list": [
13+
{ "input": "nums = [2,7,11,15], target = 9", "output": "[0,1]" },
14+
{ "input": "nums = [3,2,4], target = 6", "output": "[1,2]" },
15+
{ "input": "nums = [3,3], target = 6", "output": "[0,1]" }
16+
]
17+
},
18+
"constraints": "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9\n- -10^9 <= target <= 10^9\n- Only one valid answer exists.",
19+
"parameters": "nums: list[int], target: int",
20+
"return_type": "list[int]",
21+
"imports": "",
22+
"test_setup": "",
23+
"test_logging": "",
24+
"_test_cases": {
25+
"list": [
26+
{ "args": [[2, 7, 11, 15], 9], "expected": [0, 1] },
27+
{ "args": [[3, 2, 4], 6], "expected": [1, 2] },
28+
{ "args": [[3, 3], 6], "expected": [0, 1] }
29+
]
30+
},
31+
"param_names": "nums, target, expected",
32+
"input_description": "nums={nums}, target={target}",
33+
"input_params": "nums, target",
34+
"expected_param": "expected",
35+
"method_args": "nums, target",
36+
"test_input_setup": "nums = [2, 7, 11, 15]\ntarget = 9",
37+
"expected_output_setup": "expected = [0, 1]",
38+
"assertion_code": "assert result == expected"
39+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
// Basic problem info
3+
question_name: "two_sum", // snake_case folder name
4+
class_name: "TwoSum", // PascalCase class name
5+
method_name: "two_sum", // snake_case method name
6+
problem_number: "1", // LeetCode problem number
7+
problem_title: "Two Sum", // Display title
8+
difficulty: "Easy", // Easy/Medium/Hard
9+
topics: "Array, Hash Table", // Comma-separated topics
10+
tags: ["grind-75"], // Array of tags
11+
12+
// Problem content
13+
problem_description: "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.",
14+
examples: [
15+
{
16+
input: "nums = [2,7,11,15], target = 9",
17+
output: "[0,1]",
18+
},
19+
{
20+
input: "nums = [3,2,4], target = 6",
21+
output: "[1,2]",
22+
},
23+
],
24+
constraints: "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9",
25+
26+
// Method signature
27+
parameters: "nums: list[int], target: int", // Method parameters with types
28+
return_type: "list[int]", // Return type annotation
29+
imports: "", // Additional imports (e.g., "from leetcode_py.tree_node import TreeNode")
30+
31+
// Test cases
32+
test_cases: [
33+
{
34+
args: [[2, 7, 11, 15], 9], // Method arguments
35+
expected: [0, 1], // Expected result
36+
},
37+
{
38+
args: [[3, 2, 4], 6],
39+
expected: [1, 2],
40+
},
41+
],
42+
43+
// Test template variables (auto-generated, can be customized)
44+
param_names: "nums, target, expected",
45+
input_description: "nums={nums}, target={target}",
46+
input_params: "nums, target",
47+
expected_param: "expected",
48+
method_args: "nums, target",
49+
test_input_setup: "nums = [2, 7, 11, 15]\ntarget = 9",
50+
expected_output_setup: "expected = [0, 1]",
51+
assertion_code: "assert result == expected",
52+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
// Tree problem example
3+
question_name: "invert_binary_tree",
4+
class_name: "InvertBinaryTree",
5+
method_name: "invert_tree",
6+
problem_number: "226",
7+
problem_title: "Invert Binary Tree",
8+
difficulty: "Easy",
9+
topics: "Tree, Depth-First Search, Breadth-First Search, Binary Tree",
10+
tags: ["grind-75"],
11+
12+
problem_description: "Given the root of a binary tree, invert the tree, and return its root.",
13+
examples: [
14+
{
15+
input: "root = [4,2,7,1,3,6,9]",
16+
output: "[4,7,2,9,6,3,1]",
17+
},
18+
{
19+
input: "root = []",
20+
output: "[]",
21+
},
22+
],
23+
constraints: "- The number of nodes in the tree is in the range [0, 100].\n- -100 <= Node.val <= 100",
24+
25+
// Tree-specific configuration
26+
parameters: "root: TreeNode | None",
27+
return_type: "TreeNode | None",
28+
imports: "from leetcode_py.tree_node import TreeNode", // Use shared TreeNode
29+
30+
test_cases: [
31+
{
32+
args: [[4, 2, 7, 1, 3, 6, 9]], // Tree as list representation
33+
expected: [4, 7, 2, 9, 6, 3, 1],
34+
},
35+
{
36+
args: [[]], // Empty tree
37+
expected: [],
38+
},
39+
],
40+
41+
// Tree-specific test setup
42+
param_names: "root, expected",
43+
input_description: "root={root}",
44+
input_params: "root",
45+
expected_param: "expected",
46+
method_args: "root",
47+
test_input_setup: "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])",
48+
expected_output_setup: "expected = [4, 7, 2, 9, 6, 3, 1]",
49+
assertion_code: "assert result == expected",
50+
}

.templates/leetcode/gen.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python3
2+
"""Generate LeetCode problem from JSON using cookiecutter."""
3+
4+
import json
5+
from pathlib import Path
6+
7+
import typer
8+
from cookiecutter.main import cookiecutter
9+
10+
11+
def check_and_prompt_tags(data: dict) -> dict:
12+
"""Check if tags are empty and prompt user for common options."""
13+
import sys
14+
15+
common_tags = ["grind-75", "blind-75", "neetcode-150", "top-interview"]
16+
17+
if "tags" in data and (not data["tags"] or data["tags"] == []):
18+
if sys.stdin.isatty(): # Interactive terminal
19+
typer.echo("\n📋 No tags specified. Would you like to add any common tags?")
20+
typer.echo("Available options:")
21+
for i, tag in enumerate(common_tags, 1):
22+
typer.echo(f" {i}. {tag}")
23+
typer.echo(" 0. Skip (no tags)")
24+
25+
choices_input = typer.prompt("Select options (comma-separated, e.g. '1,2' or '0' to skip)")
26+
27+
try:
28+
choices = [int(x.strip()) for x in choices_input.split(",")]
29+
selected_tags = []
30+
31+
for choice in choices:
32+
if choice == 0:
33+
selected_tags = []
34+
break
35+
elif 1 <= choice <= len(common_tags):
36+
tag = common_tags[choice - 1]
37+
if tag not in selected_tags:
38+
selected_tags.append(tag)
39+
40+
data["tags"] = selected_tags
41+
if selected_tags:
42+
typer.echo(f"✅ Added tags: {', '.join(selected_tags)}")
43+
else:
44+
typer.echo("✅ No tags added")
45+
46+
except ValueError:
47+
typer.echo("⚠️ Invalid input, skipping tags")
48+
data["tags"] = []
49+
50+
return data
51+
52+
53+
def convert_arrays_to_nested(data: dict) -> dict:
54+
"""Convert array fields to cookiecutter-friendly nested format."""
55+
extra_context = data.copy()
56+
array_fields = ["examples", "test_cases", "tags"]
57+
for field in array_fields:
58+
if field in data and isinstance(data[field], list):
59+
extra_context[f"_{field}"] = {"list": data[field]}
60+
del extra_context[field]
61+
return extra_context
62+
63+
64+
def check_overwrite_permission(question_name: str, force: bool) -> None:
65+
"""Check if user wants to overwrite existing problem."""
66+
import sys
67+
68+
if force:
69+
return
70+
71+
output_dir = Path(__file__).parent.parent.parent / "leetcode"
72+
problem_dir = output_dir / question_name
73+
74+
if not problem_dir.exists():
75+
return
76+
77+
typer.echo(f"⚠️ Warning: Question '{question_name}' already exists in leetcode/", err=True)
78+
typer.echo("This will overwrite existing files. Use --force to skip this check.", err=True)
79+
80+
if sys.stdin.isatty(): # Interactive terminal
81+
confirm = typer.confirm("Continue?")
82+
if not confirm:
83+
typer.echo("Cancelled.")
84+
raise typer.Exit(1)
85+
else: # Non-interactive mode
86+
typer.echo("Non-interactive mode: use --force to overwrite.", err=True)
87+
raise typer.Exit(1)
88+
89+
90+
def generate_problem(json_file: str, force: bool = False) -> None:
91+
"""Generate LeetCode problem from JSON file."""
92+
json_path = Path(json_file)
93+
if not json_path.exists():
94+
typer.echo(f"Error: {json_file} not found", err=True)
95+
raise typer.Exit(1)
96+
97+
# Load JSON data
98+
with open(json_path) as f:
99+
data = json.load(f)
100+
101+
# Check and prompt for tags if empty
102+
data = check_and_prompt_tags(data)
103+
104+
# Save updated data back to JSON file
105+
with open(json_path, 'w') as f:
106+
json.dump(data, f, indent=4)
107+
108+
# Convert arrays to cookiecutter-friendly nested format
109+
extra_context = convert_arrays_to_nested(data)
110+
111+
# Check if problem already exists
112+
question_name = extra_context.get("question_name", "unknown")
113+
check_overwrite_permission(question_name, force)
114+
115+
# Generate project using cookiecutter
116+
template_dir = Path(__file__).parent
117+
output_dir = template_dir.parent.parent / "leetcode"
118+
119+
cookiecutter(
120+
str(template_dir),
121+
extra_context=extra_context,
122+
no_input=True,
123+
overwrite_if_exists=True,
124+
output_dir=str(output_dir),
125+
)
126+
127+
typer.echo(f"✅ Generated problem: {question_name}")
128+
129+
130+
if __name__ == "__main__":
131+
typer.run(generate_problem)

0 commit comments

Comments
 (0)