feat(frontend): big O complexity estimator
This commit is contained in:
398
frontend/src/lib/complexity-analyzer.ts
Normal file
398
frontend/src/lib/complexity-analyzer.ts
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
/**
|
||||||
|
* Python code for analyzing Big O complexity of user solutions.
|
||||||
|
* Runs in Pyodide and returns an estimate with explanation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const COMPLEXITY_ANALYZER = `
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
|
||||||
|
class ComplexityAnalyzer(ast.NodeVisitor):
|
||||||
|
"""Analyzes Python code to estimate Big O complexity."""
|
||||||
|
|
||||||
|
def __init__(self, source_code):
|
||||||
|
self.source_code = source_code
|
||||||
|
self.source_lines = source_code.split('\\n')
|
||||||
|
self.findings = []
|
||||||
|
self.loop_depth = 0
|
||||||
|
self.max_loop_depth = 0
|
||||||
|
self.has_recursion = False
|
||||||
|
self.recursive_calls = []
|
||||||
|
self.has_sorting = False
|
||||||
|
self.has_linear_search = False
|
||||||
|
self.has_binary_search_pattern = False
|
||||||
|
self.function_name = None
|
||||||
|
self.input_params = []
|
||||||
|
# Track variables that are dicts or sets (O(1) lookup)
|
||||||
|
self.hash_containers = set()
|
||||||
|
# Track variables that are lists (O(n) lookup)
|
||||||
|
self.list_containers = set()
|
||||||
|
|
||||||
|
def get_line_snippet(self, lineno):
|
||||||
|
"""Get a short snippet of the line."""
|
||||||
|
if 1 <= lineno <= len(self.source_lines):
|
||||||
|
line = self.source_lines[lineno - 1].strip()
|
||||||
|
if len(line) > 50:
|
||||||
|
return line[:47] + "..."
|
||||||
|
return line
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def visit_FunctionDef(self, node):
|
||||||
|
if self.function_name is None:
|
||||||
|
self.function_name = node.name
|
||||||
|
self.input_params = [arg.arg for arg in node.args.args]
|
||||||
|
# Input params that look like lists
|
||||||
|
for arg in node.args.args:
|
||||||
|
if arg.annotation:
|
||||||
|
ann_str = self._node_to_str(arg.annotation)
|
||||||
|
if 'list' in ann_str.lower() or 'List' in ann_str:
|
||||||
|
self.list_containers.add(arg.arg)
|
||||||
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
def visit_Assign(self, node):
|
||||||
|
"""Track variable assignments to identify container types."""
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name):
|
||||||
|
var_name = target.id
|
||||||
|
# Check what's being assigned
|
||||||
|
if isinstance(node.value, ast.Dict):
|
||||||
|
self.hash_containers.add(var_name)
|
||||||
|
elif isinstance(node.value, ast.Set):
|
||||||
|
self.hash_containers.add(var_name)
|
||||||
|
elif isinstance(node.value, ast.Call):
|
||||||
|
if isinstance(node.value.func, ast.Name):
|
||||||
|
func_name = node.value.func.id
|
||||||
|
if func_name in ('dict', 'set', 'defaultdict', 'Counter', 'OrderedDict'):
|
||||||
|
self.hash_containers.add(var_name)
|
||||||
|
elif func_name == 'list':
|
||||||
|
self.list_containers.add(var_name)
|
||||||
|
elif isinstance(node.value, ast.List):
|
||||||
|
self.list_containers.add(var_name)
|
||||||
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
def visit_For(self, node):
|
||||||
|
self.loop_depth += 1
|
||||||
|
self.max_loop_depth = max(self.max_loop_depth, self.loop_depth)
|
||||||
|
|
||||||
|
# Determine what we're iterating over
|
||||||
|
iter_desc = self._describe_iterator(node.iter)
|
||||||
|
|
||||||
|
self.findings.append({
|
||||||
|
"type": "loop",
|
||||||
|
"kind": "for",
|
||||||
|
"line": node.lineno,
|
||||||
|
"snippet": self.get_line_snippet(node.lineno),
|
||||||
|
"depth": self.loop_depth,
|
||||||
|
"iterates_over": iter_desc
|
||||||
|
})
|
||||||
|
|
||||||
|
self.generic_visit(node)
|
||||||
|
self.loop_depth -= 1
|
||||||
|
|
||||||
|
def visit_While(self, node):
|
||||||
|
self.loop_depth += 1
|
||||||
|
self.max_loop_depth = max(self.max_loop_depth, self.loop_depth)
|
||||||
|
|
||||||
|
# Check for binary search pattern (low < high, left <= right, etc.)
|
||||||
|
condition = self._get_condition_pattern(node.test)
|
||||||
|
|
||||||
|
self.findings.append({
|
||||||
|
"type": "loop",
|
||||||
|
"kind": "while",
|
||||||
|
"line": node.lineno,
|
||||||
|
"snippet": self.get_line_snippet(node.lineno),
|
||||||
|
"depth": self.loop_depth,
|
||||||
|
"condition": condition
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check body for binary search indicators (mid = ..., //= 2, etc.)
|
||||||
|
for child in ast.walk(node):
|
||||||
|
if isinstance(child, ast.BinOp) and isinstance(child.op, ast.FloorDiv):
|
||||||
|
if isinstance(child.right, ast.Constant) and child.right.value == 2:
|
||||||
|
self.has_binary_search_pattern = True
|
||||||
|
|
||||||
|
self.generic_visit(node)
|
||||||
|
self.loop_depth -= 1
|
||||||
|
|
||||||
|
def visit_Call(self, node):
|
||||||
|
# Check for recursive calls
|
||||||
|
if isinstance(node.func, ast.Name) and node.func.id == self.function_name:
|
||||||
|
self.has_recursion = True
|
||||||
|
self.recursive_calls.append({
|
||||||
|
"line": node.lineno,
|
||||||
|
"snippet": self.get_line_snippet(node.lineno)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check for sorting
|
||||||
|
if isinstance(node.func, ast.Attribute):
|
||||||
|
if node.func.attr in ('sort', 'sorted'):
|
||||||
|
self.has_sorting = True
|
||||||
|
self.findings.append({
|
||||||
|
"type": "operation",
|
||||||
|
"kind": "sort",
|
||||||
|
"line": node.lineno,
|
||||||
|
"snippet": self.get_line_snippet(node.lineno),
|
||||||
|
"complexity": "O(n log n)"
|
||||||
|
})
|
||||||
|
elif isinstance(node.func, ast.Name) and node.func.id == 'sorted':
|
||||||
|
self.has_sorting = True
|
||||||
|
self.findings.append({
|
||||||
|
"type": "operation",
|
||||||
|
"kind": "sort",
|
||||||
|
"line": node.lineno,
|
||||||
|
"snippet": self.get_line_snippet(node.lineno),
|
||||||
|
"complexity": "O(n log n)"
|
||||||
|
})
|
||||||
|
|
||||||
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
def visit_Compare(self, node):
|
||||||
|
# Check for "x in container" - O(1) for dict/set, O(n) for list
|
||||||
|
for op in node.ops:
|
||||||
|
if isinstance(op, (ast.In, ast.NotIn)):
|
||||||
|
for comparator in node.comparators:
|
||||||
|
if isinstance(comparator, ast.Name):
|
||||||
|
var_name = comparator.id
|
||||||
|
# Check if it's a known hash container (O(1))
|
||||||
|
if var_name in self.hash_containers:
|
||||||
|
# O(1) lookup, no need to flag
|
||||||
|
pass
|
||||||
|
# Check if it's a known list or input param that's a list (O(n))
|
||||||
|
elif var_name in self.list_containers:
|
||||||
|
if self.loop_depth > 0:
|
||||||
|
self.has_linear_search = True
|
||||||
|
self.findings.append({
|
||||||
|
"type": "operation",
|
||||||
|
"kind": "linear_search",
|
||||||
|
"line": node.lineno,
|
||||||
|
"snippet": self.get_line_snippet(node.lineno),
|
||||||
|
"note": f"'in' on list '{var_name}' is O(n)"
|
||||||
|
})
|
||||||
|
# Unknown variable - could be either, don't flag
|
||||||
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
def _describe_iterator(self, node):
|
||||||
|
"""Describe what a for loop is iterating over."""
|
||||||
|
if isinstance(node, ast.Call):
|
||||||
|
if isinstance(node.func, ast.Name):
|
||||||
|
if node.func.id == 'range':
|
||||||
|
if node.args:
|
||||||
|
return self._describe_range_args(node.args)
|
||||||
|
elif node.func.id == 'enumerate':
|
||||||
|
if node.args:
|
||||||
|
return f"enumerate({self._node_to_str(node.args[0])})"
|
||||||
|
elif node.func.id == 'zip':
|
||||||
|
return "zip(...)"
|
||||||
|
elif isinstance(node, ast.Name):
|
||||||
|
return node.id
|
||||||
|
elif isinstance(node, ast.Attribute):
|
||||||
|
return f"{self._node_to_str(node.value)}.{node.attr}"
|
||||||
|
elif isinstance(node, ast.Subscript):
|
||||||
|
return self._node_to_str(node)
|
||||||
|
return "iterable"
|
||||||
|
|
||||||
|
def _describe_range_args(self, args):
|
||||||
|
"""Describe range() arguments."""
|
||||||
|
if len(args) == 1:
|
||||||
|
return f"range({self._node_to_str(args[0])})"
|
||||||
|
elif len(args) >= 2:
|
||||||
|
return f"range({self._node_to_str(args[0])}, {self._node_to_str(args[1])})"
|
||||||
|
return "range(...)"
|
||||||
|
|
||||||
|
def _node_to_str(self, node):
|
||||||
|
"""Convert an AST node to a simple string representation."""
|
||||||
|
if isinstance(node, ast.Name):
|
||||||
|
return node.id
|
||||||
|
elif isinstance(node, ast.Constant):
|
||||||
|
return str(node.value)
|
||||||
|
elif isinstance(node, ast.Call):
|
||||||
|
if isinstance(node.func, ast.Name):
|
||||||
|
return f"{node.func.id}(...)"
|
||||||
|
elif isinstance(node, ast.Attribute):
|
||||||
|
return f"{self._node_to_str(node.value)}.{node.attr}"
|
||||||
|
elif isinstance(node, ast.BinOp):
|
||||||
|
left = self._node_to_str(node.left)
|
||||||
|
right = self._node_to_str(node.right)
|
||||||
|
op = self._op_to_str(node.op)
|
||||||
|
return f"{left} {op} {right}"
|
||||||
|
elif isinstance(node, ast.Subscript):
|
||||||
|
return f"{self._node_to_str(node.value)}[...]"
|
||||||
|
return "..."
|
||||||
|
|
||||||
|
def _op_to_str(self, op):
|
||||||
|
"""Convert operator to string."""
|
||||||
|
ops = {
|
||||||
|
ast.Add: "+", ast.Sub: "-", ast.Mult: "*", ast.Div: "/",
|
||||||
|
ast.FloorDiv: "//", ast.Mod: "%", ast.Pow: "**"
|
||||||
|
}
|
||||||
|
return ops.get(type(op), "?")
|
||||||
|
|
||||||
|
def _get_condition_pattern(self, node):
|
||||||
|
"""Describe a while loop condition."""
|
||||||
|
return self._node_to_str(node)
|
||||||
|
|
||||||
|
def analyze(self):
|
||||||
|
"""Run the analysis and return results."""
|
||||||
|
try:
|
||||||
|
tree = ast.parse(self.source_code)
|
||||||
|
self.visit(tree)
|
||||||
|
except SyntaxError as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Syntax error: {e}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine overall complexity
|
||||||
|
complexity, explanation = self._determine_complexity()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"complexity": complexity,
|
||||||
|
"explanation": explanation,
|
||||||
|
"details": {
|
||||||
|
"max_loop_depth": self.max_loop_depth,
|
||||||
|
"has_recursion": self.has_recursion,
|
||||||
|
"has_sorting": self.has_sorting,
|
||||||
|
"has_binary_search_pattern": self.has_binary_search_pattern,
|
||||||
|
"findings": self.findings,
|
||||||
|
"recursive_calls": self.recursive_calls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _determine_complexity(self):
|
||||||
|
"""Determine the overall complexity based on findings."""
|
||||||
|
explanations = []
|
||||||
|
complexity_factors = []
|
||||||
|
|
||||||
|
# Handle recursion
|
||||||
|
if self.has_recursion:
|
||||||
|
if self.has_binary_search_pattern:
|
||||||
|
complexity_factors.append(("log n", "binary search recursion"))
|
||||||
|
explanations.append("Recursion with halving pattern suggests O(log n)")
|
||||||
|
else:
|
||||||
|
complexity_factors.append(("?", "recursion detected"))
|
||||||
|
explanations.append(f"Recursion found - complexity depends on recurrence relation")
|
||||||
|
for rc in self.recursive_calls:
|
||||||
|
explanations.append(f" Line {rc['line']}: {rc['snippet']}")
|
||||||
|
|
||||||
|
# Handle loops
|
||||||
|
if self.max_loop_depth > 0:
|
||||||
|
loop_findings = [f for f in self.findings if f["type"] == "loop"]
|
||||||
|
|
||||||
|
if self.has_binary_search_pattern and self.max_loop_depth == 1:
|
||||||
|
complexity_factors.append(("log n", "binary search loop"))
|
||||||
|
explanations.append("While loop with halving pattern: O(log n)")
|
||||||
|
else:
|
||||||
|
# Describe each loop level
|
||||||
|
for finding in loop_findings:
|
||||||
|
snippet = finding.get("snippet", "")
|
||||||
|
depth_info = f" (nested, depth {finding['depth']})" if finding['depth'] > 1 else ""
|
||||||
|
if finding["kind"] == "for":
|
||||||
|
iter_over = finding.get("iterates_over", "iterable")
|
||||||
|
explanations.append(
|
||||||
|
f"Loop over {iter_over}{depth_info} → O(n)"
|
||||||
|
)
|
||||||
|
if snippet:
|
||||||
|
explanations.append(f" └─ {snippet}")
|
||||||
|
else:
|
||||||
|
condition = finding.get("condition", "...")
|
||||||
|
explanations.append(
|
||||||
|
f"While loop{depth_info}"
|
||||||
|
)
|
||||||
|
if snippet:
|
||||||
|
explanations.append(f" └─ {snippet}")
|
||||||
|
|
||||||
|
# Estimate based on nesting
|
||||||
|
if self.max_loop_depth == 1:
|
||||||
|
complexity_factors.append(("n", "single loop"))
|
||||||
|
elif self.max_loop_depth == 2:
|
||||||
|
complexity_factors.append(("n²", "nested loops"))
|
||||||
|
elif self.max_loop_depth == 3:
|
||||||
|
complexity_factors.append(("n³", "triple nested loops"))
|
||||||
|
else:
|
||||||
|
complexity_factors.append((f"n^{self.max_loop_depth}", f"{self.max_loop_depth} nested loops"))
|
||||||
|
|
||||||
|
# Handle sorting
|
||||||
|
if self.has_sorting:
|
||||||
|
complexity_factors.append(("n log n", "sorting"))
|
||||||
|
sort_findings = [f for f in self.findings if f.get("kind") == "sort"]
|
||||||
|
for sf in sort_findings:
|
||||||
|
explanations.append(f"Line {sf['line']}: sorting operation O(n log n)")
|
||||||
|
|
||||||
|
# Handle linear search in loop
|
||||||
|
if self.has_linear_search and self.max_loop_depth >= 1:
|
||||||
|
explanations.append("Note: 'in' operator on list inside loop adds O(n) per iteration")
|
||||||
|
# This could bump n to n², but let's just note it
|
||||||
|
|
||||||
|
# Combine factors to get final complexity
|
||||||
|
if not complexity_factors:
|
||||||
|
return "O(1)", ["No loops or recursion detected - constant time"]
|
||||||
|
|
||||||
|
# Take the dominant term
|
||||||
|
complexity = self._get_dominant_complexity(complexity_factors)
|
||||||
|
|
||||||
|
return f"O({complexity})", explanations
|
||||||
|
|
||||||
|
def _get_dominant_complexity(self, factors):
|
||||||
|
"""Get the dominant complexity from a list of factors."""
|
||||||
|
# Priority order (higher = more complex)
|
||||||
|
priority = {
|
||||||
|
"1": 0, "log n": 1, "n": 2, "n log n": 3,
|
||||||
|
"n²": 4, "n³": 5, "n^4": 6, "2^n": 7, "n!": 8, "?": 10
|
||||||
|
}
|
||||||
|
|
||||||
|
dominant = "1"
|
||||||
|
for factor, _ in factors:
|
||||||
|
if factor.startswith("n^"):
|
||||||
|
try:
|
||||||
|
exp = int(factor[2:])
|
||||||
|
if exp > priority.get(dominant, 0):
|
||||||
|
dominant = factor
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif priority.get(factor, 0) > priority.get(dominant, 0):
|
||||||
|
dominant = factor
|
||||||
|
|
||||||
|
# Handle sorting + loops
|
||||||
|
has_sort = any(f[0] == "n log n" for f in factors)
|
||||||
|
has_n2 = any(f[0] == "n²" for f in factors)
|
||||||
|
|
||||||
|
if has_sort and not has_n2 and dominant == "n":
|
||||||
|
return "n log n"
|
||||||
|
|
||||||
|
return dominant
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_complexity(code):
|
||||||
|
"""Main entry point for complexity analysis."""
|
||||||
|
analyzer = ComplexityAnalyzer(code)
|
||||||
|
return analyzer.analyze()
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
export interface ComplexityResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
complexity?: string;
|
||||||
|
explanation?: string[];
|
||||||
|
details?: {
|
||||||
|
max_loop_depth: number;
|
||||||
|
has_recursion: boolean;
|
||||||
|
has_sorting: boolean;
|
||||||
|
has_binary_search_pattern: boolean;
|
||||||
|
findings: Array<{
|
||||||
|
type: string;
|
||||||
|
kind: string;
|
||||||
|
line: number;
|
||||||
|
snippet: string;
|
||||||
|
depth?: number;
|
||||||
|
iterates_over?: string;
|
||||||
|
condition?: string;
|
||||||
|
complexity?: string;
|
||||||
|
note?: string;
|
||||||
|
}>;
|
||||||
|
recursive_calls: Array<{
|
||||||
|
line: number;
|
||||||
|
snippet: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
200
frontend/src/lib/python-helpers.ts
Normal file
200
frontend/src/lib/python-helpers.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* Python helper code and detection utilities for the test runner.
|
||||||
|
*
|
||||||
|
* This module provides Python code as string constants for data structure
|
||||||
|
* classes, conversion functions, and utilities to detect problem types.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Data Structure Classes
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const TREE_NODE_CLASS = `
|
||||||
|
class TreeNode:
|
||||||
|
def __init__(self, val=0, left=None, right=None):
|
||||||
|
self.val = val
|
||||||
|
self.left = left
|
||||||
|
self.right = right
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
export const LIST_NODE_CLASS = `
|
||||||
|
class ListNode:
|
||||||
|
def __init__(self, val=0, next=None):
|
||||||
|
self.val = val
|
||||||
|
self.next = next
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Conversion Functions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a binary tree from a level-order array representation.
|
||||||
|
* Example: [1, 2, 3, null, 4] -> TreeNode(1, TreeNode(2, None, TreeNode(4)), TreeNode(3))
|
||||||
|
*/
|
||||||
|
export const BUILD_TREE = `
|
||||||
|
def __build_tree(arr):
|
||||||
|
if not arr or arr[0] is None:
|
||||||
|
return None
|
||||||
|
root = TreeNode(arr[0])
|
||||||
|
queue = [root]
|
||||||
|
i = 1
|
||||||
|
while queue and i < len(arr):
|
||||||
|
node = queue.pop(0)
|
||||||
|
if i < len(arr) and arr[i] is not None:
|
||||||
|
node.left = TreeNode(arr[i])
|
||||||
|
queue.append(node.left)
|
||||||
|
i += 1
|
||||||
|
if i < len(arr) and arr[i] is not None:
|
||||||
|
node.right = TreeNode(arr[i])
|
||||||
|
queue.append(node.right)
|
||||||
|
i += 1
|
||||||
|
return root
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a binary tree to a level-order array representation.
|
||||||
|
* Example: TreeNode(1, TreeNode(2), TreeNode(3)) -> [1, 2, 3]
|
||||||
|
*/
|
||||||
|
export const TREE_TO_ARRAY = `
|
||||||
|
def __tree_to_array(root):
|
||||||
|
if root is None:
|
||||||
|
return []
|
||||||
|
result = []
|
||||||
|
queue = [root]
|
||||||
|
while queue:
|
||||||
|
node = queue.pop(0)
|
||||||
|
if node is None:
|
||||||
|
result.append(None)
|
||||||
|
else:
|
||||||
|
result.append(node.val)
|
||||||
|
queue.append(node.left)
|
||||||
|
queue.append(node.right)
|
||||||
|
# Trim trailing nulls
|
||||||
|
while result and result[-1] is None:
|
||||||
|
result.pop()
|
||||||
|
return result
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a linked list from an array.
|
||||||
|
* Example: [1, 2, 3] -> ListNode(1, ListNode(2, ListNode(3)))
|
||||||
|
*/
|
||||||
|
export const BUILD_LIST = `
|
||||||
|
def __build_list(arr):
|
||||||
|
if not arr:
|
||||||
|
return None
|
||||||
|
head = ListNode(arr[0])
|
||||||
|
current = head
|
||||||
|
for val in arr[1:]:
|
||||||
|
current.next = ListNode(val)
|
||||||
|
current = current.next
|
||||||
|
return head
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a linked list to an array.
|
||||||
|
* Example: ListNode(1, ListNode(2, ListNode(3))) -> [1, 2, 3]
|
||||||
|
*/
|
||||||
|
export const LIST_TO_ARRAY = `
|
||||||
|
def __list_to_array(head):
|
||||||
|
result = []
|
||||||
|
current = head
|
||||||
|
while current:
|
||||||
|
result.append(current.val)
|
||||||
|
current = current.next
|
||||||
|
return result
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Problem Type Detection
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/** Parameter names that indicate a tree-based problem */
|
||||||
|
export const TREE_PARAMS = ["root", "tree", "p", "q"];
|
||||||
|
|
||||||
|
/** Parameter names that indicate a linked-list problem */
|
||||||
|
export const LIST_PARAMS = ["head", "l1", "l2", "list1", "list2", "node"];
|
||||||
|
|
||||||
|
export type ProblemType = "simple" | "tree" | "linkedlist" | "class-based";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the problem type based on the function signature and input.
|
||||||
|
*
|
||||||
|
* @param signature - The function signature (e.g., "def two_sum(nums, target):")
|
||||||
|
* @param input - The test case input object
|
||||||
|
* @returns The detected problem type
|
||||||
|
*/
|
||||||
|
export function detectProblemType(
|
||||||
|
signature: string,
|
||||||
|
input: Record<string, unknown>
|
||||||
|
): ProblemType {
|
||||||
|
// Class-based: check if input has "operations" key
|
||||||
|
if ("operations" in input) {
|
||||||
|
return "class-based";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check signature for type hints
|
||||||
|
const hasTreeNode = signature.includes("TreeNode");
|
||||||
|
const hasListNode = signature.includes("ListNode");
|
||||||
|
|
||||||
|
if (hasTreeNode) {
|
||||||
|
return "tree";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasListNode) {
|
||||||
|
return "linkedlist";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check parameter names in signature and input keys
|
||||||
|
const inputKeys = Object.keys(input);
|
||||||
|
|
||||||
|
const hasTreeParam =
|
||||||
|
TREE_PARAMS.some((param) => signature.includes(param)) ||
|
||||||
|
inputKeys.some((key) => TREE_PARAMS.includes(key));
|
||||||
|
|
||||||
|
const hasListParam =
|
||||||
|
LIST_PARAMS.some((param) => signature.includes(param)) ||
|
||||||
|
inputKeys.some((key) => LIST_PARAMS.includes(key));
|
||||||
|
|
||||||
|
if (hasTreeParam) {
|
||||||
|
return "tree";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasListParam) {
|
||||||
|
return "linkedlist";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "simple";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Python helper code needed for a given problem type.
|
||||||
|
*/
|
||||||
|
export function getPythonHelpers(problemType: ProblemType): string {
|
||||||
|
switch (problemType) {
|
||||||
|
case "tree":
|
||||||
|
return [TREE_NODE_CLASS, BUILD_TREE, TREE_TO_ARRAY].join("\n\n");
|
||||||
|
case "linkedlist":
|
||||||
|
return [LIST_NODE_CLASS, BUILD_LIST, LIST_TO_ARRAY].join("\n\n");
|
||||||
|
case "class-based":
|
||||||
|
return ""; // Class-based problems define their own class
|
||||||
|
case "simple":
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of parameter names that need tree conversion.
|
||||||
|
*/
|
||||||
|
export function getTreeParams(input: Record<string, unknown>): string[] {
|
||||||
|
return Object.keys(input).filter((key) => TREE_PARAMS.includes(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of parameter names that need linked list conversion.
|
||||||
|
*/
|
||||||
|
export function getListParams(input: Record<string, unknown>): string[] {
|
||||||
|
return Object.keys(input).filter((key) => LIST_PARAMS.includes(key));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user