Skip to content

Commit 2e6875d

Browse files
committed
Add batch of 15 missing LeetCode problems
1 parent 56e51eb commit 2e6875d

File tree

101 files changed

+3406
-723
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+3406
-723
lines changed

add_problems.py

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import json
2+
import os
3+
import re
4+
import shutil
5+
import subprocess
6+
import sys
7+
import time
8+
9+
10+
def get_missing_slugs():
11+
with open("batch_1_slugs.txt", "r") as f:
12+
return [line.strip() for line in f if line.strip()]
13+
14+
15+
def transform_scraped_data(data):
16+
"""Transforms scraped data into the format expected by cookiecutter/gen."""
17+
transformed = {}
18+
19+
# Basic fields
20+
# Sanitize problem_name
21+
problem_name = data.get("slug", "").replace("-", "_")
22+
if problem_name and problem_name[0].isdigit():
23+
problem_name = f"_{problem_name}"
24+
transformed["problem_name"] = problem_name
25+
transformed["problem_number"] = data.get("number", "0")
26+
transformed["problem_title"] = data.get("title", "")
27+
transformed["difficulty"] = data.get("difficulty", "Medium")
28+
29+
# Topics: list -> string
30+
topics = data.get("topics", [])
31+
if isinstance(topics, list):
32+
transformed["topics"] = ", ".join(topics)
33+
else:
34+
transformed["topics"] = str(topics)
35+
36+
# Description
37+
transformed["readme_description"] = data.get("description", "")
38+
39+
# Constraints: list -> string
40+
constraints = data.get("constraints", [])
41+
if isinstance(constraints, list):
42+
transformed["readme_constraints"] = "\n".join([f"- {c}" for c in constraints])
43+
else:
44+
transformed["readme_constraints"] = str(constraints)
45+
46+
# Examples
47+
examples = data.get("examples", [])
48+
readme_examples = []
49+
for ex in examples:
50+
# Scraped example might be dict or string?
51+
# 01_matrix.json shows examples as empty list []?
52+
# But raw_content has them.
53+
# If examples is list of dicts with 'text' or 'input'/'output'
54+
if isinstance(ex, dict):
55+
content = ex.get("text", "")
56+
if not content:
57+
inp = ex.get("input", "")
58+
out = ex.get("output", "")
59+
if inp and out:
60+
content = f"```\nInput: {inp}\nOutput: {out}\n```"
61+
if content:
62+
readme_examples.append({"content": content})
63+
64+
transformed["_readme_examples"] = {"list": readme_examples}
65+
66+
# Python Code Parsing
67+
python_code = data.get("python_code", "")
68+
solution_class_name = "Solution"
69+
method_name = "unknown"
70+
method_signature = ""
71+
method_body = " pass"
72+
73+
# Simple regex to find class and method
74+
class_match = re.search(r"class\s+(\w+):", python_code)
75+
if class_match:
76+
solution_class_name = class_match.group(1)
77+
78+
# Find method def
79+
# def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
80+
method_match = re.search(r"def\s+(\w+)\s*\((.*?)\)\s*(->\s*.*?)?:", python_code)
81+
if method_match:
82+
method_name = method_match.group(1)
83+
params = method_match.group(2)
84+
return_type = method_match.group(3) or ""
85+
86+
# Clean params to remove 'self'
87+
param_list = [p.strip() for p in params.split(",") if p.strip()]
88+
if param_list and param_list[0] == "self":
89+
param_list = param_list[1:]
90+
91+
clean_params = ", ".join(param_list)
92+
93+
# Construct signature
94+
if clean_params:
95+
method_signature = f"(self, {clean_params}){return_type}"
96+
else:
97+
method_signature = f"(self){return_type}"
98+
99+
# Snake case method name
100+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", method_name)
101+
snake_method_name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
102+
103+
method_body = f" # TODO: Implement {snake_method_name}\n return "
104+
if "List" in return_type:
105+
method_body += "[]"
106+
elif "int" in return_type:
107+
method_body += "0"
108+
elif "bool" in return_type:
109+
method_body += "False"
110+
elif "str" in return_type:
111+
method_body += '""'
112+
else:
113+
method_body += "None"
114+
115+
transformed["solution_class_name"] = solution_class_name
116+
117+
transformed["_solution_methods"] = {
118+
"list": [{"name": snake_method_name, "signature": method_signature, "body": method_body}]
119+
}
120+
121+
# Helpers and Tests
122+
transformed["helpers_run_name"] = snake_method_name
123+
124+
if clean_params:
125+
helper_sig = f"(solution_class: type, {clean_params})"
126+
else:
127+
helper_sig = "(solution_class: type)"
128+
129+
transformed["helpers_run_signature"] = helper_sig
130+
131+
# Extract argument names for the call
132+
arg_names = [p.split(":")[0].strip() for p in param_list]
133+
call_args = ", ".join(arg_names)
134+
135+
transformed["helpers_run_body"] = (
136+
f" implementation = solution_class()\n"
137+
f" return implementation.{snake_method_name}({call_args})"
138+
)
139+
140+
transformed["helpers_assert_name"] = snake_method_name
141+
# Infer expected type from return type
142+
expected_type = return_type.replace("->", "").strip()
143+
transformed["helpers_assert_signature"] = (
144+
f"(result: {expected_type}, expected: {expected_type}) -> bool"
145+
)
146+
transformed["helpers_assert_body"] = " assert result == expected\n return True"
147+
148+
transformed["test_class_name"] = "".join(
149+
[word.capitalize() for word in transformed["problem_name"].split("_")]
150+
)
151+
transformed["test_class_content"] = (
152+
" def setup_method(self):\n self.solution = Solution()"
153+
)
154+
155+
# Test Cases
156+
formatted_test_cases = []
157+
raw_test_cases = data.get("test_cases", [])
158+
159+
# Count args
160+
arg_count = len(arg_names)
161+
162+
for tc in raw_test_cases:
163+
# Expected length is arg_count + 1 (for expected output)
164+
if len(tc) == arg_count + 1:
165+
args = ", ".join(tc[:-1])
166+
expected = tc[-1]
167+
formatted_test_cases.append(f"({args}, {expected})")
168+
else:
169+
print(
170+
f"[{data.get('slug')}] Warning: Skipping invalid test case "
171+
f"(len={len(tc)}, expected={arg_count+1}): {tc}"
172+
)
173+
174+
# Parametrize string
175+
if call_args:
176+
parametrize_str = f"{call_args}, expected"
177+
else:
178+
parametrize_str = "expected"
179+
180+
# Test signature
181+
if clean_params:
182+
test_sig = f"(self, {clean_params}, expected: {expected_type})"
183+
else:
184+
test_sig = f"(self, expected: {expected_type})"
185+
186+
transformed["_test_methods"] = {
187+
"list": [
188+
{
189+
"name": f"test_{snake_method_name}",
190+
"signature": test_sig,
191+
"parametrize": parametrize_str,
192+
"test_cases": {"list": formatted_test_cases},
193+
"body": (
194+
f" result = run_{snake_method_name}(Solution, {call_args})\n"
195+
f" assert_{snake_method_name}(result, expected)"
196+
),
197+
}
198+
]
199+
}
200+
201+
# Imports
202+
typing_imports = []
203+
for type_name in ["List", "Optional", "Dict", "Set", "Tuple"]:
204+
if type_name in method_signature:
205+
typing_imports.append(type_name)
206+
207+
if typing_imports:
208+
typing_str = f"from typing import {', '.join(typing_imports)}"
209+
transformed["solution_imports"] = typing_str
210+
transformed["test_imports"] = (
211+
f"import pytest\n{typing_str}\nfrom leetcode_py import logged_test\n"
212+
f"from .helpers import assert_{snake_method_name}, run_{snake_method_name}\n"
213+
f"from .solution import Solution"
214+
)
215+
transformed["helpers_imports"] = typing_str
216+
else:
217+
transformed["solution_imports"] = ""
218+
transformed["test_imports"] = (
219+
f"import pytest\nfrom leetcode_py import logged_test\n"
220+
f"from .helpers import assert_{snake_method_name}, run_{snake_method_name}\n"
221+
f"from .solution import Solution"
222+
)
223+
transformed["helpers_imports"] = ""
224+
225+
return transformed
226+
227+
228+
def process_problem(slug):
229+
scrape_slug = slug.replace("_", "-")
230+
231+
print(f"\n[{slug}] Starting process...")
232+
233+
try:
234+
# 1. Scrape
235+
print(f"[{slug}] Scraping {scrape_slug}...")
236+
result = subprocess.run(
237+
["uv", "run", "python", "-m", "leetcode_py.cli.main", "scrape", "-s", scrape_slug],
238+
check=False,
239+
capture_output=True,
240+
text=True,
241+
)
242+
243+
if result.returncode != 0:
244+
print(f"[{slug}] Scrape FAILED with code {result.returncode}.")
245+
return False
246+
247+
output = result.stdout
248+
json_start = output.find("{")
249+
if json_start == -1:
250+
print(f"[{slug}] Scrape output does not contain JSON start.")
251+
return False
252+
253+
json_content = output[json_start:]
254+
try:
255+
data = json.loads(json_content)
256+
except json.JSONDecodeError:
257+
print(f"[{slug}] Extracted content is not valid JSON.")
258+
return False
259+
260+
# TRANSFORM DATA
261+
transformed_data = transform_scraped_data(data)
262+
263+
# Convert to snake_case for file naming
264+
snake_slug = slug.replace("-", "_")
265+
266+
# If starts with digit, prepend _
267+
if snake_slug[0].isdigit():
268+
snake_slug = f"_{snake_slug}"
269+
270+
# Save to file
271+
json_dir = "leetcode_py/cli/resources/leetcode/json/problems"
272+
os.makedirs(json_dir, exist_ok=True)
273+
json_path = os.path.join(json_dir, f"{snake_slug}.json")
274+
275+
with open(json_path, "w") as f:
276+
json.dump(transformed_data, f, indent=2)
277+
278+
abs_json_path = os.path.abspath(json_path)
279+
print(f"[{slug}] Scrape successful. Saved to {abs_json_path}")
280+
281+
# 2. Generate
282+
print(f"[{snake_slug}] Generating...")
283+
gen_result = subprocess.run(
284+
[
285+
"uv",
286+
"run",
287+
"python",
288+
"-m",
289+
"leetcode_py.cli.main",
290+
"gen",
291+
"-s",
292+
snake_slug,
293+
"-o",
294+
"leetcode",
295+
"--force",
296+
],
297+
check=False,
298+
capture_output=True,
299+
text=True,
300+
)
301+
302+
if gen_result.returncode != 0:
303+
print(f"[{slug}] Generation FAILED.")
304+
print(f"Error output: {gen_result.stderr}")
305+
print(f"Standard output: {gen_result.stdout}")
306+
return False
307+
308+
print(f"[{slug}] Generation successful.")
309+
return True
310+
311+
except Exception as e:
312+
print(f"[{slug}] Unexpected error: {e}")
313+
return False
314+
315+
316+
def main():
317+
if not shutil.which("uv"):
318+
print("Error: 'uv' executable not found in PATH.")
319+
sys.exit(1)
320+
321+
slugs = get_missing_slugs()
322+
print(f"Found {len(slugs)} problems to process.")
323+
324+
success_count = 0
325+
failed_slugs = []
326+
327+
for i, slug in enumerate(slugs):
328+
print(f"\n--- Processing {i+1}/{len(slugs)}: {slug} ---")
329+
if process_problem(slug):
330+
success_count += 1
331+
else:
332+
failed_slugs.append(slug)
333+
334+
time.sleep(0.5)
335+
336+
print("\n" + "=" * 30)
337+
print(f"Completed. Success: {success_count}, Failed: {len(failed_slugs)}")
338+
339+
if failed_slugs:
340+
print("\nFailed slugs:")
341+
for s in failed_slugs:
342+
print(s)
343+
344+
with open("failed_slugs.txt", "w") as f:
345+
for s in failed_slugs:
346+
f.write(s + "\n")
347+
print("Failed slugs written to failed_slugs.txt")
348+
else:
349+
if os.path.exists("failed_slugs.txt"):
350+
os.remove("failed_slugs.txt")
351+
352+
353+
if __name__ == "__main__":
354+
main()

0 commit comments

Comments
 (0)