├── .gitignore ├── README.md ├── cookies └── .gitignore ├── get.sh ├── lchelper ├── __init__.py ├── codegen │ ├── __init__.py │ ├── base.py │ ├── cpp.py │ └── python.py ├── common.py ├── crawler.py ├── logging.py ├── parser.py └── utils.py ├── main.py ├── requirements.txt └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | /contest_problems.pkl 3 | /contest_cpp/ 4 | /contest_python/ 5 | 6 | ### Python template 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # celery beat schedule file 100 | celerybeat-schedule 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LeetCode Contest Helper 2 | 3 | > A LeetCode contest utility for the dead serious. 4 | 5 | 6 | ## A Bit of Background... 7 | 8 | If you've taken part in [LeetCode contests](https://leetcode.com/contest/), you might have been annoyed by their 9 | ridiculous sample input formats. I know I have. The format of LeetCode format is similar to that of TopCoder -- you're 10 | required to implement a specific function in a "Solution" class, and input is provided as arguments, while output is the 11 | return value. While this alleviates the burden of hand-written I/O, it makes local testing especially difficult. 12 | 13 | TopCoder contestants might be familiar with Arena plug-ins like 14 | [TZTester](https://community.topcoder.com/contest/classes/TZTester/TZTester.html), which parses test cases and 15 | generates a template code file that runs your solution against the test cases locally. Well, that's what this project 16 | aims to do. 17 | 18 | 19 | ## Usage 20 | 21 | 1. Install [Selenium](https://selenium-python.readthedocs.io/installation.html) and the 22 | [Chrome web driver](https://sites.google.com/a/chromium.org/chromedriver/downloads) by following instructions 23 | in the links. 24 | 25 | **Note:** Although not tested, the code should also work with other web drivers supported by Selenium. Please change 26 | the code accordingly if you wish to use alternative browsers. 27 | 2. Clone the repository: 28 | ```bash 29 | git clone https://github.com/huzecong/leetcode-contest-helper.git 30 | cd leetcode-contest-helper 31 | ``` 32 | 3. Login using your LeetCode account: 33 | ```bash 34 | python main.py login 35 | ``` 36 | A browser window will open and navigate to the LeetCode login page. Please enter your credentials and click login 37 | (and get pass CAPTCHAs if they pop up). 38 | 39 | If your account is registered on [LeetCode-CN](https://leetcode-cn.com), use this command instead: 40 | ```bash 41 | python main.py login -s leetcode-cn 42 | ``` 43 | 44 | **Note:** Unfortunately, it is not possible to access problem statements without logging in, as LeetCode prevents you 45 | from accessing the problems unless you have taken part in the contest or have a premium subscription. LCHelper stores 46 | your cookies and uses them to access the problems. Don't worry, your sensitive information is always stored locally. 47 | 48 | **Note:** Third-party login is not supported as of now. 49 | 4. Download problem descriptions and generate testing code in your favorite language: 50 | ```bash 51 | python main.py get [-l ] 52 | ``` 53 | For instance, to generate testing code in C++ and Python for 54 | [Weekly Contest 163](https://leetcode.com/contest/weekly-contest-163), run the command: 55 | ```bash 56 | python main.py get -l cpp -l python -o projects/ https://leetcode.com/contest/weekly-contest-163 57 | ``` 58 | This will generate two folders under the folder `projects/`: 59 | 60 | - `weekly-contest-163_cpp`: C++ code of problems in the contest. 61 | - `weekly-contest-163_python`: Python code of problems in the contest. 62 | 63 | 64 | ## Instructions for Using Generated Code 65 | 66 | The project folder will contain one code file for each problem, and potentially other files required for compiling or 67 | testing. Problems are renamed to single uppercase letters (in the same order as on the web page) for simplicity. 68 | 69 | The generated code contains a certain amount of boilerplate code for debugging. When submitting, remember to copy 70 | everything between the comments `BEGIN SUBMIT` and `END SUBMIT`. 71 | 72 | You can add your custom code template to the generated code. Currently, this is only possible through modifying the code 73 | for LCHelper: 74 | 75 | 1. Find the code generator class for your language. The C++ generator is located in `lchelper/codegen/cpp.py`, and the 76 | Python generator in `lchelper/codegen/python.py`. 77 | 2. Add a property named `user_template_code`, and make it return you code template. The syntax looks like this: 78 | ```python 79 | @property 80 | def user_template_code(self) -> str: 81 | return r""" 82 | template 83 | void my_amazing_debug_function(Args ...args) { 84 | // ... 85 | } 86 | """ 87 | ``` 88 | The property might already exist (it does in C++), in this case, feel free to replace it with your own. 89 | 90 | See below for language-specific instructions. Currently, only C++ and Python are supported. 91 | 92 | ### C++ 93 | 94 | The C++ project folder contains these additional files: 95 | 96 | - `CMakeLists.txt`, CMake configuration for building the project. 97 | - `_testing.h`, a header-only library for comparing outputs. 98 | - `_boilerplate.h`, boilerplate code for LeetCode-specific stuff. 99 | 100 | The generated C++ project builds using CMake. To compile the problems, run the following commands: 101 | ```bash 102 | cmake . 103 | make 104 | ./A # to run tests for problem A 105 | ``` 106 | You can also use IDEs (e.g., JetBrains CLion) to automate the process. 107 | 108 | 109 | ## Disclaimer 110 | 111 | - This tool is not affiliated, associated, authorized, endorsed by, or in any way officially connected with LeetCode. 112 | - This tool is not guaranteed to generate correct code, although the author tried their best to prevent such cases. 113 | - This tool is not (and will not be) capable of automatically generating solutions. 114 | - This tool does not grant you access to premium LeetCode problems that you cannot view with your personal account. 115 | - Passing test cases within the generated code does not guarantee the correctness of your solution. 116 | 117 | 118 | ## TODO 119 | 120 | - [ ] Automatic submission 121 | - [ ] Third-party login 122 | - [ ] Interactive problems with a query class 123 | -------------------------------------------------------------------------------- /cookies/.gitignore: -------------------------------------------------------------------------------- 1 | *.dat 2 | -------------------------------------------------------------------------------- /get.sh: -------------------------------------------------------------------------------- 1 | if [[ $1 == "--debug" ]]; then 2 | shift 3 | python main.py --debug get -l cpp -l python -p contest $@ 4 | else 5 | python main.py get -l cpp -l python -p contest $@ 6 | fi 7 | -------------------------------------------------------------------------------- /lchelper/__init__.py: -------------------------------------------------------------------------------- 1 | from . import utils 2 | from .codegen import * 3 | from .common import * 4 | from .crawler import * 5 | from .logging import * 6 | from .parser import * 7 | -------------------------------------------------------------------------------- /lchelper/codegen/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import CodeGen 2 | from .cpp import CppCodeGen 3 | from .python import PythonCodeGen 4 | 5 | __all__ = [ 6 | "create_codegen", 7 | "LANGUAGES", 8 | ] 9 | 10 | 11 | def create_codegen(lang: str) -> CodeGen: 12 | return LANGUAGES[lang]() 13 | 14 | 15 | LANGUAGES = { 16 | "cpp": CppCodeGen, 17 | "python": PythonCodeGen, 18 | } 19 | -------------------------------------------------------------------------------- /lchelper/codegen/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import os 3 | import shutil 4 | import traceback 5 | from datetime import datetime 6 | from typing import Dict, Iterable, List, Tuple, TypeVar, Union 7 | 8 | from lchelper.common import * 9 | from lchelper.logging import log 10 | from lchelper.parser import parse_problem 11 | 12 | __all__ = [ 13 | "Code", 14 | "Signature", 15 | "CodeGen", 16 | ] 17 | 18 | T = TypeVar("T") 19 | Signature = Union[ProblemSignature, InteractiveProblemSignature] 20 | Code = List[str] 21 | 22 | 23 | class CodeGen(abc.ABC): 24 | @property 25 | @abc.abstractmethod 26 | def language(self) -> str: 27 | """Name of the language to generate.""" 28 | raise NotImplementedError 29 | 30 | @property 31 | def extra_files(self) -> Dict[str, str]: 32 | """ 33 | Extra files that will be written verbatim under the project folder. The returned 34 | dictionary maps file names to raw code. 35 | """ 36 | return {} 37 | 38 | @property 39 | @abc.abstractmethod 40 | def code_extension(self) -> str: 41 | """The file extension for code files.""" 42 | raise NotImplementedError 43 | 44 | @property 45 | @abc.abstractmethod 46 | def line_comment_symbol(self) -> str: 47 | """The symbol for starting a line comment.""" 48 | raise NotImplementedError 49 | 50 | @property 51 | @abc.abstractmethod 52 | def template_code(self) -> str: 53 | """ 54 | The template code for each problem. Should include section markers for: 55 | 56 | - ``SOLUTION CLASS``: the section to insert the solution class. 57 | - ``SUBMIT``: the section to submit to LeetCode. 58 | - ``USER TEMPLATE``: the section to insert user-defined template code. 59 | - ``TEST``: the section to insert testing code. 60 | 61 | Optionally, the following section markers can be included: 62 | 63 | - ``STATEMENTS``: the section to insert problem statements. 64 | """ 65 | raise NotImplementedError 66 | 67 | @property 68 | def user_template_code(self) -> str: 69 | """ 70 | User-defined templates for convenience. These will be included in the 71 | submission. 72 | """ 73 | return "" 74 | 75 | @classmethod 76 | def write_and_backup(cls, path: str, contents: str) -> None: 77 | """ 78 | Check if there is already a file at the given path, create a backup if there is, 79 | and then write contents to the file. 80 | """ 81 | if os.path.exists(path): 82 | with open(path, "r") as f: 83 | original_contents = f.read() 84 | if original_contents != contents: 85 | # Only create backup if contents differ. 86 | creation_time = os.path.getctime(path) 87 | timestamp = datetime.fromtimestamp(creation_time).strftime( 88 | "%Y%m%d_%H%M%S" 89 | ) 90 | 91 | file_name, file_ext = os.path.splitext(path) 92 | dest_path = f"{file_name}_{timestamp}{file_ext}" 93 | shutil.move(path, dest_path) 94 | log( 95 | f"File '{path}' is modified, backup created at '{dest_path}'", 96 | level="warning", 97 | ) 98 | with open(path, "w") as f: 99 | f.write(contents) 100 | 101 | def replace_section( 102 | self, code: Code, replacements: Dict[str, Code], *, ignore_errors: bool = False 103 | ) -> Code: 104 | """ 105 | Replace a section of template with actual code. Sections are often marked with 106 | line comments. 107 | 108 | :param code: The code as a list of strings, one per line. 109 | :param replacements: A dictionary mapping section names to replacement code. 110 | :param ignore_errors: A :exc:`ValueError` will be thrown for sections that are 111 | not found, unless this argument is ``True``. Defaults to 112 | ``False``. 113 | :return: The updated code. 114 | """ 115 | for section_name, section_code in replacements.items(): 116 | try: 117 | start_line = code.index( 118 | f"{self.line_comment_symbol} BEGIN {section_name}" 119 | ) 120 | end_line = code.index( 121 | f"{self.line_comment_symbol} END {section_name}", start_line + 1 122 | ) 123 | # exclude the line comments 124 | code = code[:start_line] + section_code + code[(end_line + 1) :] 125 | except ValueError: 126 | if not ignore_errors: 127 | raise ValueError( 128 | f"Section {section_name!r} not found in template code for" 129 | f" {self.language} ({self.__class__!r})" 130 | ) 131 | return code 132 | 133 | @classmethod 134 | def list_join(cls, list_xs: Iterable[List[T]], sep: List[T]) -> List[T]: 135 | ret = [] 136 | for idx, xs in enumerate(list_xs): 137 | if idx > 0: 138 | ret.extend(sep) 139 | ret.extend(xs) 140 | return ret 141 | 142 | @abc.abstractmethod 143 | def generate_code( 144 | self, problem: Problem, signature: Signature 145 | ) -> Tuple[Code, Code]: 146 | """ 147 | Generate code given the signature. Code consists of two parts: 148 | 149 | - Code for the solution class. This is basically the template as-is. 150 | - Code for testing the solution. This includes test functions for each example, 151 | and also the main function where the test functions are called and results are 152 | compared. 153 | 154 | :param problem: The crawled raw description of the problem. 155 | :param signature: The parsed signature of the problem. 156 | :return: A tuple of two lists of strings, corresponding to code for the solution 157 | class, and code for testing. 158 | """ 159 | raise NotImplementedError 160 | 161 | def generate_additional_files( 162 | self, project_path: str, problems: List[Problem], signatures: List[Signature] 163 | ) -> None: 164 | """ 165 | Generate additional files that the project requires, besides those in 166 | :attr:`EXTRA_FILES` that are written verbatim. 167 | 168 | :param project_path: Path to the project folder. 169 | :param problems: List of problem descriptions to generate code for. 170 | :param signatures: Parsed signatures of problems. 171 | """ 172 | pass 173 | 174 | def get_problem_file_name(self, idx: int, problem: Problem) -> str: 175 | """ 176 | Generate the code file name for a problem. By default, names are uppercase 177 | letters starting from "A". 178 | 179 | :param idx: Zero-based index of the problem. 180 | :param problem: The description of the problem. 181 | :return: The code file name of the problem. 182 | """ 183 | return f"{chr(ord('A') + idx)}{self.code_extension}" 184 | 185 | def format_statement(self, problem: Problem) -> List[str]: 186 | """ 187 | Convert the problem statement into code (as comments). 188 | 189 | :param problem: The problem description. 190 | :return: Code for the problem statement. 191 | """ 192 | statement = [] 193 | max_length = 80 - (len(self.line_comment_symbol) + 1) 194 | for line in problem.statement.strip().split("\n"): 195 | comments = [ 196 | f"{self.line_comment_symbol} {line[i:(i + max_length)]}" 197 | for i in range(0, len(line), max_length) 198 | ] 199 | statement.extend(comments) 200 | return statement 201 | 202 | def create_project( 203 | self, project_path: str, problems: List[Problem], site: str, debug: bool = False 204 | ) -> None: 205 | """ 206 | Create the folder for the project and generate code and supporting files. 207 | 208 | :param project_path: Path to the project folder. 209 | :param problems: List of problem descriptions to generate code for. 210 | :param site: The LeetCode site where problems are crawled. Different sites may 211 | have slightly different syntax (or language-dependent markings). 212 | :param debug: If ``True``, exceptions will not be caught. This is probably only 213 | useful when the ``--debug`` flag is set, in which case the Python 214 | debugger is hooked to handle exceptions. 215 | """ 216 | if not os.path.exists(project_path): 217 | os.makedirs(project_path) 218 | template = self.template_code.strip().split("\n") 219 | user_template = self.user_template_code.strip().split("\n") 220 | template = self.replace_section(template, {"USER TEMPLATE": user_template}) 221 | 222 | signatures = [] 223 | for idx, problem in enumerate(problems): 224 | try: 225 | problem_signature = parse_problem(problem, site) 226 | signatures.append(problem_signature) 227 | solution_code, test_code = self.generate_code( 228 | problem, problem_signature 229 | ) 230 | problem_code = self.replace_section( 231 | template, 232 | { 233 | "SOLUTION CLASS": solution_code, 234 | "TEST": test_code, 235 | }, 236 | ) 237 | if problem.statement != "": 238 | statement = self.format_statement(problem) 239 | problem_code = self.replace_section( 240 | problem_code, {"STATEMENT": statement}, ignore_errors=True 241 | ) 242 | code_path = os.path.join( 243 | project_path, self.get_problem_file_name(idx, problem) 244 | ) 245 | self.write_and_backup(code_path, "\n".join(problem_code) + "\n") 246 | except Exception: 247 | if debug: 248 | raise 249 | traceback.print_exc() 250 | log( 251 | f"Exception occurred while processing {problem.name!r}", 252 | level="error", 253 | ) 254 | 255 | for tmpl_name, tmpl_code in self.extra_files.items(): 256 | with open(os.path.join(project_path, tmpl_name), "w") as f: 257 | f.write(tmpl_code.strip() + "\n") 258 | 259 | self.generate_additional_files(project_path, problems, signatures) 260 | -------------------------------------------------------------------------------- /lchelper/codegen/cpp.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import defaultdict 3 | from typing import Any, Dict, List, Optional, Tuple, Union 4 | 5 | from lchelper.codegen.base import Code, CodeGen, Signature 6 | from lchelper.common import * 7 | from lchelper.utils import remove_affix 8 | 9 | __all__ = [ 10 | "CppCodeGen", 11 | ] 12 | 13 | 14 | class CppCodeGen(CodeGen): 15 | @property 16 | def language(self) -> str: 17 | return "C++" 18 | 19 | @property 20 | def code_extension(self) -> str: 21 | return ".cpp" 22 | 23 | @property 24 | def line_comment_symbol(self) -> str: 25 | return "//" 26 | 27 | @property 28 | def extra_files(self) -> Dict[str, str]: 29 | return { 30 | # A header-only library for comparing outputs. 31 | "_testing.h": r""" 32 | #ifndef TESTING_H 33 | #define TESTING_H 34 | 35 | #include 36 | #include 37 | 38 | template 39 | void print(const T &x) { std::cout << x; } 40 | 41 | template 42 | void print(const std::vector &vec) { 43 | for (int i = 0; i < vec.size(); ++i) { 44 | std::cout << (i == 0 ? "{" : ", "); 45 | print(vec[i]); 46 | } 47 | std::cout << "}"; 48 | } 49 | 50 | template <> 51 | void print(const bool &x) { std::cout << (x ? "true" : "false"); } 52 | 53 | template 54 | inline bool _test(const T &a, const T &b) { 55 | return a == b; 56 | } 57 | 58 | template 59 | inline bool _test(const std::vector &a, const std::vector &b) { 60 | if (a.size() != b.size()) return false; 61 | for (int i = 0; i < a.size(); ++i) 62 | if (!_test(a[i], b[i])) return false; 63 | return true; 64 | } 65 | 66 | template 67 | inline void test(const char *msg, const T &a, const T &b) { 68 | if (_test(a, b)) { 69 | std::cout << msg << " [OK]" << std::endl; 70 | } else { 71 | std::cout << msg << " [WRONG]" << std::endl; 72 | std::cout << "Expected: "; 73 | print(a); 74 | std::cout << std::endl << "Received: "; 75 | print(b); 76 | std::cout << std::endl; 77 | } 78 | } 79 | 80 | #endif // TESTING_H 81 | """, 82 | # Boilerplate code for supporting LeetCode-specific constructs. 83 | "_boilerplate.hpp": r""" 84 | #include 85 | #include 86 | #include 87 | #include 88 | #include 89 | #include 90 | #include 91 | #include 92 | #include 93 | #include 94 | #include 95 | #include 96 | #include 97 | #include 98 | #include 99 | #include 100 | #include 101 | #include 102 | 103 | #include 104 | #include 105 | #include 106 | #include 107 | #include 108 | #include 109 | #include 110 | 111 | #include "_testing.h" 112 | 113 | using namespace std; 114 | 115 | 116 | struct TreeNode { 117 | int val; 118 | TreeNode *left; 119 | TreeNode *right; 120 | TreeNode(int x) : val(x), left(NULL), right(NULL) {} 121 | ~TreeNode() { 122 | if (left != NULL) delete left; 123 | if (right != NULL) delete right; 124 | } 125 | }; 126 | 127 | const int NONE = INT_MIN; 128 | 129 | TreeNode *_construct_tree(const vector &parent) { 130 | queue q; 131 | int ptr = 0; 132 | 133 | auto _add_node = [&]() -> TreeNode * { 134 | if (ptr >= parent.size()) return nullptr; 135 | int val = parent[ptr++]; 136 | if (val == NONE) return nullptr; 137 | auto *p = new TreeNode(val); 138 | q.push(p); 139 | return p; 140 | }; 141 | 142 | TreeNode *root = _add_node(); 143 | while (!q.empty()) { 144 | if (ptr >= parent.size()) break; 145 | TreeNode *p = q.front(); 146 | q.pop(); 147 | p->left = _add_node(); 148 | p->right = _add_node(); 149 | } 150 | return root; 151 | } 152 | 153 | #ifdef LEETCODE_LOCAL 154 | template 155 | void print(T *a, int n) { 156 | for (int i = 1; i < n; ++i) 157 | std::cout << a[i] << " "; 158 | std::cout << a[n] << std::endl; 159 | } 160 | 161 | #define PRINT(__l, __r, __s, __t) { \ 162 | std::cout << #__l #__s << "~" << #__t #__r << ": "; \ 163 | for (auto __i = __s; __i != __t; ++__i) \ 164 | std::cout << __l __i __r << " "; \ 165 | std::cout << std::endl; \ 166 | } 167 | 168 | template 169 | void debug(Args ...args); 170 | 171 | template <> 172 | void debug() { std::cout << std::endl; } 173 | 174 | template 175 | void debug(const T &x, Args ...args) { 176 | print(x); 177 | std::cout << " "; 178 | debug(args...); 179 | } 180 | #endif // LEETCODE_LOCAL 181 | """, 182 | } 183 | 184 | @property 185 | def template_code(self) -> str: 186 | return r""" 187 | #include "_boilerplate.hpp" 188 | 189 | // BEGIN SUBMIT 190 | 191 | // BEGIN USER TEMPLATE 192 | 193 | // END USER TEMPLATE 194 | 195 | // BEGIN SOLUTION CLASS 196 | 197 | // END SOLUTION CLASS 198 | 199 | // END SUBMIT 200 | 201 | // BEGIN STATEMENT 202 | 203 | // END STATEMENT 204 | 205 | // BEGIN TEST 206 | 207 | // END TEST 208 | """ 209 | 210 | @property 211 | def user_template_code(self) -> str: 212 | return r""" 213 | #ifndef LEETCODE_LOCAL 214 | # define print(...) 215 | # define PRINT(...) 216 | # define debug(...) 217 | #endif // LEETCODE_LOCAL 218 | 219 | typedef long long ll; 220 | typedef unsigned int uint; 221 | 222 | template 223 | struct _greater : less { 224 | inline bool operator() (const T& x, const T& y) const { 225 | return less::operator()(y, x); 226 | } 227 | }; 228 | template 229 | using min_heap = priority_queue, _greater>; 230 | template 231 | using max_heap = priority_queue, less>; 232 | 233 | inline double runtime() { 234 | return (double)clock() / CLOCKS_PER_SEC; 235 | } 236 | 237 | #define tget(a, b) get(a) 238 | """ 239 | 240 | def generate_code( 241 | self, problem: Problem, signature: Signature 242 | ) -> Tuple[Code, Code]: 243 | # Generate solution code as the crawled template. 244 | solution_code = problem.code.copy() 245 | 246 | def to_str(val: Any) -> str: 247 | if isinstance(val, list): 248 | return "{" + ", ".join(to_str(x) for x in val) + "}" 249 | if isinstance(val, str): 250 | if len(val) == 1: 251 | return f"'{val}'" 252 | return f'"{val}"' 253 | if isinstance(val, bool): # bool is a subtype of int 254 | return "true" if val else "false" 255 | if isinstance(val, (int, float)): 256 | return str(val) 257 | assert False 258 | 259 | def to_tree(parent: List[Optional[int]]) -> str: 260 | values = ["NONE" if x is None else str(x) for x in parent] 261 | return f"_construct_tree({{{', '.join(values)}}})" 262 | 263 | def to_val(val: Any, type_name: str) -> str: 264 | if type_name.replace(" ", "") == "TreeNode*": 265 | return to_tree(val) 266 | return to_str(val) 267 | 268 | def to_args(input: Dict[str, Any], func_sig: FunctionSignature) -> List[str]: 269 | # Return list of assignments. 270 | return [ 271 | assign( 272 | f"{func_sig.name}_{arg_name}", to_val(input[arg_name], type_name) 273 | ) 274 | for type_name, arg_name in func_sig.arguments 275 | ] 276 | 277 | def call(func_name: str, args: List[str]) -> str: 278 | return f"{func_name}({', '.join(args)})" 279 | 280 | def ctor(class_name: str, obj_name: str, args: List[str]) -> str: 281 | return f"{class_name} {call(obj_name, args)};" 282 | 283 | def remove_cv_ref(typ: str) -> str: 284 | while True: 285 | if typ.startswith("const"): 286 | typ = typ[len("const") :] 287 | elif typ.startswith("volatile"): 288 | typ = typ[len("volatile") :] 289 | elif typ.endswith("&"): 290 | typ = typ[:-1] 291 | else: 292 | break 293 | typ = typ.strip() 294 | return typ 295 | 296 | def decl(type_name: str, obj_name: Union[str, List[str]]) -> str: 297 | type_name = remove_cv_ref(type_name) 298 | if isinstance(obj_name, list): 299 | return f"{type_name} {', '.join(obj_name)};" 300 | return f"{type_name} {obj_name};" 301 | 302 | def assign(obj_name: str, value: str) -> str: 303 | return f"{obj_name} = {value};" 304 | 305 | def decl_assign(ret_type: str, obj_name: str, value: str) -> str: 306 | ret_type = remove_cv_ref(ret_type) 307 | return f"{ret_type} {obj_name} = {value};" 308 | 309 | # Generate test code as a function per example. 310 | test_functions = [] 311 | instance_name = "_sol" 312 | if isinstance(signature, InteractiveProblemSignature): 313 | func_map: Dict[str, FunctionSignature] = { 314 | func_sig.name: func_sig for func_sig in signature.functions 315 | } 316 | for idx, example in enumerate(signature.examples): 317 | statements = [] 318 | for ex_idx, ex in enumerate(example): 319 | func_sig = func_map[ex.function] 320 | statements.extend(to_args(ex.input, func_sig)) 321 | args = [ 322 | f"{func_sig.name}_{arg_name}" 323 | for _, arg_name in func_sig.arguments 324 | ] 325 | if ex.function == signature.class_name: 326 | ctor_stmt = ctor(signature.class_name, instance_name, args) 327 | statements.append(ctor_stmt) 328 | else: 329 | ret_name = f"_ret{ex_idx}" 330 | if func_sig.return_type != "void": 331 | ret_ans_var = f"_ret_ans{ex_idx}" 332 | stmts = [ 333 | decl_assign( 334 | func_sig.return_type, 335 | ret_ans_var, 336 | to_val(ex.output, func_sig.return_type), 337 | ), 338 | decl_assign( 339 | func_sig.return_type, 340 | ret_name, 341 | f"{instance_name}.{call(ex.function, args)}", 342 | ), 343 | call( 344 | "test", 345 | [ 346 | to_str( 347 | f"{problem.name} - Example {idx} -" 348 | f" Interaction {ex_idx}" 349 | ), 350 | ret_ans_var, 351 | ret_name, 352 | ], 353 | ) 354 | + ";", 355 | ] 356 | statements.extend(stmts) 357 | else: 358 | stmt = f"{instance_name}.{call(ex.function, args)};" 359 | statements.append(stmt) 360 | declarations = defaultdict(list) 361 | for func_sig in signature.functions: 362 | for type_name, arg_name in func_sig.arguments: 363 | declarations[type_name].append(f"{func_sig.name}_{arg_name}") 364 | test_fn = [ 365 | f"void test_example_{idx}() {{", 366 | *[ 367 | " " + decl(type_name, objs) 368 | for type_name, objs in declarations.items() 369 | ], 370 | *[" " + line for line in statements], 371 | "}", 372 | ] 373 | test_functions.append(test_fn) 374 | 375 | main_code = [ 376 | "int main() {", 377 | *[ 378 | " " + f"test_example_{idx}();" 379 | for idx in range(len(signature.examples)) 380 | ], 381 | "}", 382 | ] 383 | else: 384 | func_sig = signature.function 385 | for idx, example in enumerate(signature.examples): 386 | statements = [] 387 | for type_name, arg_name in func_sig.arguments: 388 | stmt = decl_assign( 389 | type_name, arg_name, to_val(example.input[arg_name], type_name) 390 | ) 391 | statements.append(stmt) 392 | args = [arg_name for _, arg_name in func_sig.arguments] 393 | ret_name = "_ret" 394 | ret_ans_var = "_ret_ans" 395 | stmts = [ 396 | decl_assign( 397 | func_sig.return_type, 398 | ret_ans_var, 399 | to_val(example.output, func_sig.return_type), 400 | ), 401 | decl_assign( 402 | func_sig.return_type, 403 | ret_name, 404 | f"{instance_name}.{call(func_sig.name, args)}", 405 | ), 406 | call( 407 | "test", 408 | [ 409 | to_str(f"{problem.name} - Example {idx}"), 410 | ret_ans_var, 411 | ret_name, 412 | ], 413 | ) 414 | + ";", 415 | ] 416 | statements.extend(stmts) 417 | 418 | test_fn = [ 419 | f"void test_example_{idx}(Solution &_sol) {{", 420 | *[" " + line for line in statements], 421 | "}", 422 | ] 423 | test_functions.append(test_fn) 424 | 425 | main_code = [ 426 | "int main() {", 427 | " Solution _sol;", 428 | *[ 429 | f" test_example_{idx}(_sol);" 430 | for idx in range(len(signature.examples)) 431 | ], 432 | "}", 433 | ] 434 | 435 | test_code = self.list_join(test_functions + [main_code], ["", ""]) 436 | return solution_code, test_code 437 | 438 | def generate_additional_files( 439 | self, project_path: str, problems: List[Problem], signatures: List[Signature] 440 | ) -> None: 441 | cmake = [ 442 | "cmake_minimum_required(VERSION 3.12)", 443 | "project(leetcode)", 444 | "set(CMAKE_CXX_STANDARD 17)", 445 | 'set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DLEETCODE_LOCAL")', 446 | ] 447 | for idx, problem in enumerate(problems): 448 | file_name = self.get_problem_file_name(idx, problem) 449 | exec_name = remove_affix(file_name, suffix=self.code_extension) 450 | cmake.append(f"add_executable({exec_name} {file_name})") 451 | with open(os.path.join(project_path, "CMakeLists.txt"), "w") as f: 452 | f.write("\n".join(cmake)) 453 | -------------------------------------------------------------------------------- /lchelper/codegen/python.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Tuple 2 | 3 | from lchelper.codegen.base import Code, CodeGen, Signature 4 | from lchelper.common import * 5 | 6 | __all__ = [ 7 | "PythonCodeGen", 8 | ] 9 | 10 | 11 | class PythonCodeGen(CodeGen): 12 | @property 13 | def language(self) -> str: 14 | return "Python" 15 | 16 | @property 17 | def code_extension(self) -> str: 18 | return ".py" 19 | 20 | @property 21 | def line_comment_symbol(self) -> str: 22 | return "#" 23 | 24 | @property 25 | def template_code(self) -> str: 26 | return r""" 27 | from typing import * 28 | 29 | class TreeNode: 30 | def __init__(self, x): 31 | self.val = x 32 | self.left = None 33 | self.right = None 34 | 35 | # BEGIN SUBMIT 36 | 37 | # BEGIN USER TEMPLATE 38 | 39 | # END USER TEMPLATE 40 | 41 | # BEGIN SOLUTION CLASS 42 | 43 | # END SOLUTION CLASS 44 | 45 | # END SUBMIT 46 | 47 | # BEGIN STATEMENT 48 | 49 | # END STATEMENT 50 | 51 | 52 | def _construct_tree(parent: List[Optional[int]]) -> Optional[TreeNode]: 53 | from queue import Queue 54 | q: 'Queue[TreeNode]' = Queue() 55 | ptr = 0 56 | 57 | def _add_node() -> Optional[TreeNode]: 58 | nonlocal ptr 59 | if ptr >= len(parent): 60 | return None 61 | val = parent[ptr] 62 | ptr += 1 63 | if val is None: 64 | return None 65 | p = TreeNode(val) 66 | q.put(p) 67 | return p 68 | 69 | root = _add_node() 70 | while not q.empty(): 71 | p = q.get() 72 | p.left = _add_node() 73 | p.right = _add_node() 74 | return root 75 | 76 | 77 | def evaluate(msg: str, a, b): 78 | if a == b: 79 | print(f"{msg} [OK]") 80 | else: 81 | print(f"{msg} [WRONG]") 82 | print(f"Expected: {a!r}") 83 | print(f"Received: {b!r}") 84 | 85 | 86 | # BEGIN TEST 87 | 88 | # END TEST 89 | """ 90 | 91 | TYPE_MAP = { 92 | "string": "str", 93 | "double": "float", 94 | "long long": "int", 95 | "unsigned int": "int", 96 | "unsigned long long": "int", 97 | "void": "None", 98 | } 99 | 100 | def _convert_cpp_type(self, type_name: str) -> str: 101 | type_name = type_name.strip().rstrip("*&") 102 | if type_name.startswith("vector<") and type_name.endswith(">"): 103 | inner_type_name = type_name[len("vector<") : -len(">")] 104 | return f"List[{self._convert_cpp_type(inner_type_name)}]" 105 | return self.TYPE_MAP.get(type_name, type_name) 106 | 107 | def generate_solution_code(self, signature: Signature) -> Code: 108 | if isinstance(signature, InteractiveProblemSignature): 109 | class_name = signature.class_name 110 | functions = signature.functions 111 | else: 112 | class_name = "Solution" 113 | functions = [signature.function] 114 | fn_codes = [] 115 | for func_sig in functions: 116 | args = "".join( 117 | f", {arg_name}: {self._convert_cpp_type(arg_type)}" 118 | for arg_type, arg_name in func_sig.arguments 119 | ) 120 | if func_sig.name == class_name: 121 | fn_code = [f" def __init__(self{args}):", f" pass"] 122 | else: 123 | ret_annotation = self._convert_cpp_type(func_sig.return_type) 124 | fn_code = [ 125 | f" def {func_sig.name}(self{args}) -> {ret_annotation}:", 126 | f" pass", 127 | ] 128 | fn_codes.append(fn_code) 129 | code = [f"class {class_name}:"] + self.list_join(fn_codes, [""]) 130 | return code 131 | 132 | def generate_code( 133 | self, problem: Problem, signature: Signature 134 | ) -> Tuple[Code, Code]: 135 | # Convert C++ code to Python code. 136 | solution_code = self.generate_solution_code(signature) 137 | 138 | def to_str(val: Any) -> str: 139 | if isinstance(val, list): 140 | return "[" + ", ".join(to_str(x) for x in val) + "]" 141 | if isinstance(val, str): 142 | return f'"{val}"' 143 | if isinstance(val, bool): # bool is a subtype of int 144 | return "True" if val else "False" 145 | if isinstance(val, (int, float)): 146 | return str(val) 147 | assert False 148 | 149 | def to_tree(parent: List[Optional[int]]) -> str: 150 | values = ["None" if x is None else str(x) for x in parent] 151 | return f"_construct_tree([{', '.join(values)}])" 152 | 153 | def to_val(val: Any, type_name: str) -> str: 154 | if self._convert_cpp_type(type_name) == "TreeNode": 155 | return to_tree(val) 156 | return to_str(val) 157 | 158 | def to_args(input: Dict[str, Any], func_sig: FunctionSignature) -> List[str]: 159 | # Return list of assignments. 160 | return [ 161 | assign( 162 | f"{func_sig.name}_{arg_name}", to_val(input[arg_name], type_name) 163 | ) 164 | for type_name, arg_name in func_sig.arguments 165 | ] 166 | 167 | def call(func_name: str, args: List[str]) -> str: 168 | return f"{func_name}({', '.join(args)})" 169 | 170 | def ctor(class_name: str, obj_name: str, args: List[str]) -> str: 171 | return f"{obj_name} = {call(class_name, args)}" 172 | 173 | def assign(obj_name: str, value: str) -> str: 174 | return f"{obj_name} = {value}" 175 | 176 | # Generate test code as a function per example. 177 | test_functions = [] 178 | instance_name = "_sol" 179 | if isinstance(signature, InteractiveProblemSignature): 180 | func_map: Dict[str, FunctionSignature] = { 181 | func_sig.name: func_sig for func_sig in signature.functions 182 | } 183 | for idx, example in enumerate(signature.examples): 184 | statements = [] 185 | for ex_idx, ex in enumerate(example): 186 | func_sig = func_map[ex.function] 187 | statements.extend(to_args(ex.input, func_sig)) 188 | args = [ 189 | f"{func_sig.name}_{arg_name}" 190 | for _, arg_name in func_sig.arguments 191 | ] 192 | if ex.function == signature.class_name: 193 | ctor_stmt = ctor(signature.class_name, instance_name, args) 194 | statements.append(ctor_stmt) 195 | else: 196 | ret_name = f"_ret{ex_idx}" 197 | if func_sig.return_type != "void": 198 | ret_ans_var = f"_ret_ans{ex_idx}" 199 | stmts = [ 200 | assign( 201 | ret_ans_var, to_val(ex.output, func_sig.return_type) 202 | ), 203 | assign( 204 | ret_name, 205 | f"{instance_name}.{call(ex.function, args)}", 206 | ), 207 | call( 208 | "evaluate", 209 | [ 210 | to_str( 211 | f"{problem.name} - Example {idx} -" 212 | f" Interaction {ex_idx}" 213 | ), 214 | ret_ans_var, 215 | ret_name, 216 | ], 217 | ), 218 | ] 219 | statements.extend(stmts) 220 | else: 221 | stmt = f"{instance_name}.{call(ex.function, args)}" 222 | statements.append(stmt) 223 | test_fn = [ 224 | f"def eval_example_{idx}():", 225 | *[" " + line for line in statements], 226 | ] 227 | test_functions.append(test_fn) 228 | 229 | main_code = [ 230 | "def main():", 231 | *[ 232 | " " + f"eval_example_{idx}()" 233 | for idx in range(len(signature.examples)) 234 | ], 235 | "", 236 | "", 237 | "if __name__ == '__main__':", 238 | " main()", 239 | ] 240 | else: 241 | func_sig = signature.function 242 | for idx, example in enumerate(signature.examples): 243 | statements = [] 244 | for type_name, arg_name in func_sig.arguments: 245 | stmt = assign(arg_name, to_val(example.input[arg_name], type_name)) 246 | statements.append(stmt) 247 | args = [arg_name for _, arg_name in func_sig.arguments] 248 | ret_name = "_ret" 249 | ret_ans_var = "_ret_ans" 250 | stmts = [ 251 | assign(ret_ans_var, to_val(example.output, func_sig.return_type)), 252 | assign(ret_name, f"{instance_name}.{call(func_sig.name, args)}"), 253 | call( 254 | "evaluate", 255 | [ 256 | to_str(f"{problem.name} - Example {idx}"), 257 | ret_ans_var, 258 | ret_name, 259 | ], 260 | ), 261 | ] 262 | statements.extend(stmts) 263 | 264 | test_fn = [ 265 | f"def eval_example_{idx}(_sol: Solution):", 266 | *[" " + line for line in statements], 267 | ] 268 | test_functions.append(test_fn) 269 | 270 | main_code = [ 271 | "def main():", 272 | " _sol = Solution()", 273 | *[ 274 | f" eval_example_{idx}(_sol)" 275 | for idx in range(len(signature.examples)) 276 | ], 277 | "", 278 | "", 279 | "if __name__ == '__main__':", 280 | " main()", 281 | ] 282 | 283 | test_code = self.list_join(test_functions + [main_code], ["", ""]) 284 | return solution_code, test_code 285 | -------------------------------------------------------------------------------- /lchelper/common.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Dict, List, NamedTuple, Optional, Tuple 3 | 4 | __all__ = [ 5 | "User", 6 | "Problem", 7 | "FunctionSignature", 8 | "Example", 9 | "ProblemSignature", 10 | "Interaction", 11 | "InteractiveProblemSignature", 12 | ] 13 | 14 | 15 | @dataclass 16 | class User: 17 | username: str 18 | site: str # "leetcode" or "leetcode-cn" 19 | 20 | def __repr__(self): 21 | if self.site == "leetcode": 22 | return self.username 23 | return f"{self.username} ({self.site})" 24 | 25 | 26 | @dataclass 27 | class Problem: 28 | """Raw description of the problem crawled from the web page.""" 29 | 30 | url: str 31 | name: str 32 | statement: str # problem statement, including examples and constraints 33 | examples: List[str] 34 | # ^ raw examples, consisting of inputs and outputs (and potentially explanations) 35 | code: List[str] # template code, in lines 36 | 37 | 38 | @dataclass 39 | class FunctionSignature: 40 | """Signature of a function.""" 41 | 42 | name: str 43 | arguments: List[Tuple[str, str]] # list of (type, name) 44 | return_type: str 45 | 46 | 47 | @dataclass 48 | class Example: 49 | """An example test case, consisting of an input--output pair.""" 50 | 51 | input: Dict[str, Any] 52 | output: Any 53 | 54 | 55 | @dataclass 56 | class ProblemSignature: 57 | """Signature of a problem, including the function signature and test cases.""" 58 | 59 | function: FunctionSignature 60 | examples: List[Example] 61 | 62 | 63 | @dataclass 64 | class Interaction: 65 | """ 66 | An "interaction" in interactive problems. An example test case for interactive 67 | problems consist of multiple "interactions", where each interaction calls a specific 68 | function, and (potentially) expects an output. 69 | """ 70 | 71 | function: str 72 | input: Dict[str, Any] 73 | output: Optional[Any] 74 | 75 | 76 | @dataclass 77 | class InteractiveProblemSignature: 78 | """Signature of an interactive problem.""" 79 | 80 | class_name: str 81 | functions: List[FunctionSignature] 82 | examples: List[List[Interaction]] 83 | -------------------------------------------------------------------------------- /lchelper/crawler.py: -------------------------------------------------------------------------------- 1 | import http.cookiejar 2 | import os 3 | from typing import List 4 | 5 | from selenium import webdriver 6 | from selenium.common.exceptions import NoSuchElementException, TimeoutException 7 | from selenium.webdriver.common.by import By 8 | from selenium.webdriver.support import expected_conditions as Expected 9 | from selenium.webdriver.support.wait import WebDriverWait 10 | 11 | from lchelper.common import Problem, User 12 | from lchelper.logging import log 13 | 14 | __all__ = [ 15 | "get_users", 16 | "get_cookie_path", 17 | "update_cookie", 18 | "get_problems", 19 | ] 20 | 21 | COOKIE_FOLDER = "cookies/" 22 | 23 | 24 | def get_users() -> List[User]: 25 | """Return a list of users that we have cookies of.""" 26 | if not os.path.exists(COOKIE_FOLDER): 27 | return [] 28 | users = [] 29 | for file in os.listdir(COOKIE_FOLDER): 30 | if file.endswith(".dat"): 31 | file = file[: -len(".dat")] 32 | *username, site = file.split("@") 33 | users.append(User("@".join(username), site)) 34 | return users 35 | 36 | 37 | def get_cookie_path(username: str, site: str) -> str: 38 | if not os.path.exists(COOKIE_FOLDER): 39 | os.makedirs(COOKIE_FOLDER) 40 | return os.path.join(COOKIE_FOLDER, f"{username}@{site}.dat") 41 | 42 | 43 | def check_login(browser, site: str, timeout: int = 10) -> bool: 44 | try: 45 | if site == "leetcode": 46 | WebDriverWait(browser, timeout).until( 47 | Expected.presence_of_element_located( 48 | ( 49 | By.CSS_SELECTOR, 50 | "#navbar_user_avatar", 51 | ) 52 | ) 53 | ) 54 | else: # site == "leetcode-cn" 55 | WebDriverWait(browser, timeout).until( 56 | Expected.presence_of_element_located( 57 | ( 58 | By.CSS_SELECTOR, 59 | 'nav div[data-cypress="NavbarMenuIconItem"] > span', 60 | ) 61 | ) 62 | ) 63 | return True 64 | except TimeoutException: 65 | return False 66 | 67 | 68 | def update_cookie(username: str, site: str) -> None: 69 | """Update the cookie for the LeetCode user.""" 70 | print( 71 | "A browser window will open shortly. Do not interact with the window until" 72 | " further instructions." 73 | ) 74 | browser = webdriver.Chrome() 75 | browser.set_window_position(0, 0) 76 | browser.set_window_size(800, 600) 77 | browser.switch_to.window(browser.window_handles[0]) 78 | url = f"https://{site}.com/accounts/login/" 79 | browser.get(url) 80 | browser.implicitly_wait(10) 81 | 82 | if site == "leetcode": 83 | WebDriverWait(browser, 10).until( 84 | Expected.visibility_of_element_located( 85 | (By.CSS_SELECTOR, 'button[data-cy="sign-in-btn"]') 86 | ) 87 | ) 88 | else: # site == "leetcode-cn" 89 | WebDriverWait(browser, 10).until( 90 | Expected.visibility_of_element_located( 91 | (By.CSS_SELECTOR, 'button[type="submit"]') 92 | ) 93 | ) 94 | print("Login page loaded. Please enter your password in the browser window.") 95 | 96 | elem = browser.find_element(By.CSS_SELECTOR, 'input[name="login"]') 97 | elem.clear() 98 | elem.send_keys(username) 99 | 100 | if not check_login(browser, site, timeout=120): 101 | raise RuntimeError("Login failed!") 102 | 103 | cookies = browser.get_cookies() 104 | jar = http.cookiejar.LWPCookieJar() 105 | for cookie in cookies: 106 | jar.set_cookie( 107 | http.cookiejar.Cookie( 108 | version=0, 109 | name=cookie["name"], 110 | value=cookie["value"], 111 | port="80", 112 | port_specified=False, 113 | domain=cookie["domain"], 114 | domain_specified=True, 115 | domain_initial_dot=False, 116 | path=cookie["path"], 117 | path_specified=True, 118 | secure=cookie["secure"], 119 | expires=cookie.get("expiry", 0), 120 | discard=False, 121 | comment=None, 122 | comment_url=None, 123 | rest={}, 124 | ) 125 | ) 126 | browser.quit() 127 | 128 | cookie_path = get_cookie_path(username, site) 129 | jar.save(cookie_path, ignore_discard=True, ignore_expires=True) 130 | 131 | 132 | def get_problems(contest_url: str, site: str, cookie_path: str) -> List[Problem]: 133 | """ 134 | Obtain the list of problems in a contest, given its URL. 135 | 136 | :param contest_url: URL to the contest page. 137 | :param site: LeetCode site name. 138 | :param cookie_path: Path to the cookie to use for signing in. 139 | :return: A list of problem descriptions. 140 | """ 141 | if not os.path.exists(cookie_path): 142 | raise ValueError( 143 | f"No cookies file found at path '{cookie_path}'. Please login first" 144 | ) 145 | 146 | options = webdriver.ChromeOptions() 147 | options.add_argument("headless") 148 | browser = webdriver.Chrome(options=options) 149 | browser.set_window_position(0, 0) 150 | browser.set_window_size( 151 | 3840, 600 152 | ) # a wide enough window so code does not get wrapped 153 | browser.implicitly_wait(10) 154 | 155 | log("Loading LeetCode contest page...") 156 | browser.get( 157 | contest_url 158 | ) # visit the page first to update the domain, and then set cookies 159 | cookie_jar = http.cookiejar.LWPCookieJar(cookie_path) 160 | cookie_jar.load(ignore_discard=True, ignore_expires=True) 161 | for c in cookie_jar: 162 | browser.add_cookie({"name": c.name, "value": c.value, "path": c.path}) 163 | browser.get(contest_url) # visit again to refresh page with cookies added 164 | 165 | if not check_login(browser, site, timeout=10): 166 | browser.quit() 167 | print(f"Cookie '{cookie_path}' might have expired. Please try logging in again") 168 | exit(1) 169 | 170 | elem = browser.find_element(By.CSS_SELECTOR, "ul.contest-question-list") 171 | links = elem.find_elements(By.TAG_NAME, "a") 172 | problem_paths = [(link.get_attribute("href"), link.text) for link in links] 173 | log(f"Found problems: {[name for _, name in problem_paths]!r}") 174 | 175 | parsed_problems = [] 176 | for idx, (problem_url, problem_name) in enumerate(problem_paths): 177 | browser.get(problem_url) 178 | try: 179 | # Page during contest; editor located below statement. 180 | statement_css_selector = "div.question-content" 181 | code_css_selector = "pre.CodeMirror-line" 182 | statement = browser.find_element( 183 | By.CSS_SELECTOR, statement_css_selector 184 | ).text 185 | except (TimeoutException, NoSuchElementException): 186 | # Page after contest; statement and editor in vertically split panes. 187 | statement_css_selector = ( 188 | "div[data-key='description-content'] div.content__1Y2H" 189 | ) 190 | code_css_selector = "div.monaco-scrollable-element div.view-line" 191 | statement = browser.find_element( 192 | By.CSS_SELECTOR, statement_css_selector 193 | ).text 194 | examples = [ 195 | elem.text 196 | for elem in browser.find_elements(By.CSS_SELECTOR, "pre:not([class])") 197 | if elem.text 198 | ] 199 | # TODO: Should make sure C++ is selected! 200 | code = [ 201 | elem.text 202 | for elem in browser.find_elements(By.CSS_SELECTOR, code_css_selector) 203 | ] 204 | problem = Problem(problem_url, problem_name, statement, examples, code) 205 | parsed_problems.append(problem) 206 | log(f"Parsed problem ({idx + 1}/{len(problem_paths)}): {problem_name}") 207 | 208 | browser.quit() 209 | log("All problems successfully crawled", level="success") 210 | 211 | return parsed_problems 212 | -------------------------------------------------------------------------------- /lchelper/logging.py: -------------------------------------------------------------------------------- 1 | from termcolor import colored 2 | 3 | __all__ = [ 4 | "log", 5 | ] 6 | 7 | COLOR_MAP = { 8 | "success": "green", 9 | "warning": "yellow", 10 | "error": "red", 11 | "info": "white", 12 | } 13 | 14 | 15 | def log(msg: str, *, level: str = "info") -> None: 16 | """ 17 | Write a line of log with the specified logging level. 18 | 19 | :param msg: Message to log. 20 | :param level: Logging level. Available options are ``success``, ``warning``, 21 | ``error``, and ``info``. 22 | """ 23 | if level not in COLOR_MAP: 24 | raise ValueError(f"Incorrect logging level '{level}'") 25 | # time_str = time.strftime("[%Y-%m-%d %H:%M:%S]") 26 | # print(colored(time_str, COLOR_MAP[level]), msg, flush=True) 27 | print(colored(msg, COLOR_MAP[level]), flush=True) 28 | -------------------------------------------------------------------------------- /lchelper/parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Dict, List, Tuple, Union 3 | 4 | from lchelper.common import * 5 | from lchelper.logging import log 6 | 7 | __all__ = [ 8 | "parse_problem", 9 | ] 10 | 11 | 12 | def parse_vardef(s: str) -> Tuple[str, str]: 13 | """ 14 | Given a variable definition, return the type and identifier name. For instance: 15 | ``TreeNode *node`` should return ``TreeNode *`` and ``node``. 16 | 17 | :param s: The string to parse. 18 | :return: A tuple of (type, name). 19 | """ 20 | s = s.strip() 21 | ident_start = next((idx for idx in range(len(s)) if s[idx:].isidentifier()), 0) 22 | # Identifier is the longest suffix that is a valid identifier. 23 | # If the entire definition is an identifier, it's a constructor and we count it as 24 | # the type name. 25 | if ident_start == 0: 26 | type_name = s 27 | identifier = s 28 | else: 29 | type_name = s[:ident_start].strip() 30 | identifier = s[ident_start:].strip() 31 | return type_name, identifier 32 | 33 | 34 | def find_functions(code: List[str]) -> Tuple[str, List[FunctionSignature]]: 35 | """ 36 | Find functions in the solution class, and parse their signatures. 37 | 38 | :param code: Lines of the template code. 39 | :return: A tuple of two elements: 40 | - The class name (in most cases it's "Solution" but in interactive problems it 41 | might not). 42 | - A list of function signatures, indicating the functions in the solution class. 43 | """ 44 | start_line = next( 45 | idx 46 | for idx in range(len(code)) 47 | if code[idx].startswith("class ") and code[idx].endswith(" {") 48 | ) 49 | class_name = code[start_line][len("class ") : -len(" {")].strip() 50 | end_line = code.index("};") 51 | signatures = [] 52 | for line in code[(start_line + 1) : end_line]: 53 | # A very heuristic way to find function beginnings. 54 | if line.startswith(" ") and line.endswith("{"): 55 | # Find function name. 56 | bracket_pos = line.find("(") 57 | return_type, func_name = parse_vardef(line[:bracket_pos]) 58 | args_str = line[(bracket_pos + 1) : line.find(")")].split(",") 59 | arguments = [parse_vardef(s) for s in args_str if s] 60 | signatures.append(FunctionSignature(func_name, arguments, return_type)) 61 | return class_name, signatures 62 | 63 | 64 | def parse_value(s: str) -> Tuple[Any, str]: 65 | """ 66 | Parse a JSON value from the string, and return the remaining part of the string. 67 | 68 | :return: A tuple of (parsed JSON object, remaining unparsed string). 69 | """ 70 | try: 71 | obj = json.loads(s) 72 | ret_str = "" 73 | except json.JSONDecodeError as e: 74 | obj = json.loads(s[: e.pos]) 75 | ret_str = s[e.pos :] 76 | return obj, ret_str.strip() 77 | 78 | 79 | def parse_problem( 80 | problem: Problem, site: str = "leetcode" 81 | ) -> Union[ProblemSignature, InteractiveProblemSignature]: 82 | r"""Parse the problem given the raw contents crawled from the web.""" 83 | 84 | def find_example_section( 85 | s: str, 86 | cur_tag: str, 87 | next_tag: str, 88 | colon: str = ":", 89 | ignore_error: bool = False, 90 | ) -> str: 91 | """ 92 | Find the part in the example that is between two tags. If ``next_tag`` does not 93 | exist, then find the part until the end. 94 | """ 95 | start_pos = s.find(cur_tag) 96 | if start_pos == -1: 97 | if not ignore_error: 98 | raise ValueError 99 | start_pos = 0 100 | else: 101 | start_pos += len(cur_tag) 102 | if s[start_pos] == colon: 103 | start_pos += 1 104 | end_pos = s.find(next_tag, start_pos) 105 | if end_pos == -1: 106 | if not ignore_error: 107 | raise ValueError 108 | end_pos = len(s) 109 | return s[start_pos:end_pos].strip() 110 | 111 | # Parse function signature from code. 112 | class_name, func_signatures = find_functions(problem.code) 113 | assert len(func_signatures) > 0 114 | if len(func_signatures) > 1: 115 | # Probably an interactive problem, skip for now. 116 | func_map: Dict[str, FunctionSignature] = { 117 | signature.name: signature for signature in func_signatures 118 | } 119 | examples: List[List[Interaction]] = [] 120 | for example in problem.examples: 121 | try: 122 | input_str = find_example_section(example, "输入", "输出", ":") 123 | output_str = find_example_section(example, "输出", "解释", ":") 124 | except ValueError: 125 | input_str = find_example_section( 126 | example, "Input", "Output", ignore_error=True 127 | ) 128 | output_str = find_example_section( 129 | example, "Output", "Explanation", ignore_error=True 130 | ) 131 | 132 | functions, input_str = parse_value(input_str) 133 | arg_vals, input_str = parse_value(input_str) 134 | if len(input_str) > 0: 135 | log( 136 | f"Problem {problem.name!r}: Extra characters in example input" 137 | f" section: {input_str}", 138 | level="warning", 139 | ) 140 | ret_vals, output_str = parse_value(output_str) 141 | if len(output_str) > 0: 142 | log( 143 | f"Problem {problem.name!r}: Extra characters in example output" 144 | f" section: {output_str}", 145 | level="warning", 146 | ) 147 | 148 | cur_examples = [ 149 | Interaction( 150 | function=func, 151 | input={ 152 | arg_name: val 153 | for (_, arg_name), val in zip(func_map[func].arguments, args) 154 | }, 155 | output=ret, 156 | ) 157 | for func, args, ret in zip(functions, arg_vals, ret_vals) 158 | ] 159 | examples.append(cur_examples) 160 | 161 | return InteractiveProblemSignature(class_name, func_signatures, examples) 162 | 163 | else: 164 | assert class_name == "Solution" 165 | 166 | func_signature = func_signatures[0] 167 | examples: List[Example] = [] 168 | for ex_id, example in enumerate(problem.examples): 169 | try: 170 | input_str = find_example_section(example, "输入", "输出", ":") 171 | output_str = find_example_section( 172 | example, "输出", "解释", ":", ignore_error=True 173 | ) 174 | except ValueError: 175 | input_str = find_example_section( 176 | example, "Input", "Output", ignore_error=True 177 | ) 178 | output_str = find_example_section( 179 | example, "Output", "Explanation", ignore_error=True 180 | ) 181 | 182 | input_vals = {} 183 | for idx, (_, name) in enumerate(func_signature.arguments): 184 | if idx > 0 and input_str.startswith(","): 185 | input_str = input_str[1:].strip() 186 | if input_str[0].isidentifier(): 187 | ident_end = ( 188 | next( 189 | ( 190 | idx 191 | for idx in range(1, len(input_str)) 192 | if not input_str[:idx].isidentifier() 193 | ), 194 | len(input_str) + 1, 195 | ) 196 | - 1 197 | ) 198 | ident = input_str[:ident_end] 199 | if ident != name: 200 | log( 201 | f"Problem {problem.name!r}: Argument {idx + 1} should be" 202 | f" {name!r}, but {ident!r} found in example {ex_id + 1}", 203 | level="warning", 204 | ) 205 | assert input_str.startswith(f"{ident} =") 206 | input_str = input_str[len(f"{ident} =") :].strip() 207 | elif idx != 0: 208 | log( 209 | f"Problem {problem.name!r}: Argument {idx + 1} is unnamed in" 210 | f" example {ex_id + 1}", 211 | level="warning", 212 | ) 213 | input_val, input_str = parse_value(input_str) 214 | input_vals[name] = input_val 215 | if len(input_str) > 0: 216 | log( 217 | f"Problem {problem.name!r}: Extra characters in example input" 218 | f" section:\n{input_str}", 219 | level="warning", 220 | ) 221 | 222 | output_val, output_str = parse_value(output_str) 223 | if len(output_str) > 0: 224 | log( 225 | f"Problem {problem.name!r}: Extra characters in example output" 226 | f" section:\n{output_str}", 227 | level="warning", 228 | ) 229 | 230 | examples.append(Example(input_vals, output_val)) 231 | 232 | return ProblemSignature(func_signature, examples) 233 | -------------------------------------------------------------------------------- /lchelper/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Optional 3 | 4 | __all__ = [ 5 | "remove_affix", 6 | "register_excepthook", 7 | ] 8 | 9 | 10 | def remove_affix( 11 | s: str, prefix: Optional[str] = None, suffix: Optional[str] = None 12 | ) -> str: 13 | if prefix is not None and s.startswith(prefix): 14 | s = s[len(prefix) :] 15 | if suffix is not None and s.endswith(suffix): 16 | s = s[: -len(suffix)] 17 | return s 18 | 19 | 20 | def register_excepthook(): 21 | def excepthook(type, value, traceback): 22 | if type is KeyboardInterrupt: 23 | # don't capture keyboard interrupts (Ctrl+C) 24 | sys.__excepthook__(type, value, traceback) 25 | else: 26 | ipython_hook(type, value, traceback) 27 | 28 | # enter IPython debugger on exception 29 | from IPython.core import ultratb 30 | 31 | ipython_hook = ultratb.FormattedTB( 32 | mode="Context", color_scheme="Linux", call_pdb=True 33 | ) 34 | sys.excepthook = excepthook 35 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import dataclasses 3 | import os 4 | import pickle 5 | import sys 6 | from typing import Any, Dict, List, NoReturn, Optional 7 | from urllib.parse import urlparse 8 | 9 | import lchelper 10 | import lchelper.utils 11 | 12 | PROGRAM = "python main.py" 13 | CACHE_FILE = "contest_problems.pkl" 14 | 15 | 16 | def parse_args(): 17 | class CustomParser(argparse.ArgumentParser): 18 | def error(self, message: str) -> NoReturn: 19 | self.print_help(sys.stderr) 20 | sys.stderr.write("\nerror: " + message + "\n") 21 | sys.exit(2) 22 | 23 | parser = CustomParser() 24 | parser.add_argument("--debug", action="store_true", default=False) 25 | subparsers = parser.add_subparsers(dest="command") 26 | 27 | parser_login = subparsers.add_parser( 28 | "login", help="Log in using your LeetCode account" 29 | ) 30 | parser_login.add_argument("username", help="Your LeetCode account username") 31 | parser_login.add_argument( 32 | "-s", 33 | "--site", 34 | dest="site", 35 | choices=["leetcode", "leetcode-cn"], 36 | default="leetcode", 37 | help="The LeetCode site for the account", 38 | ) 39 | 40 | parser_get = subparsers.add_parser( 41 | "get", help="Download contest problems and generate testing code" 42 | ) 43 | parser_get.add_argument( 44 | "-u", 45 | "--username", 46 | dest="username", 47 | default=None, 48 | help=( 49 | "The LeetCode account to use, required if you logged in with multiple" 50 | " accounts" 51 | ), 52 | ) 53 | parser_get.add_argument( 54 | "-l", 55 | "--lang", 56 | metavar="LANG", 57 | dest="lang", 58 | action="append", 59 | required=True, 60 | choices=list(lchelper.LANGUAGES.keys()), 61 | help=( 62 | "Languages to generate testing code for, supported languages are:" 63 | " [%(choices)s]" 64 | ), 65 | ) 66 | parser_get.add_argument( 67 | "--no-cache", 68 | action="store_true", 69 | default=False, 70 | help="Do not use cached problem descriptions when generating code", 71 | ) 72 | parser_get.add_argument( 73 | "-o", 74 | "--output", 75 | dest="output", 76 | default="./", 77 | help="The path to store generated projects", 78 | ) 79 | parser_get.add_argument( 80 | "-p", 81 | "--prefix", 82 | dest="prefix", 83 | default=None, 84 | help="Prefix for project folders, if not specified, the contest name (e.g. " 85 | '"weekly-contest-162") if used', 86 | ) 87 | parser_get.add_argument( 88 | "url", 89 | help='URL to the contest page, or the contest name (e.g. "weekly-contest-162")', 90 | ) 91 | 92 | args = parser.parse_args() 93 | if not args.command: 94 | parser.print_help(sys.stderr) 95 | return args 96 | 97 | 98 | def main(): 99 | args = parse_args() 100 | if args.debug: 101 | lchelper.utils.register_excepthook() 102 | 103 | if args.command == "login": 104 | print(f"Logging in using account '{args.username}'...") 105 | lchelper.update_cookie(args.username, args.site) 106 | print(f"Cookies for user '{args.username}' saved.") 107 | 108 | elif args.command == "get": 109 | if os.path.exists(CACHE_FILE): 110 | with open(CACHE_FILE, "rb") as f: 111 | info = pickle.load(f) 112 | else: 113 | info = {} 114 | 115 | url_parse = urlparse(args.url) 116 | if url_parse.netloc != "": # URL instead of name 117 | contest_name = args.url.rstrip("/").split("/")[ 118 | -1 119 | ] # use the final URL segment as contest nme 120 | site: Optional[str] = lchelper.utils.remove_affix( 121 | url_parse.netloc, "www.", ".com" 122 | ) 123 | else: 124 | contest_name = args.url 125 | site = None 126 | 127 | cached_problems: Optional[List[Dict[str, Any]]] = None 128 | if not args.no_cache: 129 | if (site, contest_name) in info: 130 | cached_problems = info[site, contest_name] 131 | 132 | if cached_problems is None: 133 | available_users = lchelper.get_users() 134 | if len(available_users) == 0: 135 | print( 136 | f"You're not logged in. Please run `{PROGRAM} login `" 137 | f" first." 138 | ) 139 | exit(1) 140 | 141 | candidates = user_candidates = available_users 142 | if args.username is not None: 143 | candidates = user_candidates = [ 144 | user for user in candidates if user.username == args.username 145 | ] 146 | if site is not None: 147 | candidates = [user for user in candidates if user.site == site] 148 | # If there exist multiple candidates with different usernames, raise an 149 | # error to avoid ambiguity. 150 | if len(set(user.username for user in candidates)) > 1: 151 | print( 152 | f"You have logged in with multiple accounts:" 153 | f" {', '.join(repr(s) for s in candidates)}.\n" 154 | f"Please select the user using the `-u ` flag." 155 | ) 156 | exit(1) 157 | if len(candidates) == 0: 158 | if args.username is not None: 159 | if len(user_candidates) > 0: 160 | print( 161 | f"The specified user {args.username!r} is not from the site" 162 | f" {site!r}.\n" 163 | f"Please log in with a user from {site!r} by running " 164 | f"`{PROGRAM} login -s {site} `." 165 | ) 166 | else: 167 | print( 168 | f"The specified user {args.username!r} is not logged in.\n" 169 | f"Please log in by running" 170 | f" `{PROGRAM} login {args.username}` first." 171 | ) 172 | else: 173 | print( 174 | f"There are no users from the site {site!r}.\n" 175 | f"Please log in with a user from {site!r} by running" 176 | f" `{PROGRAM} login -s {site} `." 177 | ) 178 | exit(1) 179 | 180 | user = candidates[0] 181 | cookie_path = lchelper.get_cookie_path(user.username, user.site) 182 | url = f"https://{user.site}.com/contest/{contest_name}" 183 | lchelper.log(f"User: {user}, URL: {url}") 184 | 185 | problems = lchelper.get_problems(url, user.site, cookie_path) 186 | 187 | info[site, contest_name] = [dataclasses.asdict(p) for p in problems] 188 | with open(CACHE_FILE, "wb") as f: 189 | pickle.dump(info, f) 190 | else: 191 | problems = [lchelper.Problem(**p) for p in cached_problems] 192 | 193 | for lang in args.lang: 194 | codegen = lchelper.create_codegen(lang) 195 | project_path = os.path.join( 196 | args.output, f"{(args.prefix or contest_name)}_{lang}" 197 | ) 198 | codegen.create_project(project_path, problems, site, debug=args.debug) 199 | lchelper.log( 200 | f"Project in language {lang!r} stored at: {project_path}", 201 | level="success", 202 | ) 203 | 204 | 205 | if __name__ == "__main__": 206 | main() 207 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium 2 | termcolor 3 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Dict, List, Optional, Union 3 | 4 | import lchelper.codegen 5 | from lchelper.common import ( 6 | Example, 7 | FunctionSignature, 8 | Interaction, 9 | InteractiveProblemSignature, 10 | Problem, 11 | ProblemSignature, 12 | ) 13 | 14 | 15 | class EndToEndTest(unittest.TestCase): 16 | def _test_problem_set( 17 | self, 18 | url: str, 19 | site: str = "leetcode", 20 | ignore_problems: Optional[List[int]] = None, 21 | ): 22 | available_users = [user for user in lchelper.get_users() if user.site == site] 23 | assert ( 24 | len(available_users) > 0 25 | ), f'User cookie from site "{site}" required for end-to-end tests' 26 | user = available_users[0] 27 | problems = lchelper.get_problems( 28 | url, site, lchelper.get_cookie_path(user.username, user.site) 29 | ) 30 | codegen: Dict[str, lchelper.codegen.CodeGen] = { 31 | lang: codegen_klass() for lang, codegen_klass in lchelper.LANGUAGES.items() 32 | } 33 | 34 | ignore_problems = ignore_problems or [] 35 | for idx, problem in enumerate(problems): 36 | if idx in ignore_problems: 37 | continue 38 | problem_signature = lchelper.parse_problem(problem, site) 39 | for lang, gen in codegen.items(): 40 | _, _ = gen.generate_code(problem, problem_signature) 41 | 42 | def test_contests(self): 43 | contests = [ 44 | ("weekly-contest-183", []), 45 | ("weekly-contest-182", []), 46 | ("weekly-contest-181", []), 47 | ("weekly-contest-180", []), 48 | ("weekly-contest-163", []), 49 | ("biweekly-contest-14", []), 50 | ] 51 | for contest, ignore_problems in contests: 52 | url = f"https://leetcode.com/contest/{contest}" 53 | self._test_problem_set(url, ignore_problems=ignore_problems) 54 | 55 | 56 | class ParseTest(unittest.TestCase): 57 | def _function_equal( 58 | self, parsed_function: FunctionSignature, function: FunctionSignature 59 | ): 60 | assert parsed_function.return_type == function.return_type 61 | assert parsed_function.name == function.name 62 | assert parsed_function.arguments == function.arguments 63 | 64 | def _test_parse_problem( 65 | self, 66 | problem: Problem, 67 | signature: Union[ProblemSignature, InteractiveProblemSignature], 68 | ): 69 | parsed_signature = lchelper.parse_problem(problem) 70 | assert type(parsed_signature) is type(signature) 71 | if isinstance(signature, InteractiveProblemSignature): 72 | assert parsed_signature.class_name == signature.class_name 73 | for parsed_function, function in zip( 74 | parsed_signature.functions, signature.functions 75 | ): 76 | self._function_equal(parsed_function, function) 77 | else: 78 | self._function_equal(parsed_signature.function, signature.function) 79 | 80 | assert len(parsed_signature.examples) == len(signature.examples) 81 | for idx in range(len(parsed_signature.examples)): 82 | if isinstance(signature, InteractiveProblemSignature): 83 | for parsed_example, example in zip( 84 | parsed_signature.examples[idx], signature.examples[idx] 85 | ): 86 | assert parsed_example.function == example.function 87 | assert parsed_example.input == example.input 88 | assert parsed_example.output == example.output 89 | else: 90 | assert ( 91 | parsed_signature.examples[idx].input 92 | == signature.examples[idx].input 93 | ) 94 | assert ( 95 | parsed_signature.examples[idx].output 96 | == signature.examples[idx].output 97 | ) 98 | 99 | # from lchelper.codegen.cpp import generate_code 100 | # solution_code, test_code = generate_code(problem, signature) 101 | # print(solution_code) 102 | # print(test_code) 103 | 104 | def test_parse_problem_1(self): 105 | problem = Problem( 106 | url="", 107 | name="Shift 2D Grid", 108 | statement="", 109 | examples=[ 110 | ( 111 | "Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 1\n" 112 | "Output: [[9,1,2],[3,4,5],[6,7,8]]" 113 | ), 114 | ( 115 | "Input: grid = [[3,8,1,9],[19,7,2,5],[4,6,11,10],[12,0,21,13]], k = 4\n" 116 | "Output: [[12,0,21,13],[3,8,1,9],[19,7,2,5],[4,6,11,10]]" 117 | ), 118 | ( 119 | "Input: grid = [[1,2,3],[4,5,6],[7,8,9]], k = 9\n" 120 | "Output: [[1,2,3],[4,5,6],[7,8,9]]" 121 | ), 122 | ], 123 | code=[ 124 | "class Solution {", 125 | "public:", 126 | " vector> shiftGrid(vector>& grid, int k) {", 127 | " ", 128 | " }", 129 | "};", 130 | ], 131 | ) 132 | signature = ProblemSignature( 133 | function=FunctionSignature( 134 | return_type="vector>", 135 | name="shiftGrid", 136 | arguments=[("vector>&", "grid"), ("int", "k")], 137 | ), 138 | examples=[ 139 | Example( 140 | {"grid": [[1, 2, 3], [4, 5, 6], [7, 8, 9]], "k": 1}, 141 | [[9, 1, 2], [3, 4, 5], [6, 7, 8]], 142 | ), 143 | Example( 144 | { 145 | "grid": [ 146 | [3, 8, 1, 9], 147 | [19, 7, 2, 5], 148 | [4, 6, 11, 10], 149 | [12, 0, 21, 13], 150 | ], 151 | "k": 4, 152 | }, 153 | [[12, 0, 21, 13], [3, 8, 1, 9], [19, 7, 2, 5], [4, 6, 11, 10]], 154 | ), 155 | Example( 156 | {"grid": [[1, 2, 3], [4, 5, 6], [7, 8, 9]], "k": 9}, 157 | [[1, 2, 3], [4, 5, 6], [7, 8, 9]], 158 | ), 159 | ], 160 | ) 161 | self._test_parse_problem(problem, signature) 162 | 163 | def test_parse_problem_2(self): 164 | problem = Problem( 165 | url="", 166 | name="Find Elements in a Contaminated Binary Tree", 167 | statement="", 168 | examples=[ 169 | ( 170 | "Input\n" 171 | '["FindElements","find","find"]\n' 172 | "[[[-1,null,-1]],[1],[2]]\n" 173 | "Output\n" 174 | "[null,false,true]\n" 175 | "Explanation\n" 176 | "FindElements findElements = new FindElements([-1,null,-1]); \n" 177 | "findElements.find(1); // return False \n" 178 | "findElements.find(2); // return True " 179 | ), 180 | ( 181 | "Input\n" 182 | '["FindElements","find","find","find"]\n' 183 | "[[[-1,-1,-1,-1,-1]],[1],[3],[5]]\n" 184 | "Output\n" 185 | "[null,true,true,false]\n" 186 | "Explanation\n" 187 | "FindElements findElements = new FindElements([-1,-1,-1,-1,-1]);\n" 188 | "findElements.find(1); // return True\n" 189 | "findElements.find(3); // return True\n" 190 | "findElements.find(5); // return False" 191 | ), 192 | ( 193 | "Input\n" 194 | '["FindElements","find","find","find","find"]\n' 195 | "[[[-1,null,-1,-1,null,-1]],[2],[3],[4],[5]]\n" 196 | "Output\n" 197 | "[null,true,false,false,true]\n" 198 | "Explanation\n" 199 | "FindElements findElements = new FindElements([-1,null,-1,-1,null,-1]);\n" 200 | "findElements.find(2); // return True\n" 201 | "findElements.find(3); // return False\n" 202 | "findElements.find(4); // return False\n" 203 | "findElements.find(5); // return True" 204 | ), 205 | ], 206 | code=[ 207 | "/**", 208 | " * Definition for a binary tree node.", 209 | " * struct TreeNode {", 210 | " * int val;", 211 | " * TreeNode *left;", 212 | " * TreeNode *right;", 213 | " * TreeNode(int x) : val(x), left(NULL), right(NULL) {}", 214 | " * };", 215 | " */", 216 | "class FindElements {", 217 | "public:", 218 | " FindElements(TreeNode* root) {", 219 | " ", 220 | " }", 221 | " ", 222 | " bool find(int target) {", 223 | " ", 224 | " }", 225 | "};", 226 | "", 227 | "/**", 228 | " * Your FindElements object will be instantiated and called as such:", 229 | " * FindElements* obj = new FindElements(root);", 230 | " * bool param_1 = obj->find(target);", 231 | " */", 232 | ], 233 | ) 234 | signature = InteractiveProblemSignature( 235 | class_name="FindElements", 236 | functions=[ 237 | FunctionSignature( 238 | return_type="FindElements", 239 | name="FindElements", 240 | arguments=[("TreeNode*", "root")], 241 | ), 242 | FunctionSignature( 243 | return_type="bool", name="find", arguments=[("int", "target")] 244 | ), 245 | ], 246 | examples=[ 247 | [ 248 | Interaction( 249 | function="FindElements", 250 | input={"root": [-1, None, -1]}, 251 | output=None, 252 | ), 253 | Interaction(function="find", input={"target": 1}, output=False), 254 | Interaction(function="find", input={"target": 2}, output=True), 255 | ], 256 | [ 257 | Interaction( 258 | function="FindElements", 259 | input={"root": [-1, -1, -1, -1, -1]}, 260 | output=None, 261 | ), 262 | Interaction(function="find", input={"target": 1}, output=True), 263 | Interaction(function="find", input={"target": 3}, output=True), 264 | Interaction(function="find", input={"target": 5}, output=False), 265 | ], 266 | [ 267 | Interaction( 268 | function="FindElements", 269 | input={"root": [-1, None, -1, -1, None, -1]}, 270 | output=None, 271 | ), 272 | Interaction(function="find", input={"target": 2}, output=True), 273 | Interaction(function="find", input={"target": 3}, output=False), 274 | Interaction(function="find", input={"target": 4}, output=False), 275 | Interaction(function="find", input={"target": 5}, output=True), 276 | ], 277 | ], 278 | ) 279 | self._test_parse_problem(problem, signature) 280 | 281 | def test_parse_problem_3(self): 282 | problem = Problem( 283 | url="", 284 | name="Greatest Sum Divisible by Three", 285 | statement="", 286 | examples=[ 287 | ( 288 | "Input: nums = [3,6,5,1,8]\n" 289 | "Output: 18\n" 290 | "Explanation: Pick numbers 3, 6, 1 and 8 their sum is 18 (maximum sum divisible by 3)." 291 | ), 292 | ( 293 | "Input: nums = [4]\n" 294 | "Output: 0\n" 295 | "Explanation: Since 4 is not divisible by 3, do not pick any number." 296 | ), 297 | ( 298 | "Input: nums = [1,2,3,4,4]\n" 299 | "Output: 12\n" 300 | "Explanation: Pick numbers 1, 3, 4 and 4 their sum is 12 (maximum sum divisible by 3)." 301 | ), 302 | ], 303 | code=[ 304 | "class Solution {", 305 | "public:", 306 | " int maxSumDivThree(vector& nums) {", 307 | " ", 308 | " }", 309 | "};", 310 | ], 311 | ) 312 | signature = ProblemSignature( 313 | function=FunctionSignature( 314 | return_type="int", 315 | name="maxSumDivThree", 316 | arguments=[("vector&", "nums")], 317 | ), 318 | examples=[ 319 | Example({"nums": [3, 6, 5, 1, 8]}, 18), 320 | Example({"nums": [4]}, 0), 321 | Example({"nums": [1, 2, 3, 4, 4]}, 12), 322 | ], 323 | ) 324 | self._test_parse_problem(problem, signature) 325 | 326 | def test_parse_problem_4(self): 327 | problem = Problem( 328 | url="", 329 | name="Minimum Moves to Move a Box to Their Target Location", 330 | statement="", 331 | examples=[ 332 | ( 333 | 'Input: grid = [["#","#","#","#","#","#"],\n' 334 | ' ["#","T","#","#","#","#"],\n' 335 | ' ["#",".",".","B",".","#"],\n' 336 | ' ["#",".","#","#",".","#"],\n' 337 | ' ["#",".",".",".","S","#"],\n' 338 | ' ["#","#","#","#","#","#"]]\n' 339 | "Output: 3\n" 340 | "Explanation: We return only the number of times the box is pushed." 341 | ), 342 | ( 343 | 'Input: grid = [["#","#","#","#","#","#"],\n' 344 | ' ["#","T","#","#","#","#"],\n' 345 | ' ["#",".",".","B",".","#"],\n' 346 | ' ["#","#","#","#",".","#"],\n' 347 | ' ["#",".",".",".","S","#"],\n' 348 | ' ["#","#","#","#","#","#"]]\n' 349 | "Output: -1" 350 | ), 351 | ( 352 | 'Input: grid = [["#","#","#","#","#","#"],\n' 353 | ' ["#","T",".",".","#","#"],\n' 354 | ' ["#",".","#","B",".","#"],\n' 355 | ' ["#",".",".",".",".","#"],\n' 356 | ' ["#",".",".",".","S","#"],\n' 357 | ' ["#","#","#","#","#","#"]]\n' 358 | "Output: 5\n" 359 | "Explanation: push the box down, left, left, up and up." 360 | ), 361 | ( 362 | 'Input: grid = [["#","#","#","#","#","#","#"],\n' 363 | ' ["#","S","#",".","B","T","#"],\n' 364 | ' ["#","#","#","#","#","#","#"]]\n' 365 | "Output: -1" 366 | ), 367 | ], 368 | code=[ 369 | "class Solution {", 370 | "public:", 371 | " int minPushBox(vector>& grid) {", 372 | " ", 373 | " }", 374 | "};", 375 | ], 376 | ) 377 | signature = ProblemSignature( 378 | function=FunctionSignature( 379 | return_type="int", 380 | name="minPushBox", 381 | arguments=[("vector>&", "grid")], 382 | ), 383 | examples=[ 384 | Example( 385 | { 386 | "grid": [ 387 | ["#", "#", "#", "#", "#", "#"], 388 | ["#", "T", "#", "#", "#", "#"], 389 | ["#", ".", ".", "B", ".", "#"], 390 | ["#", ".", "#", "#", ".", "#"], 391 | ["#", ".", ".", ".", "S", "#"], 392 | ["#", "#", "#", "#", "#", "#"], 393 | ] 394 | }, 395 | 3, 396 | ), 397 | Example( 398 | { 399 | "grid": [ 400 | ["#", "#", "#", "#", "#", "#"], 401 | ["#", "T", "#", "#", "#", "#"], 402 | ["#", ".", ".", "B", ".", "#"], 403 | ["#", "#", "#", "#", ".", "#"], 404 | ["#", ".", ".", ".", "S", "#"], 405 | ["#", "#", "#", "#", "#", "#"], 406 | ] 407 | }, 408 | -1, 409 | ), 410 | Example( 411 | { 412 | "grid": [ 413 | ["#", "#", "#", "#", "#", "#"], 414 | ["#", "T", ".", ".", "#", "#"], 415 | ["#", ".", "#", "B", ".", "#"], 416 | ["#", ".", ".", ".", ".", "#"], 417 | ["#", ".", ".", ".", "S", "#"], 418 | ["#", "#", "#", "#", "#", "#"], 419 | ] 420 | }, 421 | 5, 422 | ), 423 | Example( 424 | { 425 | "grid": [ 426 | ["#", "#", "#", "#", "#", "#", "#"], 427 | ["#", "S", "#", ".", "B", "T", "#"], 428 | ["#", "#", "#", "#", "#", "#", "#"], 429 | ] 430 | }, 431 | -1, 432 | ), 433 | ], 434 | ) 435 | self._test_parse_problem(problem, signature) 436 | --------------------------------------------------------------------------------