├── testutils ├── rust │ ├── .gitignore │ ├── src │ │ ├── lib.rs │ │ ├── utils.rs │ │ ├── parse.rs │ │ ├── list.rs │ │ └── tree.rs │ ├── Cargo.toml │ └── Cargo.lock ├── python │ ├── README.md │ ├── src │ │ └── leetgo_py │ │ │ ├── utils.py │ │ │ ├── __init__.py │ │ │ ├── list.py │ │ │ ├── tree.py │ │ │ └── parse.py │ └── pyproject.toml ├── go │ ├── go.mod │ ├── utils.go │ ├── serialize_test.go │ ├── go.sum │ ├── prefdefined_test.go │ ├── parse_test.go │ ├── parse.go │ └── predefined.go └── cpp │ ├── header.go │ ├── tests │ └── tests.cpp │ ├── stdc++.h │ └── LC_IO.h ├── main.go ├── constants ├── consts_unix.go ├── consts_win.go └── constants.go ├── .gitignore ├── .github ├── renovate.json5 ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yaml └── workflows │ ├── ci.yaml │ └── release.yaml ├── scripts ├── completions.sh ├── fetch │ └── fetch_question_data.go ├── analyze │ └── analyze.go ├── update_readme.go └── install.sh ├── Makefile ├── lang ├── rust_test.go ├── dep.go ├── range_test.go ├── judge.go ├── list.go ├── gen.go └── testcase.go ├── config ├── styles.go ├── state.go └── encoder.go ├── cmd ├── cache.go ├── open.go ├── edit.go ├── debug.go ├── git.go ├── tui.go ├── pick.go ├── submit.go ├── init.go ├── fix.go ├── info.go ├── root.go ├── test.go └── contest.go ├── utils ├── wait.go ├── str_test.go ├── file.go └── str.go ├── leetcode ├── cache.go ├── contest.go ├── cache_json.go ├── decoder.go ├── qid.go └── credential.go ├── .golangci.yaml ├── LICENSE ├── misc └── demo.tape ├── editor └── editor.go ├── .goreleaser.yaml └── go.mod /testutils/rust/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/j178/leetgo/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /testutils/python/README.md: -------------------------------------------------------------------------------- 1 | # leetgo-py 2 | 3 | Python test utils for [leetgo](https://github.com/j178/leetgo). 4 | -------------------------------------------------------------------------------- /constants/consts_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package constants 4 | 5 | const ( 6 | DefaultPython = "python3" 7 | VenvPython = "bin/python" 8 | ) 9 | -------------------------------------------------------------------------------- /constants/consts_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package constants 4 | 5 | const ( 6 | DefaultPython = "python.exe" 7 | VenvPython = "Scripts/python.exe" 8 | ) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | leetgo 2 | leetgo.exe 3 | .idea 4 | .DS_Store 5 | leetgo.yaml 6 | completions/ 7 | dist/ 8 | out/ 9 | go.work 10 | go.work.sum 11 | misc/*.gif 12 | misc/questions.json 13 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "schedule:monthly", 6 | ], 7 | "prHourlyLimit": 10, 8 | } 9 | -------------------------------------------------------------------------------- /scripts/completions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | rm -rf completions 4 | mkdir completions 5 | export CGO_ENABLED=0 6 | for sh in bash zsh fish; do 7 | go run main.go completion "$sh" >"completions/leetgo.$sh" 8 | done 9 | -------------------------------------------------------------------------------- /testutils/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use list::{LinkedList, ListNode}; 2 | pub use parse::{deserialize, serialize, split_array}; 3 | pub use tree::{BinaryTree, TreeNode}; 4 | pub use utils::*; 5 | 6 | mod list; 7 | mod tree; 8 | mod parse; 9 | mod utils; 10 | -------------------------------------------------------------------------------- /testutils/python/src/leetgo_py/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import List 3 | 4 | 5 | def join_array(arr: List[str]) -> str: 6 | return "[" + ",".join(arr) + "]" 7 | 8 | 9 | def read_line() -> str: 10 | return sys.stdin.readline().strip() 11 | -------------------------------------------------------------------------------- /testutils/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leetgo-rs" 3 | version = "0.2.1" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Rust test utils for leetgo" 7 | homepage = "https://github.com/j178/leetgo" 8 | 9 | [dependencies] 10 | anyhow = "1.0.70" 11 | serde = "1.0.156" 12 | serde_json = "1.0.94" 13 | -------------------------------------------------------------------------------- /testutils/rust/src/utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | pub fn read_line() -> Result { 4 | let mut line = String::new(); 5 | std::io::stdin().read_line(&mut line)?; 6 | Ok(line) 7 | } 8 | 9 | pub fn join_array(arr: Vec) -> String { 10 | "[".to_string() + &arr.join(",") + "]" 11 | } 12 | -------------------------------------------------------------------------------- /testutils/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/j178/leetgo/testutils/go 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/goccy/go-json v0.10.5 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /testutils/go/utils.go: -------------------------------------------------------------------------------- 1 | package goutils 2 | 3 | import ( 4 | "bufio" 5 | "strings" 6 | ) 7 | 8 | func ReadLine(r *bufio.Reader) string { 9 | if line, err := r.ReadString('\n'); err != nil { 10 | panic(err) 11 | } else { 12 | return line 13 | } 14 | } 15 | 16 | func JoinArray(s []string) string { 17 | return "[" + strings.Join(s, ",") + "]" 18 | } 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install-tools: 2 | brew install golangci-lint 3 | brew install caarlos0/tap/svu 4 | 5 | release-patch: 6 | git tag -a $(shell svu patch) 7 | git push --tags 8 | 9 | release-pypi: 10 | git tag -a $(shell svu patch --prefix 'testutils/python/') 11 | git push --tags 12 | 13 | release-cargo: 14 | git tag -a $(shell svu patch --prefix 'testutils/rust/') 15 | git push --tags 16 | -------------------------------------------------------------------------------- /testutils/python/src/leetgo_py/__init__.py: -------------------------------------------------------------------------------- 1 | from .list import ListNode 2 | from .tree import TreeNode 3 | from .parse import split_array, deserialize, serialize 4 | from .utils import join_array, read_line 5 | 6 | 7 | __all__ = [ 8 | "ListNode", 9 | "TreeNode", 10 | "split_array", 11 | "deserialize", 12 | "serialize", 13 | "join_array", 14 | "read_line", 15 | ] 16 | -------------------------------------------------------------------------------- /lang/rust_test.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import "testing" 4 | 5 | func TestToRustVarName(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | want string 9 | }{ 10 | {"query", "query"}, 11 | {"isValidBST", "is_valid_bst"}, 12 | } 13 | for _, tt := range tests { 14 | if got := toRustVarName(tt.name); got != tt.want { 15 | t.Errorf("toRustVarName(%v) = %v, want %v", tt.name, got, tt.want) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /config/styles.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | var ( 6 | SkippedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#b8b8b8")) 7 | PassedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00b300")) 8 | ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0000")) 9 | FailedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff6600")) 10 | StdoutStyle = lipgloss.NewStyle().Faint(true) 11 | ) 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | 3 | contact_links: 4 | - name: Get help in GitHub Discussions 5 | url: https://github.com/j178/leetgo/discussions 6 | about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on GitHub Discussions! 7 | - name: Chat in Discord 8 | url: https://discord.gg/bHsEwQQj9m 9 | about: Want to chat with other users? Join our Discord server! 10 | -------------------------------------------------------------------------------- /testutils/cpp/header.go: -------------------------------------------------------------------------------- 1 | package cpp 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | const HeaderName = "LC_IO.h" 8 | 9 | // StdCxxContent is stdc++.h from gcc 12.2.0 10 | // "cstdalign", "cuchar", "memory_resource" commented out for compatibility with clang 11 | // https://github.com/gcc-mirror/gcc/raw/releases/gcc-12.2.0/libstdc%2B%2B-v3/include/precompiled/stdc%2B%2B.h 12 | // 13 | //go:embed stdc++.h 14 | var StdCxxContent []byte 15 | 16 | //go:embed LC_IO.h 17 | var HeaderContent []byte 18 | -------------------------------------------------------------------------------- /constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | var ( 4 | Version = "dev" 5 | Commit = "HEAD" 6 | BuildDate = "unknown" 7 | ) 8 | 9 | const ( 10 | CmdName = "leetgo" 11 | ConfigFilename = "leetgo.yaml" 12 | QuestionCacheBaseName = "leetcode-questions" 13 | StateFilename = "state.json" 14 | DepVersionFilename = "deps.json" 15 | CodeBeginMarker = "@lc code=begin" 16 | CodeEndMarker = "@lc code=end" 17 | ProjectURL = "https://github.com/j178/leetgo" 18 | ) 19 | -------------------------------------------------------------------------------- /cmd/cache.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/j178/leetgo/leetcode" 7 | ) 8 | 9 | var cacheCmd = &cobra.Command{ 10 | Use: "cache", 11 | Short: "Manage local questions cache", 12 | } 13 | 14 | var cacheUpdateCmd = &cobra.Command{ 15 | Use: "update", 16 | Short: "Update local questions cache", 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | c := leetcode.NewClient(leetcode.ReadCredentials()) 19 | return leetcode.GetCache(c).Update() 20 | }, 21 | } 22 | 23 | func init() { 24 | cacheCmd.AddCommand(cacheUpdateCmd) 25 | } 26 | -------------------------------------------------------------------------------- /utils/wait.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | type RateLimiter struct { 6 | per time.Duration 7 | last time.Time 8 | } 9 | 10 | func NewRateLimiter(per time.Duration) *RateLimiter { 11 | r := &RateLimiter{ 12 | per: per, 13 | last: time.Time{}, 14 | } 15 | return r 16 | } 17 | 18 | func (r *RateLimiter) Take() { 19 | now := time.Now() 20 | if r.last.IsZero() { 21 | r.last = now 22 | return 23 | } 24 | 25 | sleep := r.per - now.Sub(r.last) 26 | if sleep > 0 { 27 | time.Sleep(sleep) 28 | r.last = now.Add(sleep) 29 | } else { 30 | r.last = now 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /leetcode/cache.go: -------------------------------------------------------------------------------- 1 | package leetcode 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/j178/leetgo/config" 7 | ) 8 | 9 | type QuestionsCache interface { 10 | CacheFile() string 11 | GetBySlug(slug string) *QuestionData 12 | GetById(id string) *QuestionData 13 | GetAllQuestions() []*QuestionData 14 | Outdated() bool 15 | Update() error 16 | } 17 | 18 | func GetCache(c Client) QuestionsCache { 19 | once.Do( 20 | func() { 21 | cfg := config.Get() 22 | lazyCache = newCache(cfg.QuestionCacheFile(cacheExt), c) 23 | }, 24 | ) 25 | return lazyCache 26 | } 27 | 28 | var ( 29 | lazyCache QuestionsCache 30 | once sync.Once 31 | ) 32 | -------------------------------------------------------------------------------- /testutils/python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "leetgo-py" 3 | version = "0.2.4" 4 | description = "Python test utils for leetgo" 5 | authors = ["j178 <10510431+j178@users.noreply.github.com>"] 6 | license = "MIT" 7 | homepage = "https://github.com/j178/leetgo" 8 | repository = "https://github.com/j178/leetgo" 9 | keywords = ["leetcode"] 10 | readme = "README.md" 11 | 12 | packages = [ 13 | { include = "leetgo_py", from = "src"}, 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.6" 18 | 19 | [tool.poetry.dev-dependencies] 20 | 21 | [build-system] 22 | requires = ["poetry-core>=1.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | exclusions: 4 | generated: lax 5 | presets: 6 | - comments 7 | - common-false-positives 8 | - legacy 9 | - std-error-handling 10 | paths: 11 | - out 12 | - third_party$ 13 | - builtin$ 14 | - examples$ 15 | formatters: 16 | enable: 17 | - gci 18 | - gofumpt 19 | settings: 20 | gci: 21 | sections: 22 | - standard 23 | - default 24 | - prefix(github.com/j178/leetgo) 25 | custom-order: true 26 | exclusions: 27 | generated: lax 28 | paths: 29 | - out 30 | - third_party$ 31 | - builtin$ 32 | - examples$ 33 | -------------------------------------------------------------------------------- /testutils/go/serialize_test.go: -------------------------------------------------------------------------------- 1 | package goutils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestInfiniteLoopDetect(t *testing.T) { 11 | linkedList := &ListNode{Val: 1} 12 | linkedList.Next = &ListNode{Val: 2, Next: linkedList} 13 | 14 | tree := &TreeNode{Val: 1} 15 | tree.Left = &TreeNode{Val: 2, Right: tree} 16 | 17 | naryTree := &NaryTreeNode{Val: 1} 18 | naryTree.Children = []*NaryTreeNode{{Val: 2, Children: []*NaryTreeNode{naryTree}}} 19 | 20 | tests := []fmt.Stringer{ 21 | linkedList, 22 | tree, 23 | naryTree, 24 | } 25 | 26 | for _, tc := range tests { 27 | assert.PanicsWithValue(t, ErrInfiniteLoop, func() { _ = tc.String() }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmd/open.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/cli/browser" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/j178/leetgo/leetcode" 8 | ) 9 | 10 | var openCmd = &cobra.Command{ 11 | Use: "open qid", 12 | Short: "Open one or multiple question pages in a browser", 13 | Example: `leetgo open today 14 | leetgo open 549 15 | leetgo open w330/`, 16 | Args: cobra.ExactArgs(1), 17 | ValidArgs: []string{"today", "last"}, 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | c := leetcode.NewClient(leetcode.ReadCredentials()) 20 | qid := args[0] 21 | qs, err := leetcode.ParseQID(qid, c) 22 | if err != nil { 23 | return err 24 | } 25 | for _, q := range qs { 26 | if q.IsContest() { 27 | err = browser.OpenURL(q.ContestUrl()) 28 | } else { 29 | err = browser.OpenURL(q.Url()) 30 | } 31 | } 32 | return err 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /testutils/python/src/leetgo_py/list.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional 3 | 4 | 5 | class ListNode: 6 | def __init__(self, val: int = 0, next: "ListNode" = None): 7 | self.val = val 8 | self.next = next 9 | 10 | @classmethod 11 | def deserialize(cls, s: str) -> Optional["ListNode"]: 12 | arr = json.loads(s) 13 | if not arr: 14 | return None 15 | root = ListNode(arr[0]) 16 | node = root 17 | for v in arr[1:]: 18 | node.next = ListNode(v) 19 | node = node.next 20 | return root 21 | 22 | def serialize(self) -> str: 23 | s = ["["] 24 | node = self 25 | while node: 26 | s.append(str(node.val)) 27 | node = node.next 28 | if node: 29 | s.append(",") 30 | s.append("]") 31 | return "".join(s) 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | title: "[Bug] " 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: "leetgo debug" 8 | description: " 9 | 在下方附上 `leetgo debug` 的输出 10 | Paste the output of `leetgo debug` below 11 | " 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | render: shell 17 | label: Debug log 18 | description: " 19 | 请设置环境变量 `DEBUG=1`(PowerShell: `$env:DEBUG=1`, CMD: `set DEBUG=1`, Others: `export DEBUG=1`) 后再运行有问题的 leetgo 命令,并在下方附上 `leetgo` 的输出 20 | Set environment variable `DEBUG=1`(PowerShell: `$env:DEBUG=1`, CMD: `set DEBUG=1`, Others: `export DEBUG=1`) and run the leetgo command that has the issue, and paste the output of `leetgo` below 21 | " 22 | - type: textarea 23 | attributes: 24 | label: Description 25 | validations: 26 | required: true 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | permissions: 8 | contents: read 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: '1.24.x' 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v7 19 | with: 20 | version: latest 21 | args: --timeout 3m0s 22 | test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-go@v5 27 | with: 28 | go-version: '1.24.x' 29 | - name: check-readme 30 | run: | 31 | go run ./scripts/update_readme.go 32 | git diff --exit-code README_zh.md README.md || (echo "README.md is not up to date. Please run 'go run ./scripts/update_readme.go' and commit the changes." && exit 1) 33 | 34 | - name: test 35 | run: | 36 | go test -v ./... 37 | cd ./testutils/go/... && go test -v ./... 38 | cd ./testutils/cpp/tests && g++ -std=c++17 -O2 -o tests tests.cpp && ./tests 39 | -------------------------------------------------------------------------------- /scripts/fetch/fetch_question_data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/goccy/go-json" 9 | 10 | "github.com/j178/leetgo/leetcode" 11 | ) 12 | 13 | func main() { 14 | client := leetcode.NewClient(leetcode.ReadCredentials()) 15 | cache := leetcode.GetCache(client) 16 | questions := cache.GetAllQuestions() 17 | paidOnly := 0 18 | for _, q := range questions { 19 | if q.IsPaidOnly { 20 | paidOnly++ 21 | } 22 | } 23 | fmt.Printf("Total questions: %d, paid only: %d\n", len(questions), paidOnly) 24 | 25 | for i, q := range questions { 26 | if q.IsPaidOnly { 27 | continue 28 | } 29 | err := q.Fulfill() 30 | if err != nil { 31 | fmt.Printf("fetch error: %s, q=%s\n", err, q.TitleSlug) 32 | continue 33 | } 34 | if i > 0 && i%100 == 0 { 35 | fmt.Printf("\rfetching %d/%d", i+1, len(questions)) 36 | save(questions) 37 | } 38 | time.Sleep(10 * time.Millisecond) 39 | } 40 | fmt.Println("\nDone") 41 | } 42 | 43 | func save(questions []*leetcode.QuestionData) { 44 | f, err := os.Create("./misc/questions.json") 45 | if err != nil { 46 | panic(err) 47 | } 48 | defer f.Close() 49 | enc := json.NewEncoder(f) 50 | enc.SetIndent("", " ") 51 | err = enc.Encode(questions) 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scripts/analyze/analyze.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/goccy/go-json" 9 | 10 | "github.com/j178/leetgo/config" 11 | "github.com/j178/leetgo/lang" 12 | "github.com/j178/leetgo/leetcode" 13 | ) 14 | 15 | func main() { 16 | f, err := os.Open("misc/questions.json") 17 | if err != nil { 18 | panic(err) 19 | } 20 | defer f.Close() 21 | 22 | _ = os.Chdir(os.Getenv("LEETGO_WORKDIR")) 23 | err = config.Load(false) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | var questions []*leetcode.QuestionData 29 | err = json.NewDecoder(f).Decode(&questions) 30 | if err != nil { 31 | panic(err) 32 | } 33 | c := leetcode.NewClient(leetcode.NonAuth()) 34 | 35 | categories := map[leetcode.CategoryTitle]int{} 36 | for _, q := range questions { 37 | q.SetClient(c) 38 | 39 | categories[q.CategoryTitle]++ 40 | if q.MetaData.Manual && q.CategoryTitle == leetcode.CategoryAlgorithms { 41 | fmt.Printf("%s.%s\n", q.QuestionFrontendId, q.TitleSlug) 42 | out, err := lang.Generate(q) 43 | if err != nil { 44 | fmt.Println(err) 45 | continue 46 | } 47 | f, _ := os.Create(filepath.Join(out.TargetDir(), "question.json")) 48 | enc := json.NewEncoder(f) 49 | enc.SetIndent("", " ") 50 | _ = enc.Encode(q) 51 | f.Close() 52 | } 53 | } 54 | 55 | fmt.Printf("total: %d, %v\n", len(questions), categories) 56 | } 57 | -------------------------------------------------------------------------------- /testutils/python/src/leetgo_py/tree.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional 3 | 4 | 5 | class TreeNode: 6 | def __init__(self, val: int, left: "TreeNode" = None, right: "TreeNode" = None): 7 | self.val = val 8 | self.left = left 9 | self.right = right 10 | 11 | @classmethod 12 | def deserialize(cls, s: str) -> Optional["TreeNode"]: 13 | res = json.loads(s) 14 | if not res: 15 | return None 16 | 17 | nodes = [TreeNode(val) if val is not None else None for val in res] 18 | root = nodes[0] 19 | 20 | j = 1 21 | for node in nodes: 22 | if node is not None: 23 | if j < len(res): 24 | node.left = nodes[j] 25 | j += 1 26 | if j < len(res): 27 | node.right = nodes[j] 28 | j += 1 29 | if j >= len(res): 30 | break 31 | 32 | return root 33 | 34 | def serialize(self) -> str: 35 | nodes = [] 36 | queue = [self] 37 | 38 | while queue: 39 | t = queue.pop(0) 40 | nodes.append(t) 41 | 42 | if t is not None: 43 | queue.extend([t.left, t.right]) 44 | 45 | while nodes and nodes[-1] is None: 46 | nodes.pop() 47 | 48 | arr = [node.val if node is not None else None for node in nodes] 49 | return json.dumps(arr) 50 | -------------------------------------------------------------------------------- /cmd/edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/j178/leetgo/editor" 9 | "github.com/j178/leetgo/lang" 10 | "github.com/j178/leetgo/leetcode" 11 | ) 12 | 13 | var editCmd = &cobra.Command{ 14 | Use: "edit qid", 15 | Short: "Open solution in editor", 16 | Aliases: []string{"e"}, 17 | Args: cobra.ExactArgs(1), 18 | ValidArgs: []string{"today", "last"}, 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | c := leetcode.NewClient(leetcode.ReadCredentials()) 21 | qs, err := leetcode.ParseQID(args[0], c) 22 | if err != nil { 23 | return err 24 | } 25 | if len(qs) > 1 { 26 | return fmt.Errorf("multiple questions found") 27 | } 28 | result, err := lang.GeneratePathsOnly(qs[0]) 29 | if err != nil { 30 | return err 31 | } 32 | return editor.Open(result) 33 | }, 34 | } 35 | 36 | var extractCmd = &cobra.Command{ 37 | Use: "extract qid", 38 | Short: "Extract solution code from generated file", 39 | Args: cobra.ExactArgs(1), 40 | Hidden: true, 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | c := leetcode.NewClient(leetcode.ReadCredentials()) 43 | qs, err := leetcode.ParseQID(args[0], c) 44 | if err != nil { 45 | return err 46 | } 47 | if len(qs) > 1 { 48 | return fmt.Errorf("multiple questions found") 49 | } 50 | code, err := lang.GetSolutionCode(qs[0]) 51 | if err != nil { 52 | return err 53 | } 54 | cmd.Println(code) 55 | return nil 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /testutils/rust/src/parse.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::Value; 4 | 5 | pub fn deserialize<'de, T: Deserialize<'de>>(s: &'de str) -> Result { 6 | let res: T = serde_json::from_str(s)?; 7 | Ok(res) 8 | } 9 | 10 | pub fn serialize(v: T) -> Result { 11 | let res = serde_json::to_string(&v)?; 12 | Ok(res) 13 | } 14 | 15 | pub fn split_array(raw: &str) -> Result> { 16 | let trimmed = raw.trim(); 17 | 18 | if trimmed.len() <= 1 || !trimmed.starts_with('[') || !trimmed.ends_with(']') { 19 | bail!("invalid array: {}", trimmed); 20 | } 21 | 22 | let splits: Vec = serde_json::from_str(trimmed)?; 23 | let res: Vec = splits.iter().map(|v| v.to_string()).collect(); 24 | Ok(res) 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | #[test] 32 | fn test_split_array() { 33 | let test_cases = vec![ 34 | ("[]", vec![]), 35 | ("[1]", vec!["1"]), 36 | (r#"["a", "b"]"#, vec![r#""a""#, r#""b""#]), 37 | ("[1, 2, 3]", vec!["1", "2", "3"]), 38 | (r#"[1, "a", null, true, false]"#, vec!["1", r#""a""#, "null", "true", "false"]), 39 | ("[1, [2, 3], 4]", vec!["1", "[2,3]", "4"]), 40 | (" [1, 2] ", vec!["1", "2"]), 41 | ]; 42 | 43 | for (input, expected) in test_cases { 44 | let result = split_array(input); 45 | match result { 46 | Ok(res) => assert_eq!(res, expected), 47 | Err(_) => panic!("Test failed for input: {}", input), 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/state.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/goccy/go-json" 8 | 9 | "github.com/j178/leetgo/utils" 10 | ) 11 | 12 | // Small project state management. 13 | 14 | type LastQuestion struct { 15 | FrontendID string `json:"frontend_id"` 16 | Slug string `json:"slug"` 17 | Gen string `json:"gen"` 18 | } 19 | 20 | type State struct { 21 | LastQuestion LastQuestion `json:"last_question"` 22 | LastContest string `json:"last_contest"` 23 | } 24 | 25 | type States map[string]State 26 | 27 | func loadStates() States { 28 | s := make(States) 29 | 30 | file := Get().StateFile() 31 | f, err := os.Open(file) 32 | if err != nil { 33 | log.Debug("failed to open state file", "err", err) 34 | return s 35 | } 36 | defer func() { _ = f.Close() }() 37 | 38 | dec := json.NewDecoder(f) 39 | err = dec.Decode(&s) 40 | if err != nil { 41 | log.Debug("failed to load state", "err", err) 42 | } 43 | 44 | return s 45 | } 46 | 47 | func LoadState() State { 48 | s := loadStates() 49 | projectRoot := Get().ProjectRoot() 50 | return s[projectRoot] 51 | } 52 | 53 | func SaveState(s State) { 54 | projectRoot := Get().ProjectRoot() 55 | file := Get().StateFile() 56 | states := loadStates() 57 | states[projectRoot] = s 58 | 59 | err := utils.CreateIfNotExists(file, false) 60 | if err != nil { 61 | log.Error("failed to create state file", "err", err) 62 | return 63 | } 64 | f, err := os.OpenFile(file, os.O_WRONLY|os.O_TRUNC, 0o644) 65 | if err != nil { 66 | log.Error("failed to open state file", "err", err) 67 | return 68 | } 69 | enc := json.NewEncoder(f) 70 | err = enc.Encode(states) 71 | if err != nil { 72 | log.Error("failed to save state", "err", err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lang/dep.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/goccy/go-json" 8 | 9 | "github.com/j178/leetgo/config" 10 | ) 11 | 12 | // If client dependency needs to be updated, update this version number. 13 | var depVersions = map[string]int{ 14 | cppGen.slug: 1, 15 | golangGen.slug: 3, 16 | python3Gen.slug: 1, 17 | rustGen.slug: 1, 18 | } 19 | 20 | func readDepVersions() (map[string]int, error) { 21 | depVersionFile := config.Get().DepVersionFile() 22 | records := make(map[string]int) 23 | f, err := os.Open(depVersionFile) 24 | if errors.Is(err, os.ErrNotExist) { 25 | return records, nil 26 | } 27 | if err != nil { 28 | return nil, err 29 | } 30 | defer f.Close() 31 | err = json.NewDecoder(f).Decode(&records) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return records, nil 36 | } 37 | 38 | func IsDepUpdateToDate(lang Lang) (bool, error) { 39 | ver := depVersions[lang.Slug()] 40 | if ver == 0 { 41 | return true, nil 42 | } 43 | 44 | records, err := readDepVersions() 45 | if err != nil { 46 | return false, err 47 | } 48 | old := records[lang.Slug()] 49 | if old == 0 || old != ver { 50 | return false, nil 51 | } 52 | 53 | return true, nil 54 | } 55 | 56 | func UpdateDep(lang Lang) error { 57 | ver := depVersions[lang.Slug()] 58 | if ver == 0 { 59 | return nil 60 | } 61 | 62 | records, err := readDepVersions() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | records[lang.Slug()] = ver 68 | 69 | depVersionFile := config.Get().DepVersionFile() 70 | f, err := os.Create(depVersionFile) 71 | if err != nil { 72 | return err 73 | } 74 | defer f.Close() 75 | enc := json.NewEncoder(f) 76 | enc.SetIndent("", " ") 77 | err = enc.Encode(records) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /testutils/cpp/tests/tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../LC_IO.h" 4 | 5 | using namespace std; 6 | 7 | int main_ret = 0; 8 | 9 | template 10 | void test_scan_print(const char *raw) { 11 | std::stringstream in, out; 12 | in << raw; 13 | T x; 14 | LeetCodeIO::scan(in, x); 15 | LeetCodeIO::print(out, x); 16 | if (in.str() == out.str()) { 17 | printf("passed\n"); 18 | } else { 19 | printf("want: %s, got: %s\n", raw, out.str().c_str()); 20 | main_ret = 1; 21 | } 22 | } 23 | 24 | void test_all() { 25 | test_scan_print("19890604"); 26 | test_scan_print("1989060419890604"); 27 | test_scan_print("true"); 28 | test_scan_print("\"a\""); 29 | test_scan_print("\"hello\""); 30 | test_scan_print("1.98964"); 31 | test_scan_print("[19,89,0,6,0,4]"); 32 | test_scan_print("[1989,null,6,null,4]"); 33 | 34 | test_scan_print>>("[[1989,6,4],[19890604],[]]"); 35 | test_scan_print>>("[[1989060419890604,1989,6,4],[1989060419890604],[]]"); 36 | //test_scan_print>("[[true,false,true],[false],[]]"); // https://isocpp.org/blog/2012/11/on-vectorbool 37 | test_scan_print>>("[[\"t\",\"i\",\"a\",\"n\",\"a\",\"n\",\"m\",\"e\",\"n\"],[\"s\",\"q\",\"u\",\"a\",\"r\",\"e\"],[]]"); 38 | test_scan_print>>("[[\"tiananmen\",\"square\"],[""],[]]"); 39 | test_scan_print>>("[[1989.06040,19.89640],[1.98964],[]]"); 40 | test_scan_print>>("[[[19,89,0,6,0,4],[1989,6,4]],[[19890604]],[]]"); 41 | test_scan_print>>("[[[1989,null,6,null,4],[1989,6,4]],[[19890604]],[]]"); 42 | } 43 | 44 | int main() { 45 | test_all(); 46 | return main_ret; 47 | } 48 | -------------------------------------------------------------------------------- /utils/str_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/j178/leetgo/utils" 7 | ) 8 | 9 | func TestCondenseEmptyLines(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | input string 13 | expected string 14 | }{ 15 | { 16 | name: "No empty lines", 17 | input: "line 1\nline 2\nline 3", 18 | expected: "line 1\nline 2\nline 3", 19 | }, 20 | { 21 | name: "One empty line", 22 | input: "line 1\n\nline 2\nline 3", 23 | expected: "line 1\n\nline 2\nline 3", 24 | }, 25 | { 26 | name: "Two empty lines", 27 | input: "line 1\n\n\nline 2\nline 3", 28 | expected: "line 1\n\nline 2\nline 3", 29 | }, 30 | { 31 | name: "Multiple empty lines", 32 | input: "line 1\n\n\n\n\n\n\nline 2\nline 3\n\n\n\n\nline 4", 33 | expected: "line 1\n\nline 2\nline 3\n\nline 4", 34 | }, 35 | } 36 | 37 | for _, tc := range testCases { 38 | t.Run( 39 | tc.name, func(t *testing.T) { 40 | actual := utils.CondenseEmptyLines(tc.input) 41 | if actual != tc.expected { 42 | t.Errorf("Expected result '%s', but got '%s'", tc.expected, actual) 43 | } 44 | }, 45 | ) 46 | } 47 | } 48 | 49 | func TestDecodeRawUnicodeEscape(t *testing.T) { 50 | tests := []struct { 51 | input string 52 | output string 53 | }{ 54 | { 55 | input: "Hello\\u0020world", 56 | output: "Hello world", 57 | }, 58 | { 59 | input: "\\u00a9 2023", 60 | output: "© 2023", 61 | }, 62 | { 63 | input: "\\u4e16\\u754c\\u60a8\\u597d", 64 | output: "世界您好", 65 | }, 66 | } 67 | 68 | for _, tt := range tests { 69 | t.Run( 70 | tt.input, func(t *testing.T) { 71 | got := utils.DecodeRawUnicodeEscape(tt.input) 72 | if got != tt.output { 73 | t.Errorf("DecodeRawUnicodeEscape(%q) = %q; want %q", tt.input, got, tt.output) 74 | } 75 | }, 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /testutils/python/src/leetgo_py/parse.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, List 3 | 4 | from . import ListNode, TreeNode 5 | 6 | 7 | def split_array(s: str) -> List[str]: 8 | s = s.strip() 9 | if len(s) <= 1 or s[0] != "[" or s[-1] != "]": 10 | raise Exception("Invalid array: " + s) 11 | 12 | splits = json.loads(s) 13 | res = [json.dumps(split) for split in splits] 14 | return res 15 | 16 | 17 | def serialize(val: Any, ty: str = None) -> str: 18 | if val is None: 19 | if ty is None: 20 | raise Exception("None value without type") 21 | if ty == "ListNode" or ty == "TreeNode": 22 | return "[]" 23 | return "null" 24 | elif isinstance(val, bool): 25 | return "true" if val else "false" 26 | elif isinstance(val, int): 27 | return str(val) 28 | elif isinstance(val, float): 29 | return str(val) 30 | elif isinstance(val, str): 31 | return '"' + val + '"' 32 | elif isinstance(val, list): 33 | return "[" + ",".join(serialize(v) for v in val) + "]" 34 | elif isinstance(val, (ListNode, TreeNode)): 35 | return val.serialize() 36 | else: 37 | raise Exception("Unknown type: " + str(type(val))) 38 | 39 | 40 | def deserialize(ty: str, s: str) -> Any: 41 | if ty == "int": 42 | return int(s) 43 | elif ty == "float": 44 | return float(s) 45 | elif ty == "str": 46 | return s[1:-1] 47 | elif ty == "bool": 48 | return s == "true" 49 | elif ty.startswith("List["): 50 | arr = [] 51 | for v in split_array(s): 52 | arr.append(deserialize(ty[5:-1], v)) 53 | return arr 54 | elif ty == "ListNode": 55 | return ListNode.deserialize(s) 56 | elif ty == "TreeNode": 57 | return TreeNode.deserialize(s) 58 | else: 59 | raise Exception("Unknown type: " + ty) 60 | -------------------------------------------------------------------------------- /lang/range_test.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import "testing" 4 | 5 | func TestParseRange(t *testing.T) { 6 | cases := []struct { 7 | input string 8 | max int 9 | ranges [][2]int 10 | whole bool 11 | }{ 12 | {"1", 5, [][2]int{{1, 1}}, false}, 13 | {"1,2", 5, [][2]int{{1, 1}, {2, 2}}, false}, 14 | {"1,2,3", 5, [][2]int{{1, 1}, {2, 2}, {3, 3}}, false}, 15 | {"-", 5, [][2]int{}, true}, 16 | {"", 5, [][2]int{}, true}, 17 | {"-1", 5, [][2]int{{5, 5}}, false}, 18 | {"1-", 5, [][2]int{{1, 5}}, false}, 19 | {"1-2", 5, [][2]int{{1, 2}}, false}, 20 | {"1-2,3", 5, [][2]int{{1, 2}, {3, 3}}, false}, 21 | {"1-2,3,4-", 5, [][2]int{{1, 2}, {3, 3}, {4, 5}}, false}, 22 | {"1-2,3-,5", 5, [][2]int{{1, 2}, {3, 5}, {5, 5}}, false}, 23 | {"1-2,3,4-5", 5, [][2]int{{1, 2}, {3, 3}, {4, 5}}, false}, 24 | {"1--1", 5, [][2]int{{1, 5}}, false}, 25 | {"-1-5", 5, [][2]int{{5, 5}}, false}, 26 | {"-2--1", 5, [][2]int{{4, 5}}, false}, 27 | } 28 | for _, c := range cases { 29 | r, err := ParseRange(c.input, c.max) 30 | if err != nil { 31 | t.Errorf("parse range %s: %v", c.input, err) 32 | } 33 | if r.whole != c.whole { 34 | t.Errorf("parse range %s: whole = %v, want %v", c.input, r.whole, c.whole) 35 | } 36 | if len(r.ranges) != len(c.ranges) { 37 | t.Errorf("parse range %s: ranges = %v, want %v", c.input, r.ranges, c.ranges) 38 | } 39 | for i, rg := range r.ranges { 40 | if rg[0] != c.ranges[i][0] || rg[1] != c.ranges[i][1] { 41 | t.Errorf("parse range %s: ranges = %v, want %v", c.input, r.ranges, c.ranges) 42 | } 43 | } 44 | } 45 | 46 | invalidCases := []string{ 47 | "0", 48 | "100", 49 | "1,100", 50 | "-100", 51 | "1-100", 52 | "1-2,100", 53 | "1-2,100-", 54 | "1-2,100-200", 55 | "a-b", 56 | "1-a", 57 | "100-1", 58 | "1-2-3", 59 | "error", 60 | "----", 61 | "1,,,2", 62 | "-1--2", 63 | "--1", 64 | "--1-2", 65 | "--1--2", 66 | } 67 | for _, c := range invalidCases { 68 | _, err := ParseRange(c, 5) 69 | if err == nil { 70 | t.Errorf("parse range %s: want error", c) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | if: "startsWith(github.ref, 'refs/tags/v')" 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - run: git fetch --force --tags 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version: '1.24.x' 24 | cache: true 25 | - uses: goreleaser/goreleaser-action@v6 26 | with: 27 | distribution: goreleaser 28 | version: latest 29 | args: release --clean 30 | env: 31 | # Custom secret here since we need to access to j178/homebrew-tap and j178/scoop-bucket repo. 32 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 33 | DISCORD_WEBHOOK_ID: ${{ secrets.DISCORD_WEBHOOK_ID }} 34 | DISCORD_WEBHOOK_TOKEN: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} 35 | AUR_PRIVATE_KEY: ${{ secrets.AUR_PRIVATE_KEY }} 36 | 37 | release_pypi: 38 | runs-on: ubuntu-latest 39 | if: "startsWith(github.ref, 'refs/tags/testutils/python/v')" 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/setup-python@v5 43 | with: 44 | python-version: '3.13' 45 | - name: Install deps 46 | run: 47 | python -m pip install build twine 48 | - run: | 49 | python -m build 50 | twine upload dist/* 51 | working-directory: testutils/python 52 | env: 53 | TWINE_USERNAME: __token__ 54 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 55 | 56 | release_cargo: 57 | runs-on: ubuntu-latest 58 | if: "startsWith(github.ref, 'refs/tags/testutils/rust/v')" 59 | steps: 60 | - uses: actions/checkout@v4 61 | - run: cargo publish 62 | working-directory: testutils/rust 63 | env: 64 | RUST_BACKTRACE: 1 65 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 66 | -------------------------------------------------------------------------------- /testutils/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 5 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 6 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 7 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 13 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 15 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 16 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 17 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 18 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 23 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /misc/demo.tape: -------------------------------------------------------------------------------- 1 | # Usage: brew install vhs && vhs misc/demo.tape 2 | 3 | Set Theme "Monokai Vivid" 4 | Set FontSize 20 5 | Set FontFamily "FiraCode Nerd Font Mono" 6 | Set LineHeight 1.2 7 | Set Width 1600 8 | Set Height 800 9 | Set Padding 12 10 | Set Shell fish 11 | Set TypingSpeed 250ms 12 | Set PlaybackSpeed 0.6 13 | 14 | Output misc/demo.gif 15 | 16 | Require leetgo 17 | Require vim 18 | Require tree 19 | 20 | Hide 21 | Type "cd /tmp && rm -rf my-leetcode-solutions && clear" Enter 22 | Show 23 | 24 | Type "# First, let's create a new leetgo workspace" Enter 25 | Sleep 2 26 | Type "mkdir my-leetcode-solutions" Enter 27 | Sleep 2 28 | Type "cd my-leetcode-solutions" Enter 29 | Sleep 2 30 | Type "leetgo init -t cn -l go" Enter 31 | Sleep 2 32 | Type "# A new leetgo.yaml file is created" Enter 33 | Sleep 2 34 | Type "tree" Sleep 1 Enter 35 | Sleep 5 36 | 37 | Type "# Now, let's pick a question to solve" Enter 38 | Sleep 2 39 | Type "leetgo pick 1" Enter 40 | Sleep 5 41 | Type "# Let's see what we got" Enter 42 | Sleep 2 43 | Type "tree" Enter 44 | Sleep 5 45 | 46 | Type "# Test it without changing any code" Enter 47 | Sleep 2 48 | Type "leetgo test last -L" 49 | Sleep 2 50 | Enter 2 51 | Sleep 5 52 | 53 | Type "# Of course, it failed :(" Enter 54 | Sleep 3 55 | 56 | Type "# Let's fix it" Enter 57 | Sleep 2 58 | Type "vim go/0001.two-sum/solution.go" 59 | Sleep 2 60 | Enter 61 | 62 | Type "18Gi" 63 | Sleep 1 64 | Tab Type@0.1 "m := make(map[int]int)" Enter 65 | Tab Type@0.1 "for i, num := range nums {" Enter 66 | Tab 2 Type@0.1 "if j, ok := m[target-num]; ok {" Enter 67 | Tab 3 Type@0.1 "return []int{j, i}" Enter 68 | Tab 2 Type@0.1 "}" Enter 69 | Tab 2 Type@0.1 "m[num] = i" Enter 70 | Tab Type@0.1 "}" 71 | Escape 72 | Sleep 2 73 | Type "G" 74 | Sleep 3 75 | Type ":wq" Enter 76 | Sleep 3 77 | 78 | Type "# Test and submit again" Enter 79 | Sleep 2 80 | Type "leetgo test last -L -s" Enter 81 | Sleep 3 82 | Type "# It's accepted!" Enter 83 | Sleep 3 84 | 85 | Enter 2 86 | Type "# Some other powerful commands you may want to know:" Enter 87 | Sleep 2 88 | Type "# `leetgo contest` to watch and generate contest questions" Enter 89 | Sleep 2 90 | Type "# `leetgo fix` use ChatGPT to fix your code" Enter 91 | Sleep 5 92 | -------------------------------------------------------------------------------- /scripts/update_readme.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "slices" 8 | "strings" 9 | 10 | "github.com/fatih/color" 11 | "github.com/jedib0t/go-pretty/v6/table" 12 | 13 | "github.com/j178/leetgo/cmd" 14 | "github.com/j178/leetgo/config" 15 | "github.com/j178/leetgo/lang" 16 | "github.com/j178/leetgo/utils" 17 | ) 18 | 19 | func main() { 20 | for _, f := range []string{"README_zh.md", "README_en.md", "README.md"} { 21 | if !utils.IsExist(f) { 22 | continue 23 | } 24 | readme, _ := os.ReadFile(f) 25 | readme = updateUsage(readme) 26 | readme = updateConfig(readme) 27 | readme = updateSupportMatrix(readme) 28 | _ = os.WriteFile(f, readme, 0o644) 29 | } 30 | } 31 | 32 | func replace(mark string, origin []byte, new []byte) []byte { 33 | beginMark := fmt.Appendf(nil, "", mark) 34 | endMark := fmt.Appendf(nil, "", mark) 35 | begin := bytes.Index(origin, beginMark) + len(beginMark) 36 | end := bytes.Index(origin, endMark) 37 | return slices.Replace(origin, begin, end, new...) 38 | } 39 | 40 | func updateUsage(readme []byte) []byte { 41 | color.NoColor = true 42 | usage := cmd.UsageString() 43 | usage = "\n```\n" + usage + "```\n" 44 | 45 | return replace("USAGE", readme, []byte(usage)) 46 | } 47 | 48 | func updateConfig(readme []byte) []byte { 49 | buf := new(strings.Builder) 50 | _ = config.Get().Write(buf, true) 51 | configStr := buf.String() 52 | configStr = "\n```yaml\n" + configStr + "```\n" 53 | 54 | return replace("CONFIG", readme, []byte(configStr)) 55 | } 56 | 57 | func updateSupportMatrix(readme []byte) []byte { 58 | w := table.NewWriter() 59 | w.AppendHeader(table.Row{"", "Generation", "Local testing"}) 60 | for _, l := range lang.SupportedLangs { 61 | if l.Name() == "Pandas" { 62 | continue 63 | } 64 | localTest := ":white_check_mark:" 65 | if _, ok := l.(lang.LocalTestable); !ok { 66 | localTest = "Not yet" 67 | } 68 | w.AppendRow( 69 | table.Row{ 70 | l.Name(), 71 | ":white_check_mark:", 72 | localTest, 73 | }, 74 | ) 75 | } 76 | matrixStr := w.RenderMarkdown() 77 | matrixStr = "\n" + matrixStr + "\n" 78 | 79 | return replace("MATRIX", readme, []byte(matrixStr)) 80 | } 81 | -------------------------------------------------------------------------------- /leetcode/contest.go: -------------------------------------------------------------------------------- 1 | package leetcode 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | type Contest struct { 9 | client Client 10 | Id int 11 | TitleSlug string 12 | Title string 13 | StartTime int64 14 | OriginStartTime int64 15 | Duration int 16 | Description string 17 | Questions []*QuestionData 18 | Registered bool 19 | ContainsPremium bool 20 | IsVirtual bool 21 | } 22 | 23 | func (ct *Contest) HasStarted() bool { 24 | return time.Unix(ct.StartTime, 0).Before(time.Now()) 25 | } 26 | 27 | func (ct *Contest) HasFinished() bool { 28 | return time.Unix(ct.StartTime, 0).Add(time.Duration(ct.Duration) * time.Second).Before(time.Now()) 29 | } 30 | 31 | func (ct *Contest) TimeTillStart() time.Duration { 32 | return time.Until(time.Unix(ct.StartTime, 0)) 33 | } 34 | 35 | func (ct *Contest) checkAccessQuestions() error { 36 | if !ct.HasStarted() { 37 | return ErrContestNotStarted 38 | } 39 | if len(ct.Questions) > 0 { 40 | return nil 41 | } 42 | err := ct.Refresh() 43 | if err != nil { 44 | return err 45 | } 46 | if len(ct.Questions) == 0 { 47 | return errors.New("no questions in contest") 48 | } 49 | return nil 50 | } 51 | 52 | func (ct *Contest) GetQuestionNumber(slug string) (int, error) { 53 | err := ct.checkAccessQuestions() 54 | if err != nil { 55 | return 0, err 56 | } 57 | for i, q2 := range ct.Questions { 58 | if q2.TitleSlug == slug { 59 | return i + 1, nil 60 | } 61 | } 62 | return 0, ErrQuestionNotFound 63 | } 64 | 65 | func (ct *Contest) GetQuestionByNumber(num int) (*QuestionData, error) { 66 | err := ct.checkAccessQuestions() 67 | if err != nil { 68 | return nil, err 69 | } 70 | if num < 1 || num > len(ct.Questions) { 71 | return nil, errors.New("invalid question number") 72 | } 73 | 74 | q := ct.Questions[num-1] 75 | return q, err 76 | } 77 | 78 | func (ct *Contest) GetAllQuestions() ([]*QuestionData, error) { 79 | err := ct.checkAccessQuestions() 80 | if err != nil { 81 | return nil, err 82 | } 83 | return ct.Questions, nil 84 | } 85 | 86 | func (ct *Contest) Refresh() error { 87 | contest, err := ct.client.GetContest(ct.TitleSlug) 88 | if err != nil { 89 | return err 90 | } 91 | *ct = *contest 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /cmd/debug.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | 8 | "github.com/goccy/go-json" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/j178/leetgo/config" 12 | "github.com/j178/leetgo/leetcode" 13 | ) 14 | 15 | var inspectCmd = &cobra.Command{ 16 | Use: "inspect", 17 | Short: "Inspect LeetCode API, developer only", 18 | Args: cobra.ExactArgs(1), 19 | Hidden: true, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | c := leetcode.NewClient(leetcode.ReadCredentials()) 22 | resp, err := c.Inspect(args[0]) 23 | if err != nil { 24 | return err 25 | } 26 | var buf strings.Builder 27 | enc := json.NewEncoder(&buf) 28 | enc.SetIndent("", " ") 29 | _ = enc.Encode(resp) 30 | cmd.Print(buf.String()) 31 | return nil 32 | }, 33 | } 34 | 35 | var whoamiCmd = &cobra.Command{ 36 | Use: "whoami", 37 | Short: "Print the current user", 38 | Hidden: true, 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | c := leetcode.NewClient(leetcode.ReadCredentials()) 41 | user, err := c.GetUserStatus() 42 | if err != nil { 43 | return err 44 | } 45 | if !user.IsSignedIn { 46 | return errors.New("user not signed in") 47 | } 48 | cmd.Println(user.Whoami(c)) 49 | return nil 50 | }, 51 | } 52 | 53 | var debugCmd = &cobra.Command{ 54 | Use: "debug", 55 | Short: "Show debug info", 56 | Run: func(cmd *cobra.Command, args []string) { 57 | cfg := config.Get() 58 | cwd, _ := os.Getwd() 59 | projectConfig, err := os.ReadFile(cfg.ConfigFile()) 60 | if err != nil { 61 | projectConfig = []byte("No project config file found") 62 | } 63 | cmd.Println("Leetgo version info :") 64 | cmd.Println("```") 65 | cmd.Println(buildVersion()) 66 | cmd.Println("```") 67 | cmd.Println("Home dir :", cfg.HomeDir()) 68 | cmd.Println("Project root :", cfg.ProjectRoot()) 69 | cmd.Println("Working dir :", cwd) 70 | cmd.Println("Project config file :", cfg.ConfigFile()) 71 | cmd.Println("Project configuration:") 72 | cmd.Println("```yaml") 73 | cmd.Println(string(projectConfig)) 74 | cmd.Println("```") 75 | cmd.Println("Full configuration :") 76 | cmd.Println("```yaml") 77 | _ = cfg.Write(cmd.OutOrStdout(), false) 78 | cmd.Println("```") 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /testutils/go/prefdefined_test.go: -------------------------------------------------------------------------------- 1 | package goutils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_NaryTreeNodeToString(t *testing.T) { 10 | type testcase struct { 11 | tree *NaryTreeNode 12 | expected string 13 | } 14 | tests := []testcase{ 15 | { 16 | tree: &NaryTreeNode{Val: 1}, 17 | expected: "[1]", 18 | }, 19 | { 20 | tree: &NaryTreeNode{ 21 | Val: 1, 22 | Children: []*NaryTreeNode{ 23 | { 24 | Val: 3, 25 | Children: []*NaryTreeNode{ 26 | {Val: 5}, 27 | {Val: 6}, 28 | }, 29 | }, 30 | {Val: 2}, 31 | {Val: 4}, 32 | }, 33 | }, 34 | expected: "[1,null,3,2,4,null,5,6]", 35 | }, 36 | { 37 | tree: &NaryTreeNode{ 38 | Val: 1, 39 | Children: []*NaryTreeNode{ 40 | {Val: 2}, 41 | { 42 | Val: 3, 43 | Children: []*NaryTreeNode{ 44 | {Val: 6}, 45 | { 46 | Val: 7, 47 | Children: []*NaryTreeNode{ 48 | { 49 | Val: 11, 50 | Children: []*NaryTreeNode{{Val: 14}}, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, 56 | { 57 | Val: 4, 58 | Children: []*NaryTreeNode{ 59 | {Val: 8, Children: []*NaryTreeNode{{Val: 12}}}, 60 | }, 61 | }, 62 | { 63 | Val: 5, 64 | Children: []*NaryTreeNode{ 65 | { 66 | Val: 9, 67 | Children: []*NaryTreeNode{{Val: 13}}, 68 | }, 69 | {Val: 10}, 70 | }, 71 | }, 72 | }, 73 | }, 74 | expected: "[1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]", 75 | }, 76 | } 77 | for _, test := range tests { 78 | t.Run( 79 | "", func(t *testing.T) { 80 | assert.Equal(t, test.expected, test.tree.String()) 81 | }, 82 | ) 83 | } 84 | } 85 | 86 | func Test_DeserializeNaryTree(t *testing.T) { 87 | testcases := []string{ 88 | "[1]", 89 | "[1,null,3,2,4,null,5,6]", 90 | "[1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]", 91 | } 92 | for _, test := range testcases { 93 | t.Run( 94 | "", func(t *testing.T) { 95 | tree, err := DeserializeNaryTreeNode(test) 96 | if assert.NoError(t, err) { 97 | assert.Equal(t, test, tree.String()) 98 | } 99 | }, 100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // IsExist checks if a file or directory exists 10 | func IsExist(path string) bool { 11 | _, err := os.Stat(path) 12 | if err == nil { 13 | return true 14 | } 15 | if errors.Is(err, os.ErrNotExist) { 16 | return false 17 | } 18 | return false 19 | } 20 | 21 | func MakeDir(dir string) error { 22 | return os.MkdirAll(dir, 0o755) 23 | } 24 | 25 | // CreateIfNotExists creates a file or a directory only if it does not already exist. 26 | func CreateIfNotExists(path string, isDir bool) error { 27 | if _, err := os.Stat(path); err != nil { 28 | if os.IsNotExist(err) { 29 | if isDir { 30 | return os.MkdirAll(path, 0o755) 31 | } 32 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { 33 | return err 34 | } 35 | f, err := os.OpenFile(path, os.O_CREATE, 0o755) 36 | if err != nil { 37 | return err 38 | } 39 | f.Close() 40 | } else { 41 | return err 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | func WriteFile(file string, content []byte) error { 48 | err := CreateIfNotExists(file, false) 49 | if err != nil { 50 | return err 51 | } 52 | err = os.WriteFile(file, content, 0o644) 53 | if err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | 59 | func WriteOrAppendFile(file string, content []byte) error { 60 | _, err := os.Stat(file) 61 | if err != nil { 62 | if os.IsNotExist(err) { 63 | return WriteFile(file, content) 64 | } 65 | return err 66 | } 67 | f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY, 0o644) 68 | if err != nil { 69 | return err 70 | } 71 | defer f.Close() 72 | _, err = f.Write(content) 73 | return err 74 | } 75 | 76 | func RemoveIfExist(path string) error { 77 | err := os.Remove(path) 78 | if os.IsNotExist(err) { 79 | return nil 80 | } 81 | return err 82 | } 83 | 84 | func RemoveDirIfExist(path string) error { 85 | err := os.RemoveAll(path) 86 | if os.IsNotExist(err) { 87 | return nil 88 | } 89 | return err 90 | } 91 | 92 | func Truncate(filename string) error { 93 | f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC, 0o755) 94 | if err != nil { 95 | return err 96 | } 97 | if err = f.Close(); err != nil { 98 | return err 99 | } 100 | return nil 101 | } 102 | 103 | func RelToCwd(path string) string { 104 | wd, err := os.Getwd() 105 | if err != nil { 106 | return path 107 | } 108 | relPath, err := filepath.Rel(wd, path) 109 | if err != nil { 110 | relPath = path 111 | } 112 | return filepath.ToSlash(relPath) 113 | } 114 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Most of this script is taken from https://github.com/mitsuhiko/rye/blob/main/scripts/install.sh 5 | 6 | # Wrap everything in a function so that a truncated script 7 | # does not have the chance to cause issues. 8 | __wrap__() { 9 | 10 | # allow overriding the version 11 | VERSION=${LEETGO_VERSION:-latest} 12 | PREFIX=${LEETGO_PREFIX:-${HOME}/.local} 13 | 14 | REPO=j178/leetgo 15 | PLATFORM=`uname -s` 16 | ARCH=`uname -m` 17 | 18 | if [[ $PLATFORM == "Darwin" ]]; then 19 | PLATFORM="macOS" 20 | elif [[ $PLATFORM == "Linux" ]]; then 21 | PLATFORM="linux" 22 | fi 23 | 24 | if [[ $ARCH == armv8* ]] || [[ $ARCH == arm64* ]] || [[ $ARCH == aarch64* ]]; then 25 | ARCH="arm64" 26 | elif [[ $ARCH == i686* ]]; then 27 | ARCH="x86_64" 28 | fi 29 | 30 | BINARY="leetgo_${PLATFORM}_${ARCH}" 31 | 32 | # Oddly enough GitHub has different URLs for latest vs specific version 33 | if [[ $VERSION == "latest" ]]; then 34 | DOWNLOAD_URL=https://github.com/${REPO}/releases/latest/download/${BINARY}.tar.gz 35 | else 36 | DOWNLOAD_URL=https://github.com/${REPO}/releases/download/${VERSION}/${BINARY}.tar.gz 37 | fi 38 | 39 | echo "This script will automatically download and install leetgo (${VERSION}) for you." 40 | echo "leetgo will be installed to \${PREFIX}/bin/leetgo, which is ${PREFIX}/bin/leetgo" 41 | echo "You may install leetgo in a different location by setting the PREFIX environment variable." 42 | 43 | if [ "x$(id -u)" == "x0" ]; then 44 | echo "warning: this script is running as root. This is dangerous and unnecessary!" 45 | fi 46 | 47 | if ! hash curl 2> /dev/null; then 48 | echo "error: you do not have 'curl' installed which is required for this script." 49 | exit 1 50 | fi 51 | 52 | if ! hash tar 2> /dev/null; then 53 | echo "error: you do not have 'tar' installed which is required for this script." 54 | exit 1 55 | fi 56 | 57 | TEMP_INSTALL_DIR=`mktemp "${TMPDIR:-/tmp/}.leetgoinstall.XXXXXXXX"` 58 | TEMP_FILE_GZ="${TEMP_INSTALL_DIR}.tar.gz" 59 | 60 | rm -rf "$TEMP_INSTALL_DIR" 61 | mkdir -p "$TEMP_INSTALL_DIR" 62 | 63 | cleanup() { 64 | rm -rf "$TEMP_INSTALL_DIR" 65 | rm -f "$TEMP_FILE_GZ" 66 | } 67 | 68 | trap cleanup EXIT 69 | echo "Downloading $DOWNLOAD_URL" 70 | HTTP_CODE=$(curl -SL --progress-bar "$DOWNLOAD_URL" --output "$TEMP_FILE_GZ" --write-out "%{http_code}") 71 | if [[ ${HTTP_CODE} -lt 200 || ${HTTP_CODE} -gt 299 ]]; then 72 | echo "error: platform ${PLATFORM} (${ARCH}) is unsupported." 73 | exit 1 74 | fi 75 | 76 | tar -xzf "$TEMP_FILE_GZ" -C "$TEMP_INSTALL_DIR" 77 | 78 | DEST="$PREFIX/bin/leetgo" 79 | 80 | chmod +x "$TEMP_INSTALL_DIR/leetgo" 81 | mv "$TEMP_INSTALL_DIR/leetgo" "$DEST" 82 | 83 | echo "leetgo was installed successfully to $DEST" 84 | 85 | }; __wrap__ 86 | -------------------------------------------------------------------------------- /cmd/git.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/AlecAivazis/survey/v2" 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/j178/leetgo/lang" 14 | "github.com/j178/leetgo/leetcode" 15 | ) 16 | 17 | var gitCmd = &cobra.Command{ 18 | Use: "git", 19 | Hidden: true, 20 | Short: "Git related commands", 21 | } 22 | 23 | var gitPushCmd = &cobra.Command{ 24 | Use: "push qid", 25 | Short: "Add, commit and push your solution to remote repository", 26 | Args: cobra.ExactArgs(1), 27 | ValidArgs: []string{"today", "last"}, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | c := leetcode.NewClient(leetcode.ReadCredentials()) 30 | qid := args[0] 31 | qs, err := leetcode.ParseQID(qid, c) 32 | if err != nil { 33 | return err 34 | } 35 | if len(qs) > 1 { 36 | return fmt.Errorf("multiple questions found") 37 | } 38 | result, err := lang.GeneratePathsOnly(qs[0]) 39 | if err != nil { 40 | return err 41 | } 42 | err = gitAddCommitPush(result) 43 | return err 44 | }, 45 | } 46 | 47 | func init() { 48 | gitCmd.AddCommand(gitPushCmd) 49 | } 50 | 51 | func gitAddCommitPush(genResult *lang.GenerateResult) error { 52 | files := make([]string, 0, len(genResult.Files)) 53 | for _, f := range genResult.Files { 54 | files = append(files, f.GetPath()) 55 | } 56 | err := runCmd("git", "add", files...) 57 | if err != nil { 58 | return fmt.Errorf("git add: %w", err) 59 | } 60 | var msg string 61 | prompt := &survey.Input{ 62 | Message: "Commit message", 63 | Default: fmt.Sprintf( 64 | "Add solution for %s.", 65 | genResult.Question.TitleSlug, 66 | ), 67 | } 68 | err = survey.AskOne(prompt, &msg) 69 | if err != nil { 70 | return fmt.Errorf("git commit message: %w", err) 71 | } 72 | 73 | msg = stripComments(msg) 74 | msg = strings.TrimSpace(msg) 75 | if msg == "" { 76 | return errors.New("git: empty commit message") 77 | } 78 | err = runCmd("git", "commit", "-m", msg) 79 | if err != nil { 80 | return fmt.Errorf("git commit: %w", err) 81 | } 82 | err = runCmd("git", "push") 83 | if err != nil { 84 | return fmt.Errorf("git push: %w", err) 85 | } 86 | return nil 87 | } 88 | 89 | func stripComments(s string) string { 90 | lines := strings.Split(s, "\n") 91 | result := make([]string, 0, len(lines)) 92 | for _, line := range lines { 93 | if !strings.HasPrefix(line, "#") { 94 | result = append(result, line) 95 | } 96 | } 97 | return strings.Join(result, "\n") 98 | } 99 | 100 | func runCmd(command string, subcommand string, args ...string) error { 101 | cmd := exec.Command(command, subcommand) 102 | cmd.Args = append(cmd.Args, args...) 103 | cmd.Stdin = os.Stdin 104 | cmd.Stdout = os.Stdout 105 | cmd.Stderr = os.Stderr 106 | return cmd.Run() 107 | } 108 | -------------------------------------------------------------------------------- /leetcode/cache_json.go: -------------------------------------------------------------------------------- 1 | //go:build !sqlite 2 | 3 | package leetcode 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "github.com/charmbracelet/log" 12 | "github.com/goccy/go-json" 13 | 14 | "github.com/j178/leetgo/utils" 15 | ) 16 | 17 | var cacheExt = ".json" 18 | 19 | type jsonCache struct { 20 | path string 21 | client Client 22 | once sync.Once 23 | slugs map[string]*QuestionData 24 | frontIds map[string]*QuestionData 25 | } 26 | 27 | func newCache(path string, c Client) QuestionsCache { 28 | return &jsonCache{path: path, client: c} 29 | } 30 | 31 | func (c *jsonCache) CacheFile() string { 32 | return c.path 33 | } 34 | 35 | func (c *jsonCache) doLoad() error { 36 | c.slugs = make(map[string]*QuestionData) 37 | c.frontIds = make(map[string]*QuestionData) 38 | 39 | if _, err := os.Stat(c.path); errors.Is(err, os.ErrNotExist) { 40 | return err 41 | } 42 | s, err := os.ReadFile(c.path) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | var records []*QuestionData 48 | err = json.Unmarshal(s, &records) 49 | if err != nil { 50 | return err 51 | } 52 | for _, r := range records { 53 | r.partial = 1 54 | r.client = c.client 55 | c.slugs[r.TitleSlug] = r 56 | c.frontIds[r.QuestionFrontendId] = r 57 | } 58 | return nil 59 | } 60 | 61 | func (c *jsonCache) load() { 62 | c.once.Do( 63 | func() { 64 | defer func(start time.Time) { 65 | log.Debug("cache loaded", "path", c.path, "elapsed", time.Since(start)) 66 | }(time.Now()) 67 | err := c.doLoad() 68 | if err != nil { 69 | log.Error("failed to load cache, try updating with `leetgo cache update`", "err", err) 70 | return 71 | } 72 | if len(c.slugs) == 0 { 73 | log.Warn("cache is empty, try updating with `leetgo cache update`") 74 | return 75 | } 76 | if c.Outdated() { 77 | log.Warn("cache is too old, try updating with `leetgo cache update`") 78 | } 79 | }, 80 | ) 81 | } 82 | 83 | func (c *jsonCache) Outdated() bool { 84 | stat, err := os.Stat(c.path) 85 | if os.IsNotExist(err) { 86 | return true 87 | } 88 | return time.Since(stat.ModTime()) >= 14*24*time.Hour 89 | } 90 | 91 | func (c *jsonCache) Update() error { 92 | err := utils.CreateIfNotExists(c.path, false) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | all, err := c.client.GetAllQuestions() 98 | if err != nil { 99 | return err 100 | } 101 | f, err := os.Create(c.path) 102 | if err != nil { 103 | return err 104 | } 105 | defer func() { _ = f.Close() }() 106 | enc := json.NewEncoder(f) 107 | err = enc.Encode(all) 108 | if err != nil { 109 | return err 110 | } 111 | log.Info("questions cache updated", "count", len(all), "path", c.path) 112 | return nil 113 | } 114 | 115 | func (c *jsonCache) GetBySlug(slug string) *QuestionData { 116 | c.load() 117 | return c.slugs[slug] 118 | } 119 | 120 | func (c *jsonCache) GetById(id string) *QuestionData { 121 | defer func(start time.Time) { 122 | log.Debug("get by id", "elapsed", time.Since(start)) 123 | }(time.Now()) 124 | 125 | c.load() 126 | return c.frontIds[id] 127 | } 128 | 129 | func (c *jsonCache) GetAllQuestions() []*QuestionData { 130 | c.load() 131 | all := make([]*QuestionData, 0, len(c.slugs)) 132 | for _, q := range c.slugs { 133 | all = append(all, q) 134 | } 135 | return all 136 | } 137 | -------------------------------------------------------------------------------- /testutils/rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.100" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 10 | 11 | [[package]] 12 | name = "itoa" 13 | version = "1.0.6" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 16 | 17 | [[package]] 18 | name = "leetgo-rs" 19 | version = "0.2.1" 20 | dependencies = [ 21 | "anyhow", 22 | "serde", 23 | "serde_json", 24 | ] 25 | 26 | [[package]] 27 | name = "memchr" 28 | version = "2.7.4" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 31 | 32 | [[package]] 33 | name = "proc-macro2" 34 | version = "1.0.93" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 37 | dependencies = [ 38 | "unicode-ident", 39 | ] 40 | 41 | [[package]] 42 | name = "quote" 43 | version = "1.0.38" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 46 | dependencies = [ 47 | "proc-macro2", 48 | ] 49 | 50 | [[package]] 51 | name = "ryu" 52 | version = "1.0.13" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 55 | 56 | [[package]] 57 | name = "serde" 58 | version = "1.0.228" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 61 | dependencies = [ 62 | "serde_core", 63 | ] 64 | 65 | [[package]] 66 | name = "serde_core" 67 | version = "1.0.228" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 70 | dependencies = [ 71 | "serde_derive", 72 | ] 73 | 74 | [[package]] 75 | name = "serde_derive" 76 | version = "1.0.228" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 79 | dependencies = [ 80 | "proc-macro2", 81 | "quote", 82 | "syn", 83 | ] 84 | 85 | [[package]] 86 | name = "serde_json" 87 | version = "1.0.145" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 90 | dependencies = [ 91 | "itoa", 92 | "memchr", 93 | "ryu", 94 | "serde", 95 | "serde_core", 96 | ] 97 | 98 | [[package]] 99 | name = "syn" 100 | version = "2.0.98" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 103 | dependencies = [ 104 | "proc-macro2", 105 | "quote", 106 | "unicode-ident", 107 | ] 108 | 109 | [[package]] 110 | name = "unicode-ident" 111 | version = "1.0.16" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 114 | -------------------------------------------------------------------------------- /cmd/tui.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/charmbracelet/bubbles/list" 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/charmbracelet/lipgloss" 10 | 11 | "github.com/j178/leetgo/leetcode" 12 | ) 13 | 14 | var ( 15 | titleStyle = lipgloss.NewStyle().MarginLeft(2) 16 | itemStyle = lipgloss.NewStyle().PaddingLeft(4) 17 | selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) 18 | paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) 19 | helpStyle = lipgloss.NewStyle().PaddingLeft(4).PaddingBottom(1) 20 | // textStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) 21 | ) 22 | 23 | type rowDelegate struct{} 24 | 25 | func (d rowDelegate) Height() int { 26 | return 1 27 | } 28 | 29 | func (d rowDelegate) Spacing() int { 30 | return 0 31 | } 32 | 33 | func (d rowDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { 34 | return nil 35 | } 36 | 37 | func (d rowDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { 38 | i, ok := listItem.(*item) 39 | if !ok { 40 | return 41 | } 42 | q := (*leetcode.QuestionData)(i) 43 | 44 | // TODO improve display 45 | str := q.GetTitle() 46 | if index == m.Index() { 47 | str = selectedItemStyle.Render("> " + str) 48 | } else { 49 | str = itemStyle.Render(str) 50 | } 51 | _, _ = fmt.Fprint(w, str) 52 | } 53 | 54 | type qsMsg []*leetcode.QuestionData 55 | 56 | type item leetcode.QuestionData 57 | 58 | func (i *item) FilterValue() string { 59 | return (*leetcode.QuestionData)(i).GetTitle() 60 | } 61 | 62 | type tui struct { 63 | filter leetcode.QuestionFilter 64 | client leetcode.Client 65 | idx int // nolint: unused 66 | total int 67 | hasMore bool 68 | list *list.Model 69 | selected *leetcode.QuestionData 70 | } 71 | 72 | func newTuiModel(filter leetcode.QuestionFilter, c leetcode.Client) *tui { 73 | l := list.New(nil, rowDelegate{}, 60, 60) 74 | l.Title = "Select a question" 75 | l.SetShowStatusBar(true) 76 | l.SetShowTitle(true) 77 | l.Styles.Title = titleStyle 78 | l.Styles.PaginationStyle = paginationStyle 79 | l.Styles.HelpStyle = helpStyle 80 | 81 | // TODO Implement a progressive loading list 82 | return &tui{ 83 | filter: filter, 84 | client: c, 85 | list: &l, 86 | } 87 | } 88 | 89 | func (m *tui) Selected() *leetcode.QuestionData { 90 | return m.selected 91 | } 92 | 93 | func (m *tui) Init() tea.Cmd { 94 | return func() tea.Msg { 95 | qs, err := m.client.GetQuestionsByFilter(m.filter, 100, 0) 96 | if err != nil { 97 | return nil 98 | } 99 | m.total = qs.Total 100 | m.hasMore = qs.HasMore 101 | return qsMsg(qs.Questions) 102 | } 103 | } 104 | 105 | func (m *tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 106 | switch msg := msg.(type) { 107 | case tea.KeyMsg: 108 | switch msg.String() { 109 | case "enter": 110 | if m.list.SelectedItem() != nil { 111 | m.selected = (*leetcode.QuestionData)(m.list.SelectedItem().(*item)) 112 | return m, tea.Quit 113 | } 114 | } 115 | case tea.WindowSizeMsg: 116 | if m.list == nil { 117 | return m, nil 118 | } 119 | m.list.SetSize(msg.Width, msg.Height) 120 | return m, nil 121 | case qsMsg: 122 | items := make([]list.Item, len(msg)) 123 | for i, q := range msg { 124 | items[i] = (*item)(q) 125 | } 126 | m.list.SetItems(items) 127 | return m, nil 128 | } 129 | lst, cmd := m.list.Update(msg) 130 | m.list = &lst 131 | return m, cmd 132 | } 133 | 134 | func (m *tui) View() string { 135 | return "\n" + m.list.View() 136 | } 137 | -------------------------------------------------------------------------------- /testutils/rust/src/list.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 2 | use serde::de::Visitor; 3 | use serde::ser::SerializeSeq; 4 | 5 | type ListLink = Option>; 6 | 7 | #[derive(Debug, PartialEq, Eq, Clone)] 8 | pub struct ListNode { 9 | pub val: i32, 10 | pub next: ListLink, 11 | } 12 | 13 | impl ListNode { 14 | #[inline] 15 | fn new(val: i32) -> Self { 16 | ListNode { 17 | next: None, 18 | val 19 | } 20 | } 21 | } 22 | 23 | #[macro_export] 24 | macro_rules! list { 25 | () => { 26 | None 27 | }; 28 | ($e:expr) => { 29 | Some(Box::new(ListNode::new($e))) 30 | }; 31 | ($e:expr, $($tail:tt)*) => { 32 | Some(Box::new(ListNode { 33 | val: $e, 34 | next: list!($($tail)*), 35 | })) 36 | }; 37 | } 38 | 39 | #[derive(Debug, PartialEq, Eq, Clone)] 40 | pub struct LinkedList(ListLink); 41 | 42 | impl From for ListLink { 43 | fn from(list: LinkedList) -> Self { 44 | list.0 45 | } 46 | } 47 | 48 | impl From for LinkedList { 49 | fn from(link: Option>) -> Self { 50 | LinkedList(link) 51 | } 52 | } 53 | 54 | impl Serialize for LinkedList { 55 | fn serialize(&self, serializer: S) -> Result 56 | where 57 | S: Serializer, 58 | { 59 | let mut seq = serializer.serialize_seq(None)?; 60 | let mut current = &self.0; 61 | while let Some(ref node) = current { 62 | seq.serialize_element(&node.val)?; 63 | current = &node.next; 64 | } 65 | seq.end() 66 | } 67 | } 68 | 69 | struct LinkedListVisitor; 70 | 71 | impl<'de> Visitor<'de> for LinkedListVisitor { 72 | type Value = LinkedList; 73 | 74 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 75 | formatter.write_str("a list of integers") 76 | } 77 | 78 | fn visit_seq(self, mut seq: A) -> Result 79 | where 80 | A: serde::de::SeqAccess<'de>, 81 | { 82 | let mut head = None; 83 | let mut current = &mut head; 84 | while let Some(val) = seq.next_element()? { 85 | let node = ListNode { val, next: None }; 86 | *current = Some(Box::new(node)); 87 | current = &mut current.as_mut().unwrap().next; 88 | } 89 | Ok(LinkedList(head)) 90 | } 91 | } 92 | 93 | impl<'de> Deserialize<'de> for LinkedList { 94 | fn deserialize(deserializer: D) -> Result 95 | where 96 | D: Deserializer<'de>, 97 | { 98 | deserializer.deserialize_seq(LinkedListVisitor) 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | #[test] 107 | fn test_list_serialize() { 108 | let list = LinkedList(list!(1, 2, 3)); 109 | let serialized = serde_json::to_string(&list).unwrap(); 110 | assert_eq!(serialized, "[1,2,3]"); 111 | } 112 | 113 | #[test] 114 | fn test_list_deserialize() { 115 | let serialized = "[1,2,3]"; 116 | let list: LinkedList = serde_json::from_str(serialized).unwrap(); 117 | assert_eq!(list, LinkedList(list![1, 2, 3])); 118 | 119 | let serialized = "[]"; 120 | let list: LinkedList = serde_json::from_str(serialized).unwrap(); 121 | assert!(list.0.is_none()); 122 | 123 | let serialized = "[true]"; 124 | let list = serde_json::from_str::(serialized); 125 | assert!(list.is_err()); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /leetcode/decoder.go: -------------------------------------------------------------------------------- 1 | package leetcode 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | "net/http" 7 | "reflect" 8 | "strings" 9 | 10 | "github.com/charmbracelet/log" 11 | "github.com/dghubble/sling" 12 | "github.com/goccy/go-json" 13 | "github.com/jedib0t/go-pretty/v6/progress" 14 | "github.com/tidwall/gjson" 15 | 16 | "github.com/j178/leetgo/utils" 17 | ) 18 | 19 | var ( 20 | gjsonType = reflect.TypeFor[gjson.Result]() 21 | bytesType = reflect.TypeFor[[]byte]() 22 | stringType = reflect.TypeFor[string]() 23 | errorType = reflect.TypeFor[UnexpectedStatusCode]() 24 | ) 25 | 26 | type smartDecoder struct { 27 | Debug bool 28 | LogResponse bool 29 | LogLimit int 30 | path string 31 | } 32 | 33 | func headerString(h http.Header) string { 34 | w := &strings.Builder{} 35 | _ = h.WriteSubset( 36 | w, map[string]bool{ 37 | "Content-Security-Policy": true, 38 | "Set-Cookie": true, 39 | "X-Frame-Options": true, 40 | "Vary": true, 41 | "Strict-Transport-Security": true, 42 | "Date": true, 43 | "Access-Control-Allow-Credentials": true, 44 | "Access-Control-Allow-Origin": true, 45 | }, 46 | ) 47 | return w.String() 48 | } 49 | 50 | func (d smartDecoder) Decode(resp *http.Response, v interface{}) error { 51 | if strings.EqualFold(resp.Header.Get("Content-Encoding"), "gzip") && resp.ContentLength != 0 { 52 | if _, ok := resp.Body.(*gzip.Reader); !ok { 53 | var err error 54 | resp.Body, err = gzip.NewReader(resp.Body) 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | } 60 | 61 | data, _ := io.ReadAll(resp.Body) 62 | 63 | if d.Debug { 64 | dataStr := "" 65 | if d.LogResponse { 66 | dataStr = utils.BytesToString(data) 67 | limit := d.LogLimit 68 | if len(data) > limit { 69 | dataStr = dataStr[:limit] + "..." 70 | } 71 | } 72 | log.Debug( 73 | "response", 74 | "url", resp.Request.URL.String(), 75 | "code", resp.StatusCode, 76 | "headers", headerString(resp.Header), 77 | "data", dataStr, 78 | ) 79 | } 80 | 81 | ty := reflect.TypeOf(v) 82 | ele := reflect.ValueOf(v).Elem() 83 | switch ty.Elem() { 84 | case gjsonType: 85 | if d.path == "" { 86 | ele.Set(reflect.ValueOf(gjson.ParseBytes(data))) 87 | } else { 88 | ele.Set(reflect.ValueOf(gjson.GetBytes(data, d.path))) 89 | } 90 | case bytesType: 91 | ele.SetBytes(data) 92 | case stringType: 93 | ele.SetString(utils.BytesToString(data)) 94 | case errorType: 95 | ele.Set(reflect.ValueOf(NewUnexpectedStatusCode(resp.StatusCode, data))) 96 | default: 97 | return json.Unmarshal(data, v) 98 | } 99 | return nil 100 | } 101 | 102 | // It's proxy reader, implement io.Reader 103 | type reader struct { 104 | io.Reader 105 | tracker *progress.Tracker 106 | } 107 | 108 | func (r *reader) Read(p []byte) (n int, err error) { 109 | n, err = r.Reader.Read(p) 110 | r.tracker.Increment(int64(n)) 111 | return n, err 112 | } 113 | 114 | // Close the reader when it implements io.Closer 115 | func (r *reader) Close() (err error) { 116 | r.tracker.MarkAsDone() 117 | if closer, ok := r.Reader.(io.Closer); ok { 118 | return closer.Close() 119 | } 120 | return err 121 | } 122 | 123 | type progressDecoder struct { 124 | sling.ResponseDecoder 125 | tracker *progress.Tracker 126 | } 127 | 128 | func (d progressDecoder) Decode(resp *http.Response, v interface{}) error { 129 | total := resp.ContentLength 130 | d.tracker.UpdateTotal(total) 131 | resp.Body = &reader{resp.Body, d.tracker} 132 | return d.ResponseDecoder.Decode(resp, v) 133 | } 134 | -------------------------------------------------------------------------------- /testutils/go/parse_test.go: -------------------------------------------------------------------------------- 1 | package goutils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDeserialize(t *testing.T) { 11 | assert.NotPanics( 12 | t, func() { 13 | v1 := Deserialize[int]("123") 14 | assert.Equal(t, 123, v1) 15 | }, 16 | ) 17 | assert.NotPanics( 18 | t, func() { 19 | v2 := Deserialize[string](`"abc"`) 20 | assert.Equal(t, "abc", v2) 21 | }, 22 | ) 23 | assert.NotPanics( 24 | t, func() { 25 | v3 := Deserialize[byte](`'a'`) 26 | assert.Equal(t, byte('a'), v3) 27 | }, 28 | ) 29 | assert.NotPanics( 30 | t, func() { 31 | v4 := Deserialize[[]int]("[]") 32 | assert.Equal(t, []int{}, v4) 33 | }, 34 | ) 35 | assert.NotPanics( 36 | t, func() { 37 | v5 := Deserialize[[]int]("[1,2,3]") 38 | assert.Equal(t, []int{1, 2, 3}, v5) 39 | }, 40 | ) 41 | assert.NotPanics( 42 | t, func() { 43 | v6 := Deserialize[[]string](`["a","b","c"]`) 44 | assert.Equal(t, []string{"a", "b", "c"}, v6) 45 | }, 46 | ) 47 | assert.NotPanics( 48 | t, func() { 49 | v7 := Deserialize[*TreeNode]("[1,2,3]") 50 | assert.Equal(t, 1, v7.Val) 51 | assert.Equal(t, 2, v7.Left.Val) 52 | assert.Equal(t, 3, v7.Right.Val) 53 | }, 54 | ) 55 | assert.NotPanics( 56 | t, func() { 57 | v8 := Deserialize[*ListNode]("[1,2,3]") 58 | assert.Equal(t, 1, v8.Val) 59 | assert.Equal(t, 2, v8.Next.Val) 60 | assert.Equal(t, 3, v8.Next.Next.Val) 61 | }, 62 | ) 63 | assert.NotPanics( 64 | t, func() { 65 | v9 := Deserialize[float64]("1.2") 66 | assert.Equal(t, 1.2, v9) 67 | }, 68 | ) 69 | assert.NotPanics( 70 | t, func() { 71 | v10 := Deserialize[bool]("true") 72 | assert.Equal(t, true, v10) 73 | }, 74 | ) 75 | assert.NotPanics( 76 | t, func() { 77 | v11 := Deserialize[bool]("false") 78 | assert.Equal(t, false, v11) 79 | }, 80 | ) 81 | assert.NotPanics( 82 | t, func() { 83 | v12 := Deserialize[[][]int]("[[1,2],[3,4]]") 84 | assert.Equal(t, [][]int{{1, 2}, {3, 4}}, v12) 85 | }, 86 | ) 87 | assert.NotPanics( 88 | t, func() { 89 | v13 := Deserialize[[]*TreeNode]("[[1,2,3],[4,5,6]]") 90 | assert.Len(t, v13, 2) 91 | }, 92 | ) 93 | assert.Panics(t, func() { Deserialize[bool]("True") }) 94 | assert.Panics(t, func() { Deserialize[func()]("1") }) 95 | assert.Panics(t, func() { Deserialize[int](`"1.2"`) }) 96 | } 97 | 98 | func sliceEqual(a, b []string) bool { 99 | if len(a) != len(b) { 100 | return false 101 | } 102 | for i := range a { 103 | if a[i] != b[i] { 104 | return false 105 | } 106 | } 107 | return true 108 | } 109 | 110 | func TestSplitArray(t *testing.T) { 111 | tests := []struct { 112 | input string 113 | want []string 114 | err error 115 | }{ 116 | {"[]", []string{}, nil}, 117 | {"[1]", []string{"1"}, nil}, 118 | {"[[1], [2]]", []string{"[1]", "[2]"}, nil}, 119 | {"[1,2,3]", []string{"1", "2", "3"}, nil}, 120 | {"[1, 2, 3]", []string{"1", "2", "3"}, nil}, 121 | {" [1,2,3] ", []string{"1", "2", "3"}, nil}, 122 | {`[1, "2, 3"]`, []string{"1", `"2, 3"`}, nil}, 123 | {"[1,2,3,]", nil, fmt.Errorf("invalid array: [1,2,3,]")}, // trailing comma 124 | {"[1,2,3", nil, fmt.Errorf("invalid array: [1,2,3")}, // missing closing bracket 125 | {"1,2,3", nil, fmt.Errorf("invalid array: 1,2,3")}, // no brackets 126 | {`[1,2,"[","[]"]`, []string{"1", "2", `"["`, `"[]"`}, nil}, // string contains brackets 127 | {`[null,1,2,null]`, []string{"null", "1", "2", "null"}, nil}, 128 | } 129 | 130 | for _, tc := range tests { 131 | got, err := SplitArray(tc.input) 132 | 133 | if !sliceEqual(tc.want, got) || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) { 134 | t.Errorf("SplitArray(%q) = (%q, %v), want (%q, %v)", tc.input, got, err, tc.want, tc.err) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /cmd/pick.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/AlecAivazis/survey/v2" 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/j178/leetgo/editor" 12 | "github.com/j178/leetgo/lang" 13 | "github.com/j178/leetgo/leetcode" 14 | ) 15 | 16 | func askFilter(c leetcode.Client) (filter leetcode.QuestionFilter, err error) { 17 | tags, err := c.GetQuestionTags() 18 | if err != nil { 19 | return filter, err 20 | } 21 | tagNames := make([]string, 0, len(tags)) 22 | tagNamesToSlug := make(map[string]string, len(tags)) 23 | for _, t := range tags { 24 | tagNames = append(tagNames, t.Name) 25 | tagNamesToSlug[t.Name] = t.Slug 26 | } 27 | 28 | qs := []*survey.Question{ 29 | { 30 | Name: "Difficulty", 31 | Prompt: &survey.Select{ 32 | Message: "Select a difficulty level", 33 | Options: []string{"All", "Easy", "Medium", "Hard"}, 34 | }, 35 | Transform: func(ans interface{}) (newAns interface{}) { 36 | opt := ans.(survey.OptionAnswer) 37 | if opt.Index == 0 { 38 | return survey.OptionAnswer{Value: ""} 39 | } 40 | opt.Value = strings.ToUpper(opt.Value) 41 | return opt 42 | }, 43 | }, 44 | { 45 | Name: "Status", 46 | Prompt: &survey.Select{ 47 | Message: "Select question status", 48 | Options: []string{"All", "Not Started", "Tried", "Ac"}, 49 | }, 50 | Transform: func(ans interface{}) (newAns interface{}) { 51 | opt := ans.(survey.OptionAnswer) 52 | if opt.Index == 0 { 53 | return survey.OptionAnswer{Value: ""} 54 | } 55 | opt.Value = strings.ReplaceAll(strings.ToUpper(opt.Value), " ", "_") 56 | return opt 57 | }, 58 | }, 59 | { 60 | Name: "Tags", 61 | Prompt: &survey.MultiSelect{ 62 | Message: "Select tags", 63 | Options: tagNames, 64 | }, 65 | Transform: func(ans interface{}) (newAns interface{}) { 66 | opt := ans.([]survey.OptionAnswer) 67 | if len(opt) == 0 { 68 | return opt 69 | } 70 | if len(opt) == len(tagNames) { 71 | return []survey.OptionAnswer{} 72 | } 73 | for i, o := range opt { 74 | opt[i].Value = tagNamesToSlug[o.Value] 75 | } 76 | return opt 77 | }, 78 | }, 79 | } 80 | 81 | err = survey.Ask(qs, &filter, survey.WithRemoveSelectAll()) 82 | if err != nil { 83 | return filter, err 84 | } 85 | 86 | return filter, nil 87 | } 88 | 89 | var skipEditor bool 90 | 91 | func init() { 92 | pickCmd.Flags().BoolVarP(&skipEditor, "skip-editor", "", false, "Skip opening the editor") 93 | } 94 | 95 | var pickCmd = &cobra.Command{ 96 | Use: "pick [qid]", 97 | Short: "Generate a new question", 98 | Example: `leetgo pick # show a list of questions to pick 99 | leetgo pick today 100 | leetgo pick 549 101 | leetgo pick two-sum`, 102 | Args: cobra.MaximumNArgs(1), 103 | Aliases: []string{"p"}, 104 | ValidArgs: []string{"today", "yesterday"}, 105 | RunE: func(cmd *cobra.Command, args []string) error { 106 | c := leetcode.NewClient(leetcode.ReadCredentials()) 107 | var q *leetcode.QuestionData 108 | 109 | if len(args) > 0 { 110 | qid := args[0] 111 | qs, err := leetcode.ParseQID(qid, c) 112 | if err != nil { 113 | return err 114 | } 115 | if len(qs) > 1 { 116 | return fmt.Errorf("`leetgo pick` cannot handle multiple contest questions, use `leetgo contest` instead") 117 | } 118 | q = qs[0] 119 | } else { 120 | filter, err := askFilter(c) 121 | if err != nil { 122 | return err 123 | } 124 | m := newTuiModel(filter, c) 125 | p := tea.NewProgram(m) 126 | if _, err := p.Run(); err != nil { 127 | return err 128 | } 129 | if m.Selected() == nil { 130 | return nil 131 | } 132 | q = m.Selected() 133 | } 134 | 135 | result, err := lang.Generate(q) 136 | if err != nil { 137 | return err 138 | } 139 | if !skipEditor { 140 | err = editor.Open(result) 141 | return err 142 | } 143 | return nil 144 | }, 145 | } 146 | -------------------------------------------------------------------------------- /editor/editor.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "slices" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/charmbracelet/log" 12 | "github.com/google/shlex" 13 | 14 | "github.com/j178/leetgo/config" 15 | "github.com/j178/leetgo/constants" 16 | "github.com/j178/leetgo/lang" 17 | ) 18 | 19 | type Opener interface { 20 | Open(result *lang.GenerateResult) error 21 | } 22 | 23 | const specialAllFiles = "{{.Files}}" 24 | 25 | var knownEditors = map[string]Opener{ 26 | "none": &noneEditor{}, 27 | "vim": &editor{ 28 | command: "vim", 29 | args: []string{"-p", fmt.Sprintf("+/%s", constants.CodeBeginMarker), specialAllFiles}, 30 | }, 31 | "neovim": &editor{ 32 | command: "nvim", 33 | args: []string{"-p", fmt.Sprintf("+/%s", constants.CodeBeginMarker), specialAllFiles}, 34 | }, 35 | "vscode": &editor{command: "code", args: []string{specialAllFiles}}, 36 | } 37 | 38 | type noneEditor struct{} 39 | 40 | func (e *noneEditor) Open(result *lang.GenerateResult) error { 41 | log.Info("none editor is used, skip opening files") 42 | return nil 43 | } 44 | 45 | type editor struct { 46 | command string 47 | args []string 48 | } 49 | 50 | // substituteArgs substitutes the special arguments with the actual values. 51 | func (ed *editor) substituteArgs(result *lang.GenerateResult) ([]string, error) { 52 | getPath := func(fileType lang.FileType) string { 53 | f := result.GetFile(fileType) 54 | if f == nil { 55 | return "" 56 | } 57 | return f.GetPath() 58 | } 59 | 60 | data := struct { 61 | Folder string 62 | Files string 63 | CodeFile string 64 | TestFile string 65 | DescriptionFile string 66 | TestCasesFile string 67 | }{ 68 | Folder: result.TargetDir(), 69 | Files: specialAllFiles, 70 | CodeFile: getPath(lang.CodeFile), 71 | TestFile: getPath(lang.TestFile), 72 | DescriptionFile: getPath(lang.DocFile), 73 | TestCasesFile: getPath(lang.TestCasesFile), 74 | } 75 | 76 | args := slices.Clone(ed.args) 77 | for i, arg := range args { 78 | if !strings.Contains(arg, "{{") { 79 | continue 80 | } 81 | 82 | tmpl := template.New("") 83 | _, err := tmpl.Parse(arg) 84 | if err != nil { 85 | return nil, err 86 | } 87 | var s strings.Builder 88 | err = tmpl.Execute(&s, data) 89 | if err != nil { 90 | return nil, err 91 | } 92 | args[i] = s.String() 93 | } 94 | 95 | // replace the special marker with all files 96 | for i, arg := range args { 97 | if arg == specialAllFiles { 98 | allFiles := make([]string, len(result.Files)) 99 | for j, f := range result.Files { 100 | allFiles[j] = f.GetPath() 101 | } 102 | args = slices.Replace(args, i, i+1, allFiles...) 103 | break 104 | } 105 | } 106 | 107 | return args, nil 108 | } 109 | 110 | func (ed *editor) Open(result *lang.GenerateResult) error { 111 | args, err := ed.substituteArgs(result) 112 | if err != nil { 113 | return fmt.Errorf("invalid editor command: %w", err) 114 | } 115 | return runCmd(ed.command, args, result.OutDir) 116 | } 117 | 118 | // Get returns the editor with the given name. 119 | func Get(ed config.Editor) Opener { 120 | if ed.Use == "custom" { 121 | args, _ := shlex.Split(ed.Args) 122 | return &editor{ 123 | command: ed.Command, 124 | args: args, 125 | } 126 | } 127 | return knownEditors[ed.Use] 128 | } 129 | 130 | // Open opens the files in the given result with the configured editor. 131 | func Open(result *lang.GenerateResult) error { 132 | cfg := config.Get() 133 | ed := Get(cfg.Editor) 134 | if ed == nil { 135 | return fmt.Errorf( 136 | "editor not supported: %s, you can use `editor.command` to customize the command", 137 | cfg.Editor.Use, 138 | ) 139 | } 140 | return ed.Open(result) 141 | } 142 | 143 | func runCmd(command string, args []string, dir string) error { 144 | cmd := exec.Command(command, args...) 145 | if log.GetLevel() <= log.DebugLevel { 146 | log.Info("opening files", "command", cmd.String()) 147 | } else { 148 | log.Info("opening files", "command", cmd.Path) 149 | } 150 | cmd.Dir = dir 151 | cmd.Stdin = os.Stdin 152 | cmd.Stdout = os.Stdout 153 | cmd.Stderr = os.Stderr 154 | return cmd.Run() 155 | } 156 | -------------------------------------------------------------------------------- /testutils/cpp/stdc++.h: -------------------------------------------------------------------------------- 1 | // C++ includes used for precompiling -*- C++ -*- 2 | 3 | // Copyright (C) 2003-2022 Free Software Foundation, Inc. 4 | // 5 | // This file is part of the GNU ISO C++ Library. This library is free 6 | // software; you can redistribute it and/or modify it under the 7 | // terms of the GNU General Public License as published by the 8 | // Free Software Foundation; either version 3, or (at your option) 9 | // any later version. 10 | 11 | // This library is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | 16 | // Under Section 7 of GPL version 3, you are granted additional 17 | // permissions described in the GCC Runtime Library Exception, version 18 | // 3.1, as published by the Free Software Foundation. 19 | 20 | // You should have received a copy of the GNU General Public License and 21 | // a copy of the GCC Runtime Library Exception along with this program; 22 | // see the files COPYING3 and COPYING.RUNTIME respectively. If not, see 23 | // . 24 | 25 | /** @file stdc++.h 26 | * This is an implementation file for a precompiled header. 27 | */ 28 | 29 | // 17.4.1.2 Headers 30 | 31 | // C 32 | #ifndef _GLIBCXX_NO_ASSERT 33 | #include 34 | #endif 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | 53 | #if __cplusplus >= 201103L 54 | #include 55 | #include 56 | #include 57 | // #include 58 | #include 59 | #include 60 | #include 61 | // #include 62 | #endif 63 | 64 | // C++ 65 | #include 66 | #include 67 | #include 68 | #include 69 | #include 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | #include 79 | #include 80 | #include 81 | #include 82 | #include 83 | #include 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 | 98 | #if __cplusplus >= 201103L 99 | #include 100 | #include 101 | #include 102 | #include 103 | #include 104 | #include 105 | #include 106 | #include 107 | #include 108 | #include 109 | #include 110 | #include 111 | #include 112 | #include 113 | #include 114 | #include 115 | #include 116 | #include 117 | #include 118 | #include 119 | #endif 120 | 121 | #if __cplusplus >= 201402L 122 | #include 123 | #endif 124 | 125 | #if __cplusplus >= 201703L 126 | #include 127 | // #include 128 | // #include 129 | #include 130 | #include 131 | // #include 132 | #include 133 | #include 134 | #endif 135 | 136 | #if __cplusplus >= 202002L 137 | #include 138 | #include 139 | #include 140 | #include 141 | #if __cpp_impl_coroutine 142 | # include 143 | #endif 144 | #include 145 | #include 146 | #include 147 | #include 148 | #include 149 | #include 150 | #include 151 | #include 152 | #include 153 | #endif 154 | 155 | #if __cplusplus > 202002L 156 | #include 157 | #include 158 | #if __has_include() 159 | # include 160 | #endif 161 | #include 162 | #endif 163 | -------------------------------------------------------------------------------- /cmd/submit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/charmbracelet/log" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/j178/leetgo/config" 12 | "github.com/j178/leetgo/lang" 13 | "github.com/j178/leetgo/leetcode" 14 | "github.com/j178/leetgo/utils" 15 | ) 16 | 17 | var submitCmd = &cobra.Command{ 18 | Use: "submit qid", 19 | Short: "Submit solution", 20 | Example: `leetgo submit 1 21 | leetgo submit two-sum 22 | leetgo submit last 23 | leetgo submit w330/1 24 | leetgo submit w330/ 25 | `, 26 | Aliases: []string{"s"}, 27 | Args: cobra.ExactArgs(1), 28 | ValidArgs: []string{"today", "last", "last/"}, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | cfg := config.Get() 31 | c := leetcode.NewClient(leetcode.ReadCredentials()) 32 | qs, err := leetcode.ParseQID(args[0], c) 33 | if err != nil { 34 | return err 35 | } 36 | gen, err := lang.GetGenerator(cfg.Code.Lang) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | user, err := c.GetUserStatus() 42 | if err != nil { 43 | return err 44 | } 45 | limiter := newLimiter(user) 46 | 47 | var hasFailedCase bool 48 | for _, q := range qs { 49 | log.Info("submitting solution", "question", q.TitleSlug, "user", user.Whoami(c)) 50 | result, err := submitSolution(cmd, q, c, gen, limiter) 51 | if err != nil { 52 | hasFailedCase = true 53 | log.Error("failed to submit solution", "err", err) 54 | continue 55 | } 56 | cmd.Print(result.Display(qs[0])) 57 | 58 | if !result.Accepted() { 59 | hasFailedCase = true 60 | added, _ := appendToTestCases(q, result) 61 | if added { 62 | log.Info("added failed case to testcases.txt") 63 | } 64 | } 65 | } 66 | 67 | err = showTodayStreak(c, cmd) 68 | if err != nil { 69 | log.Debug("failed to show today's streak", "err", err) 70 | } 71 | 72 | if hasFailedCase { 73 | return exitCode(1) 74 | } 75 | 76 | return nil 77 | }, 78 | } 79 | 80 | func submitSolution( 81 | cmd *cobra.Command, 82 | q *leetcode.QuestionData, 83 | c leetcode.Client, 84 | gen lang.Lang, 85 | limiter *utils.RateLimiter, 86 | ) ( 87 | *leetcode.SubmitCheckResult, 88 | error, 89 | ) { 90 | solution, err := lang.GetSolutionCode(q) 91 | if err != nil { 92 | return nil, fmt.Errorf("failed to get solution code: %w", err) 93 | } 94 | 95 | spin := newSpinner(cmd.ErrOrStderr()) 96 | spin.Suffix = " Submitting solution..." 97 | spin.Reverse() 98 | spin.Start() 99 | defer spin.Stop() 100 | 101 | limiter.Take() 102 | spin.Reverse() 103 | 104 | submissionId, err := c.SubmitCode(q, gen.Slug(), solution) 105 | if err != nil { 106 | return nil, fmt.Errorf("failed to submit solution: %w", err) 107 | } 108 | 109 | spin.Lock() 110 | spin.Suffix = " Waiting for result..." 111 | spin.Unlock() 112 | 113 | testResult, err := waitResult(c, submissionId) 114 | if err != nil { 115 | return nil, fmt.Errorf("failed to wait submit result: %w", err) 116 | } 117 | return testResult.(*leetcode.SubmitCheckResult), nil 118 | } 119 | 120 | func appendToTestCases(q *leetcode.QuestionData, result *leetcode.SubmitCheckResult) (bool, error) { 121 | genResult, err := lang.GeneratePathsOnly(q) 122 | if err != nil { 123 | return false, err 124 | } 125 | testCasesFile := genResult.GetFile(lang.TestCasesFile) 126 | if testCasesFile == nil || !utils.IsExist(testCasesFile.GetPath()) { 127 | return false, nil 128 | } 129 | 130 | failedCase := lang.TestCase{ 131 | Question: q, 132 | Input: strings.Split(result.LastTestcase, "\n"), 133 | Output: result.ExpectedOutput, 134 | } 135 | // some test cases are hidden during contest, they can be excluded by checking 136 | err = failedCase.Check() 137 | if err != nil { 138 | return false, err 139 | } 140 | 141 | tc, err := lang.ParseTestCases(q, testCasesFile) 142 | if err != nil { 143 | return false, err 144 | } 145 | if tc.Contains(failedCase) { 146 | return false, nil 147 | } 148 | tc.AddCase(failedCase) 149 | 150 | content := []byte(tc.String()) 151 | err = utils.WriteFile(testCasesFile.GetPath(), content) 152 | return true, err 153 | } 154 | 155 | func showTodayStreak(c leetcode.Client, cmd *cobra.Command) error { 156 | streak, err := c.GetStreakCounter() 157 | if errors.Is(err, errors.ErrUnsupported) { 158 | return nil 159 | } 160 | if err != nil { 161 | return err 162 | } 163 | today := "" 164 | if streak.TodayCompleted { 165 | today = config.PassedStyle.Render("+1") 166 | } 167 | cmd.Printf("\nTotal streak: %d%s\n", streak.StreakCount, today) 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod tidy 5 | - ./scripts/completions.sh 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - windows 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm64 16 | flags: 17 | - -v 18 | - -trimpath 19 | ldflags: 20 | - -s 21 | - -w 22 | - -X github.com/j178/leetgo/constants.Version={{.Version}} 23 | - -X github.com/j178/leetgo/constants.Commit={{.Commit}} 24 | - -X github.com/j178/leetgo/constants.BuildDate={{.Date}} 25 | 26 | archives: 27 | - formats: ['tar.gz'] 28 | # this name template makes the OS and Arch compatible with the results of uname. 29 | name_template: >- 30 | {{ .ProjectName }}_ 31 | {{- if eq .Os "darwin" }}macOS 32 | {{- else }}{{ .Os }}{{ end }}_ 33 | {{- if eq .Arch "amd64" }}x86_64 34 | {{- else if eq .Arch "386" }}i386 35 | {{- else }}{{ .Arch }}{{ end }} 36 | {{- if .Arm }}v{{ .Arm }}{{ end }} 37 | # use zip for windows archives 38 | format_overrides: 39 | - goos: windows 40 | formats: ['zip'] 41 | files: 42 | - LICENSE 43 | - README* 44 | - CHANGELOG* 45 | - completions/* 46 | checksum: 47 | name_template: 'checksums.txt' 48 | snapshot: 49 | version_template: "{{ incpatch .Version }}-next" 50 | changelog: 51 | use: github 52 | groups: 53 | - title: Features 54 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' 55 | order: 0 56 | - title: 'Bug fixes' 57 | regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' 58 | order: 1 59 | - title: Others 60 | order: 999 61 | filters: 62 | exclude: 63 | - '^docs:' 64 | - '^test:' 65 | - '(?i)^Minor' 66 | homebrew_casks: 67 | - repository: 68 | owner: j178 69 | name: homebrew-tap 70 | conflicts: 71 | - formula: leetgo 72 | commit_author: 73 | name: goreleaserbot 74 | email: bot@goreleaser.com 75 | homepage: https://github.com/j178/leetgo 76 | description: >- 77 | leetgo is a command line tool for leetcode.com. It can help you to login, 78 | submit, test, and view your submissions. 79 | license: MIT 80 | completions: 81 | bash: completions/leetgo.bash 82 | zsh: completions/leetgo.zsh 83 | fish: completions/leetgo.fish 84 | hooks: 85 | post: 86 | install: | 87 | if system_command("/usr/bin/xattr", args: ["-h"]).exit_status == 0 88 | system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/leetgo"] 89 | end 90 | 91 | scoops: 92 | - repository: 93 | owner: j178 94 | name: scoop-bucket 95 | commit_author: 96 | name: goreleaserbot 97 | email: bot@goreleaser.com 98 | directory: bucket 99 | homepage: https://github.com/j178/leetgo 100 | description: >- 101 | leetgo is a command line tool for leetcode.com. It can help you to login, 102 | submit, test, and view your submissions. 103 | license: MIT 104 | 105 | aurs: 106 | - homepage: https://github.com/j178/leetgo 107 | description: >- 108 | leetgo is a command line tool for leetcode.com. It can help you to login, 109 | submit, test, and view your submissions. 110 | license: MIT 111 | maintainers: 112 | - "j178 <10510431+j178@users.noreply.github.com>" 113 | private_key: "{{ .Env.AUR_PRIVATE_KEY }}" 114 | git_url: "ssh://aur@aur.archlinux.org/leetgo-bin.git" 115 | package: |- 116 | # bin 117 | install -Dm755 "./leetgo" "${pkgdir}/usr/bin/leetgo" 118 | 119 | # license 120 | install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/leetgo/LICENSE" 121 | 122 | # completions 123 | mkdir -p "${pkgdir}/usr/share/bash-completion/completions/" 124 | mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" 125 | mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" 126 | install -Dm644 "./completions/leetgo.bash" "${pkgdir}/usr/share/bash-completion/completions/leetgo" 127 | install -Dm644 "./completions/leetgo.zsh" "${pkgdir}/usr/share/zsh/site-functions/_leetgo" 128 | install -Dm644 "./completions/leetgo.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/leetgo.fish" 129 | 130 | announce: 131 | # DISCORD_WEBHOOK_ID and DISCORD_WEBHOOK_TOKEN 132 | discord: 133 | enabled: true 134 | 135 | # The lines beneath this are called `modelines`. See `:help modeline` 136 | # Feel free to remove those if you don't want/use them. 137 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 138 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 139 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "os/user" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/charmbracelet/log" 12 | "github.com/spf13/cobra" 13 | 14 | "github.com/j178/leetgo/config" 15 | "github.com/j178/leetgo/constants" 16 | "github.com/j178/leetgo/leetcode" 17 | "github.com/j178/leetgo/utils" 18 | ) 19 | 20 | var ( 21 | force bool 22 | initTemplate string 23 | ) 24 | 25 | var initCmd = &cobra.Command{ 26 | Use: "init [DIR]", 27 | Short: "Init a leetcode workspace", 28 | Example: "leetgo init -t us -l cpp", 29 | Args: cobra.MaximumNArgs(1), 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | dir := "." 32 | if len(args) > 0 { 33 | dir = args[0] 34 | } 35 | dir, _ = filepath.Abs(dir) 36 | if initTemplate != "" && initTemplate != "us" && initTemplate != "cn" { 37 | return fmt.Errorf("invalid template %s, only us or cn is supported", initTemplate) 38 | } 39 | err := utils.CreateIfNotExists(dir, true) 40 | if err != nil { 41 | return err 42 | } 43 | err = createConfigDir() 44 | if err != nil { 45 | return err 46 | } 47 | err = createConfigFile(dir) 48 | if err != nil { 49 | return err 50 | } 51 | if gitAvailable() && !isInsideGitRepo(dir) { 52 | _ = initGitRepo(dir) 53 | } 54 | err = createQuestionCache() 55 | if err != nil { 56 | return err 57 | } 58 | return nil 59 | }, 60 | } 61 | 62 | func init() { 63 | initCmd.Flags().StringVarP(&initTemplate, "template", "t", "", "template to use, cn or us") 64 | initCmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite config file if exists") 65 | 66 | _ = initCmd.RegisterFlagCompletionFunc( 67 | "template", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 68 | return []string{"us", "cn"}, cobra.ShellCompDirectiveNoFileComp 69 | }, 70 | ) 71 | } 72 | 73 | func createConfigDir() error { 74 | dir := config.Get().HomeDir() 75 | if utils.IsExist(dir) { 76 | return nil 77 | } 78 | err := utils.MakeDir(dir) 79 | if err != nil { 80 | return err 81 | } 82 | log.Info("config dir created", "dir", dir) 83 | return nil 84 | } 85 | 86 | func createConfigFile(dir string) error { 87 | cfg := config.Get() 88 | site := cfg.LeetCode.Site 89 | language := cfg.Language 90 | switch initTemplate { 91 | case "us": 92 | site = config.LeetCodeUS 93 | language = config.EN 94 | case "cn": 95 | site = config.LeetCodeCN 96 | language = config.ZH 97 | } 98 | 99 | author := defaultUser() 100 | cfg.LeetCode.Site = site 101 | cfg.Language = language 102 | cfg.Author = author 103 | 104 | projectFile := filepath.Join(dir, constants.ConfigFilename) 105 | if utils.IsExist(projectFile) && !force { 106 | return fmt.Errorf("config file %s already exists, use -f to overwrite", utils.RelToCwd(projectFile)) 107 | } 108 | 109 | f, err := os.Create(projectFile) 110 | if err != nil { 111 | return err 112 | } 113 | defer func() { _ = f.Close() }() 114 | 115 | _, _ = f.WriteString("# Leetgo configuration file, see more at https://github.com/j178/leetgo\n\n") 116 | _ = cfg.Write(f, true) 117 | log.Info("config file created", "file", utils.RelToCwd(projectFile)) 118 | 119 | return nil 120 | } 121 | 122 | func createQuestionCache() error { 123 | c := leetcode.NewClient(leetcode.ReadCredentials()) 124 | cache := leetcode.GetCache(c) 125 | if !cache.Outdated() { 126 | return nil 127 | } 128 | err := cache.Update() 129 | if err != nil { 130 | return err 131 | } 132 | return nil 133 | } 134 | 135 | func defaultUser() string { 136 | username := getGitUsername() 137 | if username != "" { 138 | return username 139 | } 140 | u, err := user.Current() 141 | if err == nil { 142 | return u.Username 143 | } 144 | username = os.Getenv("USER") 145 | if username != "" { 146 | return username 147 | } 148 | return "Bob" 149 | } 150 | 151 | func gitAvailable() bool { 152 | cmd := exec.Command("git", "--version") 153 | err := cmd.Run() 154 | return err == nil 155 | } 156 | 157 | func initGitRepo(dir string) error { 158 | cmd := exec.Command("git", "init", dir) 159 | err := cmd.Run() 160 | if err != nil { 161 | return err 162 | } 163 | err = utils.WriteOrAppendFile(filepath.Join(dir, ".gitignore"), []byte(".env\n")) 164 | return err 165 | } 166 | 167 | func isInsideGitRepo(dir string) bool { 168 | cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree", dir) 169 | err := cmd.Run() 170 | return err == nil 171 | } 172 | 173 | func getGitUsername() string { 174 | cmd := exec.Command("git", "config", "user.name") 175 | out, err := cmd.Output() 176 | if err != nil { 177 | return "" 178 | } 179 | return strings.TrimSpace(string(out)) 180 | } 181 | -------------------------------------------------------------------------------- /leetcode/qid.go: -------------------------------------------------------------------------------- 1 | package leetcode 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/j178/leetgo/config" 12 | ) 13 | 14 | func QuestionFromCacheBySlug(slug string, c Client) (*QuestionData, error) { 15 | q := GetCache(c).GetBySlug(slug) 16 | if q != nil { 17 | q.client = c 18 | return q, nil 19 | } 20 | return nil, ErrQuestionNotFound 21 | } 22 | 23 | func QuestionFromCacheByID(id string, c Client) (*QuestionData, error) { 24 | q := GetCache(c).GetById(id) 25 | if q != nil { 26 | q.client = c 27 | return q, nil 28 | } 29 | return nil, ErrQuestionNotFound 30 | } 31 | 32 | // QuestionBySlug loads question data from cache first, if not found, fetch from leetcode.com 33 | func QuestionBySlug(slug string, c Client) (*QuestionData, error) { 34 | q, err := QuestionFromCacheBySlug(slug, c) 35 | if err != nil { 36 | q, err = c.GetQuestionData(slug) 37 | } 38 | if q != nil { 39 | q.client = c 40 | } 41 | return q, err 42 | } 43 | 44 | func ParseQID(qid string, c Client) ([]*QuestionData, error) { 45 | var ( 46 | q *QuestionData 47 | qs []*QuestionData 48 | err error 49 | ) 50 | switch { 51 | case isNumber(qid): 52 | q, err = QuestionFromCacheByID(qid, c) 53 | case qid == "last": 54 | state := config.LoadState() 55 | if state.LastQuestion.Slug != "" { 56 | q, err = QuestionBySlug(state.LastQuestion.Slug, c) 57 | } else { 58 | err = errors.New("invalid qid: last generated question not found") 59 | } 60 | case qid == "today": 61 | q, err = c.GetTodayQuestion() 62 | case qid == "yesterday": 63 | q, err = c.GetQuestionOfDate(time.Now().AddDate(0, 0, -1)) 64 | case strings.HasPrefix(qid, "today-"): 65 | var n int 66 | n, err = strconv.Atoi(qid[6:]) 67 | if err == nil { 68 | q, err = c.GetQuestionOfDate(time.Now().AddDate(0, 0, -n)) 69 | } 70 | case strings.Contains(qid, "/"): 71 | _, qs, err = ParseContestQID(qid, c, true) 72 | if err != nil { 73 | return nil, err 74 | } 75 | } 76 | if errors.Is(err, ErrQuestionNotFound) { 77 | err = nil 78 | } 79 | if err != nil { 80 | return nil, fmt.Errorf("invalid qid: %w", err) 81 | } 82 | if q == nil && len(qs) == 0 { 83 | q, err = QuestionBySlug(qid, c) 84 | if errors.Is(err, ErrQuestionNotFound) { 85 | q, err = QuestionFromCacheByID(qid, c) 86 | } 87 | if err != nil { 88 | return nil, fmt.Errorf("invalid qid: %w", err) 89 | } 90 | } 91 | if q != nil { 92 | qs = []*QuestionData{q} 93 | } 94 | if len(qs) == 0 { 95 | return nil, errors.New("invalid qid: no such question") 96 | } 97 | return qs, nil 98 | } 99 | 100 | func ParseContestQID(qid string, c Client, withQuestions bool) (*Contest, []*QuestionData, error) { 101 | if len(qid) < 3 { 102 | return nil, nil, errors.New("invalid contest qid") 103 | } 104 | if strings.Count(qid, "/") != 1 { 105 | return nil, nil, errors.New("invalid contest qid") 106 | } 107 | 108 | var ( 109 | contestSlug string 110 | questionNum = -1 111 | err error 112 | q *QuestionData 113 | qs []*QuestionData 114 | ) 115 | contestPat := regexp.MustCompile(`(?i)([wb])\D*(\d+)`) 116 | parts := strings.SplitN(qid, "/", 2) 117 | matches := contestPat.FindStringSubmatch(parts[0]) 118 | if matches == nil { 119 | contestSlug = parts[0] 120 | if contestSlug == "last" { 121 | state := config.LoadState() 122 | if state.LastContest == "" { 123 | return nil, nil, errors.New("invalid contest qid: last contest not found") 124 | } 125 | contestSlug = state.LastContest 126 | } 127 | } else { 128 | if matches[1][0] == 'w' || matches[1][0] == 'W' { 129 | contestSlug = "weekly-contest-" + matches[2] 130 | } else { 131 | contestSlug = "biweekly-contest-" + matches[2] 132 | } 133 | } 134 | if len(parts[1]) > 0 { 135 | questionNum, err = strconv.Atoi(parts[1]) 136 | if err != nil { 137 | return nil, nil, fmt.Errorf("invalid contest qid: %s is not a number", parts[1]) 138 | } 139 | } 140 | contest, err := c.GetContest(contestSlug) 141 | if err != nil { 142 | return nil, nil, fmt.Errorf("contest not found %s: %w", contestSlug, err) 143 | } 144 | 145 | if withQuestions { 146 | if questionNum > 0 { 147 | q, err = contest.GetQuestionByNumber(questionNum) 148 | } else { 149 | qs, err = contest.GetAllQuestions() 150 | } 151 | if err != nil { 152 | questionName := "" 153 | if questionNum > 0 { 154 | questionName = strconv.Itoa(questionNum) 155 | } 156 | return contest, nil, fmt.Errorf("get contest question %s failed: %w", questionName, err) 157 | } 158 | if q != nil { 159 | qs = []*QuestionData{q} 160 | } 161 | } 162 | 163 | return contest, qs, nil 164 | } 165 | 166 | func isNumber(s string) bool { 167 | _, err := strconv.Atoi(s) 168 | return err == nil 169 | } 170 | -------------------------------------------------------------------------------- /cmd/fix.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/AlecAivazis/survey/v2" 11 | "github.com/charmbracelet/glamour" 12 | "github.com/charmbracelet/log" 13 | "github.com/hexops/gotextdiff" 14 | "github.com/hexops/gotextdiff/myers" 15 | "github.com/sashabaranov/go-openai" 16 | "github.com/spf13/cobra" 17 | "github.com/spf13/viper" 18 | 19 | "github.com/j178/leetgo/lang" 20 | "github.com/j178/leetgo/leetcode" 21 | "github.com/j178/leetgo/utils" 22 | ) 23 | 24 | // Use ChatGPT API to fix solution code 25 | 26 | var fixCmd = &cobra.Command{ 27 | Use: "fix qid", 28 | Short: "Use ChatGPT API to fix your solution code (just for fun)", 29 | Long: `Use ChatGPT API to fix your solution code. 30 | Set OPENAI_API_KEY environment variable to your OpenAI API key before using this command.`, 31 | Example: `leetgo fix 429`, 32 | Args: cobra.ExactArgs(1), 33 | ValidArgs: []string{"today", "last"}, 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | c := leetcode.NewClient(leetcode.ReadCredentials()) 36 | qs, err := leetcode.ParseQID(args[0], c) 37 | if err != nil { 38 | return err 39 | } 40 | if len(qs) > 1 { 41 | return fmt.Errorf("multiple questions found") 42 | } 43 | q := qs[0] 44 | err = q.Fulfill() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | code, err := lang.GetSolutionCode(q) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | fixedCode, err := askOpenAI(cmd, q, code) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | output := "# Here is the fix from OpenAI GPT-3 API\n" 60 | edits := myers.ComputeEdits("", code, fixedCode) 61 | diff := gotextdiff.ToUnified("original", "AI fixed", code, edits) 62 | output += "```diff\n" + fmt.Sprint(diff) + "\n```\n" 63 | output, err = glamour.Render(output, "dark") 64 | if err != nil { 65 | return err 66 | } 67 | cmd.Println(output) 68 | 69 | accept := true 70 | if !viper.GetBool("yes") { 71 | err = survey.AskOne( 72 | &survey.Confirm{ 73 | Message: "Do you want to accept the fix?", 74 | }, &accept, 75 | ) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | if accept { 81 | err = lang.UpdateSolutionCode(q, fixedCode) 82 | if err != nil { 83 | return err 84 | } 85 | } 86 | return nil 87 | }, 88 | } 89 | 90 | const fixPrompt = `Given a LeetCode problem %s, the problem description below is wrapped in and tags. The solution code is wrapped in and tags: 91 | 92 | %s 93 | 94 | 95 | I have written the following solution: 96 | 97 | %s 98 | 99 | 100 | Please identify any issues or inefficiencies in my code and to help me fix or improve it. DO NOT rewrite it. 101 | I want you to only reply with pure code without or markdown tags, and nothing else. DO NOT write explanations. 102 | ` 103 | 104 | var errNoFix = errors.New("no fix found") 105 | 106 | func askOpenAI(cmd *cobra.Command, q *leetcode.QuestionData, code string) (string, error) { 107 | apiKey := os.Getenv("OPENAI_API_KEY") 108 | if apiKey == "" { 109 | return "", errors.New("missing OPENAI_API_KEY environment variable, you can find or create your API key here: https://platform.openai.com/account/api-keys") 110 | } 111 | baseURI := os.Getenv("OPENAI_API_ENDPOINT") 112 | config := openai.DefaultConfig(apiKey) 113 | if baseURI != "" { 114 | config.BaseURL = baseURI 115 | } 116 | client := openai.NewClientWithConfig(config) 117 | prompt := fmt.Sprintf( 118 | fixPrompt, 119 | q.Title, 120 | q.GetFormattedContent(), 121 | code, 122 | ) 123 | log.Debug("requesting openai", "prompt", prompt) 124 | spin := newSpinner(cmd.OutOrStdout()) 125 | spin.Suffix = " Waiting for OpenAI..." 126 | spin.Start() 127 | defer spin.Stop() 128 | 129 | ctx := context.Background() 130 | resp, err := client.CreateChatCompletion( 131 | ctx, openai.ChatCompletionRequest{ 132 | Model: openai.GPT3Dot5Turbo, 133 | Messages: []openai.ChatCompletionMessage{ 134 | {Role: "system", Content: "Help solve LeetCode questions and fix the code"}, 135 | {Role: "user", Content: prompt}, 136 | }, 137 | MaxTokens: 1000, 138 | Temperature: 0, 139 | }, 140 | ) 141 | if err != nil { 142 | return "", err 143 | } 144 | if len(resp.Choices) == 0 { 145 | return "", errNoFix 146 | } 147 | log.Debug("got response from openai", "response", resp.Choices) 148 | text := resp.Choices[0].Message.Content 149 | 150 | if strings.HasPrefix(text, "```") { 151 | firstLine := strings.IndexByte(text, '\n') 152 | if firstLine == -1 { 153 | return "", errNoFix 154 | } 155 | text = text[firstLine+1:] 156 | text = strings.TrimSuffix(text, "```") 157 | } 158 | text = utils.EnsureTrailingNewline(text) 159 | return text, nil 160 | } 161 | -------------------------------------------------------------------------------- /utils/str.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strings" 7 | "unicode" 8 | "unicode/utf16" 9 | "unsafe" 10 | ) 11 | 12 | // BytesToString converts byte slice to string. 13 | func BytesToString(b []byte) string { 14 | return *(*string)(unsafe.Pointer(&b)) 15 | } 16 | 17 | // StringToBytes converts string to byte slice. 18 | func StringToBytes(s string) []byte { 19 | return *(*[]byte)(unsafe.Pointer( 20 | &struct { 21 | string 22 | Cap int 23 | }{s, len(s)}, 24 | )) 25 | } 26 | 27 | // SplitLines splits a string into lines by '\n' or '\r\n'. 28 | func SplitLines(s string) []string { 29 | return strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n") 30 | } 31 | 32 | // CondenseEmptyLines condenses multiple consecutive empty lines in a string to a single empty line, 33 | // while preserving non-empty lines. 34 | func CondenseEmptyLines(s string) string { 35 | lines := strings.Split(s, "\n") 36 | var filtered []string 37 | for i := 0; i < len(lines); i++ { 38 | if i == 0 || lines[i] != "" || lines[i-1] != "" { 39 | filtered = append(filtered, lines[i]) 40 | } 41 | } 42 | return strings.Join(filtered, "\n") 43 | } 44 | 45 | func EnsureTrailingNewline(s string) string { 46 | if s == "" || s[len(s)-1] != '\n' { 47 | return s + "\n" 48 | } 49 | return s 50 | } 51 | 52 | // TruncateString shortens a string longer than n by replacing the middle part with "......" 53 | func TruncateString(s string, n int) string { 54 | if len(s) <= n || n < 30 { 55 | return s 56 | } 57 | suffix := fmt.Sprintf("......<%d bytes truncated>", len(s)-30) 58 | if n < len(suffix) { 59 | return suffix 60 | } 61 | 62 | truncated := s[:n-len(suffix)] + suffix 63 | 64 | return truncated 65 | } 66 | 67 | func CamelToSnake(name string) string { 68 | var snakeStrBuilder strings.Builder 69 | 70 | for i, r := range name { 71 | if i > 0 && unicode.IsUpper(r) && !unicode.IsUpper([]rune(name)[i-1]) { 72 | snakeStrBuilder.WriteRune('_') 73 | } 74 | snakeStrBuilder.WriteRune(unicode.ToLower(r)) 75 | } 76 | 77 | return snakeStrBuilder.String() 78 | } 79 | 80 | var ( 81 | subscripts = map[string]string{ 82 | "0": "\u2080", 83 | "1": "\u2081", 84 | "2": "\u2082", 85 | "3": "\u2083", 86 | "4": "\u2084", 87 | "5": "\u2085", 88 | "6": "\u2086", 89 | "7": "\u2087", 90 | "8": "\u2088", 91 | "9": "\u2089", 92 | "a": "\u2090", 93 | "e": "\u2091", 94 | "h": "\u2095", 95 | "i": "\u1d62", 96 | "j": "\u2c7c", 97 | "k": "\u2096", 98 | "l": "\u2097", 99 | "m": "\u2098", 100 | "n": "\u2099", 101 | "o": "\u2092", 102 | "p": "\u209a", 103 | "r": "\u1d63", 104 | "s": "\u209b", 105 | "t": "\u209c", 106 | "u": "\u1d64", 107 | "v": "\u1d65", 108 | "x": "\u2093", 109 | "y": "\u1d67", 110 | "+": "\u208A", 111 | "-": "\u208B", 112 | "=": "\u208C", 113 | "(": "\u208D", 114 | ")": "\u208E", 115 | } 116 | superscripts = map[string]string{ 117 | "0": "\u2070", 118 | "1": "\u00b9", 119 | "2": "\u00b2", 120 | "3": "\u00b3", 121 | "4": "\u2074", 122 | "5": "\u2075", 123 | "6": "\u2076", 124 | "7": "\u2077", 125 | "8": "\u2078", 126 | "9": "\u2079", 127 | "a": "\u1D43", 128 | "b": "\u1D47", 129 | "c": "\u1D9C", 130 | "d": "\u1D48", 131 | "e": "\u1D49", 132 | "f": "\u1DA0", 133 | "g": "\u1D4D", 134 | "h": "\u02B0", 135 | "i": "\u2071", 136 | "j": "\u02B2", 137 | "k": "\u1D4F", 138 | "l": "\u02E1", 139 | "m": "\u1D50", 140 | "n": "\u207F", 141 | "o": "\u1D52", 142 | "p": "\u1D56", 143 | "q": "\u02A0", 144 | "r": "\u02B3", 145 | "s": "\u02E2", 146 | "t": "\u1D57", 147 | "u": "\u1D58", 148 | "v": "\u1D5B", 149 | "w": "\u02B7", 150 | "x": "\u02E3", 151 | "y": "\u02B8", 152 | "z": "\u1DBB", 153 | "+": "\u207A", 154 | "-": "\u207B", 155 | "=": "\u207C", 156 | "(": "\u207D", 157 | ")": "\u207E", 158 | } 159 | subReplace = func() *strings.Replacer { 160 | args := make([]string, 0, len(subscripts)*2) 161 | for k, v := range subscripts { 162 | args = append(args, k, v) 163 | } 164 | return strings.NewReplacer(args...) 165 | }() 166 | supReplace = func() *strings.Replacer { 167 | args := make([]string, 0, len(superscripts)*2) 168 | for k, v := range superscripts { 169 | args = append(args, k, v) 170 | } 171 | return strings.NewReplacer(args...) 172 | }() 173 | ) 174 | 175 | func ReplaceSubscript(s string) string { 176 | return subReplace.Replace(s) 177 | } 178 | 179 | func ReplaceSuperscript(s string) string { 180 | return supReplace.Replace(s) 181 | } 182 | 183 | func DecodeRawUnicodeEscape(s string) string { 184 | var buf strings.Builder 185 | for i := 0; i < len(s); i++ { 186 | if s[i] == '\\' && i+5 < len(s) && s[i+1] == 'u' { 187 | b16, _ := hex.DecodeString(s[i+2 : i+6]) 188 | value := uint16(b16[0])<<8 + uint16(b16[1]) 189 | chr := utf16.Decode([]uint16{value})[0] 190 | buf.WriteRune(chr) 191 | i += 5 192 | } else { 193 | buf.WriteByte(s[i]) 194 | } 195 | } 196 | return buf.String() 197 | } 198 | -------------------------------------------------------------------------------- /cmd/info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/charmbracelet/log" 11 | "github.com/jedib0t/go-pretty/v6/table" 12 | "github.com/jedib0t/go-pretty/v6/text" 13 | "github.com/spf13/cobra" 14 | 15 | "github.com/j178/leetgo/leetcode" 16 | ) 17 | 18 | type outputFormat string 19 | 20 | func (e *outputFormat) String() string { 21 | return string(*e) 22 | } 23 | 24 | func (e *outputFormat) Set(v string) error { 25 | switch v { 26 | case "json": 27 | *e = outputFormat(v) 28 | return nil 29 | default: 30 | return errors.New(`must be one of the supported formats: "json"`) 31 | } 32 | } 33 | 34 | func (e *outputFormat) Type() string { 35 | return "outputFormat" 36 | } 37 | 38 | var ( 39 | flagFull bool 40 | flagFormat outputFormat = "default" 41 | ) 42 | 43 | func init() { 44 | infoCmd.Flags().BoolVar(&flagFull, "full", false, "show full question info") 45 | infoCmd.Flags().Var(&flagFormat, "format", "show question info in specific format (json)") 46 | } 47 | 48 | // A simplified version of the leetcode.QuestionData struct 49 | type question struct { 50 | FrontendId string `json:"frontend_id"` 51 | Title string `json:"title"` 52 | Slug string `json:"slug"` 53 | Difficulty string `json:"difficulty"` 54 | Url string `json:"url"` 55 | Tags []string `json:"tags"` 56 | IsPaidOnly bool `json:"is_paid_only"` 57 | TotalAccepted string `json:"total_accepted"` 58 | TotalAcceptedRaw int `json:"total_accepted_raw"` 59 | TotalSubmission string `json:"total_submission"` 60 | TotalSubmissionRaw int `json:"total_submission_raw"` 61 | ACRate string `json:"ac_rate"` 62 | Content string `json:"content"` 63 | Hints []string `json:"hints"` 64 | } 65 | 66 | var infoCmd = &cobra.Command{ 67 | Use: "info qid...", 68 | Short: "Show question info", 69 | Example: "leetgo info 145\nleetgo info two-sum", 70 | Args: cobra.MinimumNArgs(1), 71 | Aliases: []string{"i"}, 72 | ValidArgs: []string{"today", "last"}, 73 | RunE: func(cmd *cobra.Command, args []string) error { 74 | c := leetcode.NewClient(leetcode.ReadCredentials()) 75 | 76 | var questions []question 77 | for _, qid := range args { 78 | qs, err := leetcode.ParseQID(qid, c) 79 | if err != nil { 80 | log.Error("failed to get question", "qid", qid, "err", err) 81 | continue 82 | } 83 | for _, q := range qs { 84 | _ = q.Fulfill() 85 | content := "" 86 | if flagFull { 87 | content = q.GetFormattedContent() 88 | } 89 | questions = append( 90 | questions, question{ 91 | FrontendId: q.QuestionFrontendId, 92 | Title: q.GetTitle(), 93 | Slug: q.TitleSlug, 94 | Difficulty: q.Difficulty, 95 | Url: q.Url(), 96 | Tags: q.TagSlugs(), 97 | IsPaidOnly: q.IsPaidOnly, 98 | TotalAccepted: q.Stats.TotalAccepted, 99 | TotalAcceptedRaw: q.Stats.TotalAcceptedRaw, 100 | TotalSubmission: q.Stats.TotalSubmission, 101 | TotalSubmissionRaw: q.Stats.TotalSubmissionRaw, 102 | ACRate: q.Stats.ACRate, 103 | Content: content, 104 | Hints: q.Hints, 105 | }, 106 | ) 107 | } 108 | } 109 | if len(questions) == 0 { 110 | return errors.New("no questions found") 111 | } 112 | 113 | switch flagFormat { 114 | default: 115 | outputHuman(questions, cmd.OutOrStdout()) 116 | case "json": 117 | outputJson(questions, cmd.OutOrStdout()) 118 | } 119 | 120 | return nil 121 | }, 122 | } 123 | 124 | func outputHuman(qs []question, out io.Writer) { 125 | w := table.NewWriter() 126 | w.SetOutputMirror(out) 127 | w.SetStyle(table.StyleColoredDark) 128 | w.SetColumnConfigs( 129 | []table.ColumnConfig{ 130 | { 131 | Number: 2, 132 | WidthMax: 50, 133 | }, 134 | }, 135 | ) 136 | for _, q := range qs { 137 | w.AppendRow( 138 | table.Row{fmt.Sprintf("%s. %s", q.FrontendId, q.Title)}, 139 | table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignLeft}, 140 | ) 141 | w.AppendRow(table.Row{"Slug", q.Slug}) 142 | w.AppendRow(table.Row{"Difficulty", q.Difficulty}) 143 | w.AppendRow(table.Row{"URL", q.Url}) 144 | w.AppendRow(table.Row{"Tags", strings.Join(q.Tags, ", ")}) 145 | w.AppendRow(table.Row{"Paid Only", q.IsPaidOnly}) 146 | w.AppendRow( 147 | table.Row{ 148 | "AC Rate", 149 | fmt.Sprintf("%s/%s %s", q.TotalAccepted, q.TotalSubmission, q.ACRate), 150 | }, 151 | ) 152 | if q.Content != "" { 153 | w.AppendRow(table.Row{"Content", q.Content}) 154 | } 155 | for _, h := range q.Hints { 156 | w.AppendRow(table.Row{"Hint", h}) 157 | } 158 | w.AppendSeparator() 159 | } 160 | w.Render() 161 | } 162 | 163 | func outputJson(qs []question, out io.Writer) { 164 | enc := json.NewEncoder(out) 165 | enc.SetIndent("", " ") 166 | _ = enc.Encode(qs) 167 | } 168 | -------------------------------------------------------------------------------- /testutils/rust/src/tree.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::VecDeque; 3 | use std::rc::Rc; 4 | 5 | use serde::{Deserialize, Serialize, Serializer}; 6 | use serde::de::SeqAccess; 7 | use serde::ser::SerializeSeq; 8 | 9 | // LeetCode use `Option>>` for tree links, but `Option>` should be enough. 10 | // https://github.com/pretzelhammer/rust-blog/blob/master/posts/learning-rust-in-2020.md#leetcode 11 | type TreeLink = Option>>; 12 | 13 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 14 | pub struct TreeNode { 15 | pub val: i32, 16 | pub left: TreeLink, 17 | pub right: TreeLink, 18 | } 19 | 20 | impl TreeNode { 21 | #[inline] 22 | pub fn new(val: i32) -> Self { 23 | TreeNode { 24 | val, 25 | left: None, 26 | right: None 27 | } 28 | } 29 | } 30 | 31 | #[macro_export] 32 | macro_rules! tree { 33 | () => { 34 | None 35 | }; 36 | ($e:expr) => { 37 | Some(Rc::new(RefCell::new(TreeNode::new($e)))) 38 | }; 39 | } 40 | 41 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 42 | pub struct BinaryTree(TreeLink); 43 | 44 | impl From for TreeLink { 45 | fn from(tree: BinaryTree) -> Self { 46 | tree.0 47 | } 48 | } 49 | 50 | impl From for BinaryTree { 51 | fn from(link: TreeLink) -> Self { 52 | BinaryTree(link) 53 | } 54 | } 55 | 56 | impl Serialize for BinaryTree { 57 | fn serialize(&self, serializer: S) -> Result 58 | where 59 | S: Serializer, 60 | { 61 | let mut queue = VecDeque::new(); 62 | let mut nodes = Vec::new(); 63 | queue.push_back(self.0.clone()); 64 | while let Some(node) = queue.pop_front() { 65 | nodes.push(node.clone()); 66 | if let Some(ref node) = node { 67 | queue.push_back(node.borrow().left.clone()); 68 | queue.push_back(node.borrow().right.clone()); 69 | } 70 | } 71 | while nodes.len() > 0 && nodes.last().unwrap().is_none() { 72 | nodes.pop(); 73 | } 74 | 75 | let mut seq = serializer.serialize_seq(None)?; 76 | for node in nodes { 77 | if let Some(ref node) = node { 78 | seq.serialize_element(&node.borrow().val.clone())?; 79 | } else { 80 | seq.serialize_element(&None::)?; 81 | } 82 | } 83 | seq.end() 84 | } 85 | } 86 | 87 | struct BinaryTreeVisitor; 88 | 89 | impl<'de> serde::de::Visitor<'de> for BinaryTreeVisitor { 90 | type Value = BinaryTree; 91 | 92 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 93 | formatter.write_str("a list of optional integers") 94 | } 95 | 96 | fn visit_seq(self, mut seq: A) -> Result 97 | where 98 | A: SeqAccess<'de>, 99 | { 100 | let mut nodes: Vec = Vec::new(); 101 | 102 | while let Some(val) = seq.next_element::>()? { 103 | nodes.push(val.map(|v: i32| Rc::new(RefCell::new(TreeNode { 104 | val: v, 105 | left: None, 106 | right: None, 107 | })))); 108 | } 109 | 110 | let root = nodes[0].clone(); 111 | let (mut i, mut j) = (0, 1); 112 | 113 | while j < nodes.len() { 114 | if let Some(ref current_node) = nodes[i] { 115 | current_node.borrow_mut().left = nodes[j].clone(); 116 | j += 1; 117 | if j < nodes.len() { 118 | current_node.borrow_mut().right = nodes[j].clone(); 119 | j += 1; 120 | } 121 | } 122 | i += 1; 123 | } 124 | 125 | Ok(BinaryTree(root)) 126 | } 127 | } 128 | 129 | impl<'de> Deserialize<'de> for BinaryTree { 130 | fn deserialize(deserializer: D) -> Result 131 | where 132 | D: serde::Deserializer<'de>, 133 | { 134 | deserializer.deserialize_seq(BinaryTreeVisitor) 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod tests { 140 | use super::*; 141 | 142 | #[test] 143 | fn test_tree_serialize() { 144 | let root = TreeNode { 145 | val: 1, 146 | left: Some(Rc::new(RefCell::new(TreeNode { 147 | val: 2, 148 | left: None, 149 | right: None, 150 | }))), 151 | right: Some(Rc::new(RefCell::new(TreeNode { 152 | val: 4, 153 | left: Some(Rc::new(RefCell::new(TreeNode { 154 | val: 3, 155 | left: None, 156 | right: None, 157 | }))), 158 | right: None, 159 | }))), 160 | }; 161 | let tree = BinaryTree(Some(Rc::new(RefCell::new(root)))); 162 | let serialized = serde_json::to_string(&tree).unwrap(); 163 | assert_eq!(serialized, "[1,2,4,null,null,3]"); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "runtime/debug" 9 | 10 | "github.com/charmbracelet/log" 11 | cc "github.com/ivanpirog/coloredcobra" 12 | "github.com/joho/godotenv" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | 16 | "github.com/j178/leetgo/config" 17 | "github.com/j178/leetgo/constants" 18 | "github.com/j178/leetgo/lang" 19 | ) 20 | 21 | func buildVersion() string { 22 | result := constants.Version 23 | if constants.Commit != "" { 24 | result = fmt.Sprintf("%s\ncommit: %s", result, constants.Commit) 25 | } 26 | if constants.BuildDate != "" { 27 | result = fmt.Sprintf("%s\nbuilt at: %s", result, constants.BuildDate) 28 | } 29 | result = fmt.Sprintf("%s\ngoos: %s\ngoarch: %s", result, runtime.GOOS, runtime.GOARCH) 30 | if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" { 31 | result = fmt.Sprintf("%s\nmodule version: %s, checksum: %s", result, info.Main.Version, info.Main.Sum) 32 | } 33 | return result 34 | } 35 | 36 | var rootCmd = &cobra.Command{ 37 | Use: constants.CmdName, 38 | Short: "Leetcode", 39 | Long: "Leetcode friend for geek.", 40 | Version: buildVersion() + "\n\n" + constants.ProjectURL, 41 | SilenceErrors: true, 42 | SilenceUsage: true, 43 | } 44 | 45 | type exitCode int 46 | 47 | func (e exitCode) Error() string { 48 | return fmt.Sprintf("exit code %d", e) 49 | } 50 | 51 | func Execute() { 52 | err := rootCmd.Execute() 53 | if err != nil { 54 | var e exitCode 55 | if errors.As(err, &e) { 56 | os.Exit(int(e)) 57 | } 58 | log.Fatal(err) 59 | } 60 | } 61 | 62 | func preRun(cmd *cobra.Command, _ []string) error { 63 | initLogger() 64 | err := initWorkDir() 65 | if err != nil { 66 | return err 67 | } 68 | err = config.Load(cmd == initCmd) 69 | if err != nil { 70 | return err 71 | } 72 | err = godotenv.Load() 73 | if err != nil && !os.IsNotExist(err) { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | func UsageString() string { 80 | return rootCmd.UsageString() 81 | } 82 | 83 | func initWorkDir() error { 84 | if dir := os.Getenv("LEETGO_WORKDIR"); dir != "" { 85 | log.Debug("change workdir to LEETGO_WORKDIR", "dir", dir) 86 | return os.Chdir(dir) 87 | } 88 | return nil 89 | } 90 | 91 | func initLogger() { 92 | if config.Debug { 93 | log.SetReportTimestamp(true) 94 | log.SetLevel(log.DebugLevel) 95 | } else { 96 | style := log.DefaultStyles() 97 | style.Levels[log.DebugLevel] = style.Levels[log.DebugLevel].SetString("●") 98 | style.Levels[log.InfoLevel] = style.Levels[log.InfoLevel].SetString("●") 99 | style.Levels[log.WarnLevel] = style.Levels[log.WarnLevel].SetString("●") 100 | style.Levels[log.ErrorLevel] = style.Levels[log.ErrorLevel].SetString("×") 101 | style.Levels[log.FatalLevel] = style.Levels[log.FatalLevel].SetString("×") 102 | log.SetStyles(style) 103 | log.SetReportTimestamp(false) 104 | log.SetLevel(log.InfoLevel) 105 | } 106 | } 107 | 108 | func initCommands() { 109 | cobra.EnableCommandSorting = false 110 | 111 | rootCmd.SetOut(os.Stdout) 112 | rootCmd.InitDefaultVersionFlag() 113 | rootCmd.Flags().SortFlags = false 114 | rootCmd.PersistentFlags().StringP("lang", "l", "", "language of code to generate: cpp, go, python ...") 115 | rootCmd.PersistentFlags().StringP("site", "", "", "leetcode site: cn, us") 116 | rootCmd.PersistentFlags().BoolP("yes", "y", false, "answer yes to all prompts") 117 | rootCmd.InitDefaultHelpFlag() 118 | _ = viper.BindPFlag("code.lang", rootCmd.PersistentFlags().Lookup("lang")) 119 | _ = viper.BindPFlag("leetcode.site", rootCmd.PersistentFlags().Lookup("site")) 120 | _ = viper.BindPFlag("yes", rootCmd.PersistentFlags().Lookup("yes")) 121 | 122 | _ = rootCmd.RegisterFlagCompletionFunc( 123 | "lang", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 124 | langs := make([]string, 0, len(lang.SupportedLangs)) 125 | for _, l := range lang.SupportedLangs { 126 | langs = append(langs, l.Slug()) 127 | } 128 | return langs, cobra.ShellCompDirectiveNoFileComp 129 | }, 130 | ) 131 | _ = rootCmd.RegisterFlagCompletionFunc( 132 | "site", 133 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 134 | return []string{"cn", "us"}, cobra.ShellCompDirectiveNoFileComp 135 | }, 136 | ) 137 | 138 | commands := []*cobra.Command{ 139 | initCmd, 140 | pickCmd, 141 | infoCmd, 142 | testCmd, 143 | submitCmd, 144 | fixCmd, 145 | editCmd, 146 | extractCmd, 147 | contestCmd, 148 | cacheCmd, 149 | debugCmd, 150 | gitCmd, 151 | inspectCmd, 152 | whoamiCmd, 153 | openCmd, 154 | } 155 | for _, cmd := range commands { 156 | cmd.Flags().SortFlags = false 157 | cmd.PersistentPreRunE = preRun 158 | rootCmd.AddCommand(cmd) 159 | } 160 | rootCmd.InitDefaultHelpCmd() 161 | rootCmd.CompletionOptions.HiddenDefaultCmd = true 162 | cc.Init( 163 | &cc.Config{ 164 | RootCmd: rootCmd, 165 | Headings: cc.HiCyan + cc.Bold + cc.Underline, 166 | Commands: cc.HiYellow + cc.Bold, 167 | Example: cc.Italic, 168 | ExecName: cc.Bold, 169 | Flags: cc.Bold, 170 | NoExtraNewlines: true, 171 | NoBottomNewline: true, 172 | }, 173 | ) 174 | } 175 | 176 | func init() { 177 | initCommands() 178 | } 179 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/j178/leetgo 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.7 7 | github.com/JohannesKaufmann/html-to-markdown v1.6.0 8 | github.com/PuerkitoBio/goquery v1.10.3 9 | github.com/avast/retry-go v3.0.0+incompatible 10 | github.com/briandowns/spinner v1.23.2 11 | github.com/charmbracelet/bubbles v0.21.0 12 | github.com/charmbracelet/bubbletea v1.3.6 13 | github.com/charmbracelet/glamour v0.10.0 14 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 15 | github.com/charmbracelet/log v0.4.2 16 | github.com/cli/browser v1.3.0 17 | github.com/dghubble/sling v1.4.2 18 | github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 19 | github.com/fatih/color v1.18.0 20 | github.com/goccy/go-json v0.10.5 21 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 22 | github.com/grokify/html-strip-tags-go v0.1.0 23 | github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b 24 | github.com/hexops/gotextdiff v1.0.3 25 | github.com/ivanpirog/coloredcobra v1.0.1 26 | github.com/j178/kooky v0.0.0-20241229062455-9bf54ed73002 27 | github.com/j178/leetgo/testutils/go v0.2.1 28 | github.com/jedib0t/go-pretty/v6 v6.7.7 29 | github.com/joho/godotenv v1.5.1 30 | github.com/k3a/html2text v1.2.1 31 | github.com/mitchellh/go-homedir v1.1.0 32 | github.com/muesli/reflow v0.3.0 33 | github.com/pelletier/go-toml/v2 v2.2.4 34 | github.com/sashabaranov/go-openai v1.40.5 35 | github.com/spf13/cobra v1.9.1 36 | github.com/spf13/viper v1.20.1 37 | github.com/tidwall/gjson v1.18.0 38 | gopkg.in/yaml.v3 v3.0.1 39 | zombiezen.com/go/sqlite v1.4.2 40 | ) 41 | 42 | require ( 43 | github.com/alecthomas/chroma/v2 v2.19.0 // indirect 44 | github.com/andybalholm/cascadia v1.3.3 // indirect 45 | github.com/atotto/clipboard v0.1.4 // indirect 46 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 47 | github.com/aymerick/douceur v0.2.0 // indirect 48 | github.com/bobesa/go-domain-util v0.0.0-20250410211237-17ab3b2f4a95 // indirect 49 | github.com/charmbracelet/colorprofile v0.3.1 // indirect 50 | github.com/charmbracelet/x/ansi v0.9.3 // indirect 51 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 52 | github.com/charmbracelet/x/exp/slice v0.0.0-20250711012602-b1f986320f7e // indirect 53 | github.com/charmbracelet/x/term v0.2.1 // indirect 54 | github.com/dlclark/regexp2 v1.11.5 // indirect 55 | github.com/dustin/go-humanize v1.0.1 // indirect 56 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 57 | github.com/fsnotify/fsnotify v1.9.0 // indirect 58 | github.com/go-ini/ini v1.67.0 // indirect 59 | github.com/go-logfmt/logfmt v0.6.0 // indirect 60 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 61 | github.com/go-sqlite/sqlite3 v0.0.0-20180313105335-53dd8e640ee7 // indirect 62 | github.com/go-viper/mapstructure/v2 v2.3.0 // indirect 63 | github.com/godbus/dbus/v5 v5.1.0 // indirect 64 | github.com/gonuts/binary v0.2.0 // indirect 65 | github.com/google/go-querystring v1.1.0 // indirect 66 | github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect 67 | github.com/google/uuid v1.6.0 // indirect 68 | github.com/gorilla/css v1.0.1 // indirect 69 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 70 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 71 | github.com/keybase/go-keychain v0.0.1 // indirect 72 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 73 | github.com/mattn/go-colorable v0.1.14 // indirect 74 | github.com/mattn/go-isatty v0.0.20 // indirect 75 | github.com/mattn/go-localereader v0.0.1 // indirect 76 | github.com/mattn/go-runewidth v0.0.16 // indirect 77 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 78 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 79 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 80 | github.com/muesli/cancelreader v0.2.2 // indirect 81 | github.com/muesli/termenv v0.16.0 // indirect 82 | github.com/ncruces/go-strftime v0.1.9 // indirect 83 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 84 | github.com/rivo/uniseg v0.4.7 // indirect 85 | github.com/sagikazarmark/locafero v0.9.0 // indirect 86 | github.com/sahilm/fuzzy v0.1.1 // indirect 87 | github.com/sourcegraph/conc v0.3.0 // indirect 88 | github.com/spf13/afero v1.14.0 // indirect 89 | github.com/spf13/cast v1.9.2 // indirect 90 | github.com/spf13/pflag v1.0.6 // indirect 91 | github.com/subosito/gotenv v1.6.0 // indirect 92 | github.com/tidwall/match v1.1.1 // indirect 93 | github.com/tidwall/pretty v1.2.1 // indirect 94 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 95 | github.com/yuin/goldmark v1.7.12 // indirect 96 | github.com/yuin/goldmark-emoji v1.0.6 // indirect 97 | github.com/zalando/go-keyring v0.2.6 // indirect 98 | go.uber.org/multierr v1.11.0 // indirect 99 | golang.org/x/crypto v0.40.0 // indirect 100 | golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect 101 | golang.org/x/net v0.42.0 // indirect 102 | golang.org/x/sync v0.16.0 // indirect 103 | golang.org/x/sys v0.34.0 // indirect 104 | golang.org/x/term v0.33.0 // indirect 105 | golang.org/x/text v0.27.0 // indirect 106 | gopkg.in/yaml.v2 v2.4.0 // indirect 107 | modernc.org/libc v1.66.3 // indirect 108 | modernc.org/mathutil v1.7.1 // indirect 109 | modernc.org/memory v1.11.0 // indirect 110 | modernc.org/sqlite v1.38.0 // indirect 111 | ) 112 | -------------------------------------------------------------------------------- /config/encoder.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "strings" 7 | 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | // Code below is copied from https://github.com/siderolabs/talos/blob/main/pkg/machinery/config/encoder/encoder.go 12 | // with some modifications. 13 | 14 | func isEmpty(value reflect.Value) bool { 15 | if !value.IsValid() { 16 | return true 17 | } 18 | 19 | //nolint:exhaustive 20 | switch value.Kind() { 21 | case reflect.Ptr: 22 | return value.IsNil() 23 | case reflect.Map: 24 | return len(value.MapKeys()) == 0 25 | case reflect.Slice: 26 | return value.Len() == 0 27 | default: 28 | return value.IsZero() 29 | } 30 | } 31 | 32 | func isNil(value reflect.Value) bool { 33 | if !value.IsValid() { 34 | return true 35 | } 36 | 37 | //nolint:exhaustive 38 | switch value.Kind() { 39 | case reflect.Ptr, reflect.Map, reflect.Slice: 40 | return value.IsNil() 41 | default: 42 | return false 43 | } 44 | } 45 | 46 | //nolint:gocyclo 47 | func toYamlNode(in interface{}) (*yaml.Node, error) { 48 | node := &yaml.Node{} 49 | 50 | // do not wrap yaml.Node into yaml.Node 51 | if n, ok := in.(*yaml.Node); ok { 52 | return n, nil 53 | } 54 | 55 | // if input implements yaml.Marshaler we should use that marshaller instead 56 | // same way as regular yaml marshal does 57 | if m, ok := in.(yaml.Marshaler); ok && !isNil(reflect.ValueOf(in)) { 58 | res, err := m.MarshalYAML() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if n, ok := res.(*yaml.Node); ok { 64 | return n, nil 65 | } 66 | 67 | in = res 68 | } 69 | 70 | v := reflect.ValueOf(in) 71 | if v.Kind() == reflect.Ptr { 72 | v = v.Elem() 73 | } 74 | 75 | //nolint:exhaustive 76 | switch v.Kind() { 77 | case reflect.Struct: 78 | node.Kind = yaml.MappingNode 79 | 80 | t := v.Type() 81 | 82 | for i := 0; i < v.NumField(); i++ { 83 | // skip unexported fields 84 | if !v.Field(i).CanInterface() { 85 | continue 86 | } 87 | 88 | comment := t.Field(i).Tag.Get("comment") 89 | tag := t.Field(i).Tag.Get("yaml") 90 | parts := strings.Split(tag, ",") 91 | fieldName := parts[0] 92 | parts = parts[1:] 93 | 94 | if fieldName == "" { 95 | fieldName = strings.ToLower(t.Field(i).Name) 96 | } 97 | 98 | if fieldName == "-" { 99 | continue 100 | } 101 | 102 | var ( 103 | empty = isEmpty(v.Field(i)) 104 | null = isNil(v.Field(i)) 105 | 106 | skip bool 107 | inline bool 108 | flow bool 109 | ) 110 | 111 | for _, part := range parts { 112 | if part == "omitempty" && empty { 113 | skip = true 114 | } 115 | 116 | if part == "omitonlyifnil" && !null { 117 | skip = false 118 | } 119 | 120 | if part == "inline" { 121 | inline = true 122 | } 123 | 124 | if part == "flow" { 125 | flow = true 126 | } 127 | } 128 | 129 | var value interface{} 130 | if v.Field(i).CanInterface() { 131 | value = v.Field(i).Interface() 132 | } 133 | 134 | if skip { 135 | continue 136 | } 137 | 138 | var style yaml.Style 139 | if flow { 140 | style |= yaml.FlowStyle 141 | } 142 | 143 | if inline { 144 | child, err := toYamlNode(value) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | if child.Kind == yaml.MappingNode || child.Kind == yaml.SequenceNode { 150 | appendNodes(node, child.Content...) 151 | } 152 | } else if err := addToMap(node, comment, fieldName, value, style); err != nil { 153 | return nil, err 154 | } 155 | } 156 | case reflect.Map: 157 | node.Kind = yaml.MappingNode 158 | keys := v.MapKeys() 159 | // always interate keys in alphabetical order to preserve the same output for maps 160 | sort.Slice( 161 | keys, func(i, j int) bool { 162 | return keys[i].String() < keys[j].String() 163 | }, 164 | ) 165 | 166 | for _, k := range keys { 167 | element := v.MapIndex(k) 168 | value := element.Interface() 169 | 170 | if err := addToMap(node, "", k.Interface(), value, 0); err != nil { 171 | return nil, err 172 | } 173 | } 174 | case reflect.Slice: 175 | node.Kind = yaml.SequenceNode 176 | nodes := make([]*yaml.Node, v.Len()) 177 | 178 | for i := 0; i < v.Len(); i++ { 179 | element := v.Index(i) 180 | 181 | var err error 182 | 183 | nodes[i], err = toYamlNode(element.Interface()) 184 | if err != nil { 185 | return nil, err 186 | } 187 | } 188 | appendNodes(node, nodes...) 189 | default: 190 | if err := node.Encode(in); err != nil { 191 | return nil, err 192 | } 193 | } 194 | 195 | return node, nil 196 | } 197 | 198 | func appendNodes(dest *yaml.Node, nodes ...*yaml.Node) { 199 | if dest.Content == nil { 200 | dest.Content = []*yaml.Node{} 201 | } 202 | 203 | dest.Content = append(dest.Content, nodes...) 204 | } 205 | 206 | func addToMap(dest *yaml.Node, comment string, fieldName, in interface{}, style yaml.Style) error { 207 | key, err := toYamlNode(fieldName) 208 | if err != nil { 209 | return err 210 | } 211 | key.HeadComment = comment 212 | 213 | value, err := toYamlNode(in) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | value.Style = style 219 | 220 | // override head comment with line comment for non-scalar nodes 221 | if value.Kind != yaml.ScalarNode { 222 | if key.HeadComment == "" { 223 | key.HeadComment = value.LineComment 224 | } 225 | value.LineComment = "" 226 | } 227 | 228 | appendNodes(dest, key, value) 229 | 230 | return nil 231 | } 232 | -------------------------------------------------------------------------------- /testutils/go/parse.go: -------------------------------------------------------------------------------- 1 | package goutils 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/goccy/go-json" 10 | ) 11 | 12 | // MustSplitArray is a wrapper of Deserialize which panics if an error occurs. 13 | func MustSplitArray(raw string) []string { 14 | splits, err := SplitArray(raw) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return splits 19 | } 20 | 21 | // SplitArray splits a comma separated array string which may contain different types into a slice of strings. 22 | func SplitArray(raw string) ([]string, error) { 23 | raw = strings.TrimSpace(raw) 24 | invalidErr := fmt.Errorf("invalid array: %s", raw) 25 | 26 | // check [] at leftmost and rightmost 27 | if len(raw) <= 1 || raw[0] != '[' || raw[len(raw)-1] != ']' { 28 | return nil, invalidErr 29 | } 30 | 31 | var splits []json.RawMessage 32 | if err := json.Unmarshal([]byte(raw), &splits); err != nil { 33 | return nil, invalidErr 34 | } 35 | res := make([]string, len(splits)) 36 | for i, v := range splits { 37 | res[i] = string(v) 38 | } 39 | return res, nil 40 | } 41 | 42 | // DeserializeValue deserialize a string to a reflect.Value 43 | func DeserializeValue(ty reflect.Type, raw string) (reflect.Value, error) { 44 | z := reflect.Value{} 45 | switch ty.Kind() { 46 | case reflect.Bool: 47 | if raw != "true" && raw != "false" { 48 | return z, fmt.Errorf("invalid bool: %s", raw) 49 | } 50 | b := raw == "true" 51 | return reflect.ValueOf(b), nil 52 | case reflect.Uint8: // byte 53 | if len(raw) != 3 || raw[0] != '"' && raw[0] != '\'' || raw[2] != raw[0] { 54 | return z, fmt.Errorf("invalid byte: %s", raw) 55 | } 56 | return reflect.ValueOf(raw[1]), nil 57 | case reflect.String: 58 | s, err := strconv.Unquote(raw) 59 | if err != nil { 60 | return z, fmt.Errorf("invalid string: %s", raw) 61 | } 62 | return reflect.ValueOf(s), nil 63 | case reflect.Int, reflect.Int32: 64 | i, err := strconv.Atoi(raw) 65 | if err != nil { 66 | return z, fmt.Errorf("invalid int: %s", raw) 67 | } 68 | return reflect.ValueOf(i), nil 69 | case reflect.Int64: 70 | i, err := strconv.ParseInt(raw, 10, 64) 71 | if err != nil { 72 | return z, fmt.Errorf("invalid int64: %s", raw) 73 | } 74 | return reflect.ValueOf(i), nil 75 | case reflect.Uint, reflect.Uint32: 76 | i, err := strconv.ParseUint(raw, 10, 32) 77 | if err != nil { 78 | return z, fmt.Errorf("invalid uint: %s", raw) 79 | } 80 | return reflect.ValueOf(uint(i)), nil 81 | case reflect.Uint64: 82 | i, err := strconv.ParseUint(raw, 10, 64) 83 | if err != nil { 84 | return z, fmt.Errorf("invalid uint64: %s", raw) 85 | } 86 | return reflect.ValueOf(i), nil 87 | case reflect.Float64: 88 | f, err := strconv.ParseFloat(raw, 64) 89 | if err != nil { 90 | return z, fmt.Errorf("invalid float64: %s", raw) 91 | } 92 | return reflect.ValueOf(f), nil 93 | case reflect.Slice: 94 | splits, err := SplitArray(raw) 95 | if err != nil { 96 | return z, fmt.Errorf("invalid array: %s", raw) 97 | } 98 | sl := reflect.MakeSlice(ty, 0, len(splits)) 99 | for _, s := range splits { 100 | e, err := DeserializeValue(ty.Elem(), s) 101 | if err != nil { 102 | return z, err 103 | } 104 | sl = reflect.Append(sl, e) 105 | } 106 | return sl, nil 107 | case reflect.Ptr: 108 | switch ty.Elem().Name() { 109 | case "TreeNode": 110 | root, err := DeserializeTreeNode(raw) 111 | if err != nil { 112 | return z, err 113 | } 114 | return reflect.ValueOf(root), nil 115 | case "ListNode": 116 | head, err := DeserializeListNode(raw) 117 | if err != nil { 118 | return z, err 119 | } 120 | return reflect.ValueOf(head), nil 121 | } 122 | } 123 | return z, fmt.Errorf("unknown type %s", ty.String()) 124 | } 125 | 126 | // Deserialize deserialize a string to a type. 127 | func Deserialize[T any](raw string) T { 128 | raw = strings.TrimSpace(raw) 129 | var z T 130 | ty := reflect.TypeOf(z) 131 | v, err := DeserializeValue(ty, raw) 132 | if err != nil { 133 | panic(fmt.Errorf("deserialize failed: %w", err)) 134 | } 135 | rv := reflect.ValueOf(&z) 136 | rv.Elem().Set(v) 137 | return z 138 | } 139 | 140 | func serialize(v reflect.Value) (string, error) { 141 | switch v.Kind() { 142 | case reflect.Slice: 143 | sb := &strings.Builder{} 144 | sb.WriteByte('[') 145 | for i := 0; i < v.Len(); i++ { 146 | if i > 0 { 147 | sb.WriteByte(',') 148 | } 149 | _s, err := serialize(v.Index(i)) 150 | if err != nil { 151 | return "", err 152 | } 153 | sb.WriteString(_s) 154 | } 155 | sb.WriteByte(']') 156 | return sb.String(), nil 157 | case reflect.Ptr: // *TreeNode, *ListNode 158 | switch tpName := v.Type().Elem().Name(); tpName { 159 | case "TreeNode": 160 | return v.Interface().(*TreeNode).String(), nil 161 | case "ListNode": 162 | return v.Interface().(*ListNode).String(), nil 163 | default: 164 | return "", fmt.Errorf("unknown type %s", tpName) 165 | } 166 | case reflect.String: 167 | return fmt.Sprintf(`"%s"`, v.Interface()), nil 168 | case reflect.Uint8: // byte 169 | return fmt.Sprintf(`"%c"`, v.Interface()), nil 170 | case reflect.Float64: 171 | return fmt.Sprintf(`%.5f`, v.Interface()), nil 172 | default: // int uint int64 uint64 bool 173 | return fmt.Sprintf(`%v`, v.Interface()), nil 174 | } 175 | } 176 | 177 | // Serialize serialize a value to a string. 178 | func Serialize(v any) string { 179 | vt := reflect.ValueOf(v) 180 | s, err := serialize(vt) 181 | if err != nil { 182 | panic(fmt.Errorf("serialize failed: %w", err)) 183 | } 184 | return s 185 | } 186 | -------------------------------------------------------------------------------- /lang/judge.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/goccy/go-json" 11 | strip "github.com/grokify/html-strip-tags-go" 12 | 13 | "github.com/j178/leetgo/leetcode" 14 | goutils "github.com/j178/leetgo/testutils/go" 15 | ) 16 | 17 | type JudgeResult interface { 18 | IsAccepted() bool 19 | GetInfo() string 20 | } 21 | 22 | type simpleResult struct { 23 | accepted bool 24 | info string 25 | } 26 | 27 | func failed(info string) JudgeResult { 28 | return simpleResult{false, info} 29 | } 30 | 31 | func accepted() JudgeResult { 32 | return simpleResult{true, ""} 33 | } 34 | 35 | func (r simpleResult) IsAccepted() bool { 36 | return r.accepted 37 | } 38 | 39 | func (r simpleResult) GetInfo() string { 40 | return r.info 41 | } 42 | 43 | type Judger interface { 44 | Judge(input []string, output, actualOutput string) JudgeResult 45 | } 46 | 47 | type stringJudger struct{} 48 | 49 | func (stringJudger) Judge(input []string, output, actualOutput string) JudgeResult { 50 | if output != actualOutput { 51 | return failed(fmt.Sprintf("expected %q, got %q", output, actualOutput)) 52 | } 53 | return accepted() 54 | } 55 | 56 | type sliceJudger struct { 57 | ignoreOrder bool 58 | subJudger Judger 59 | } 60 | 61 | func newSliceJudger(ignoreOrder bool, subJudger Judger) *sliceJudger { 62 | return &sliceJudger{ignoreOrder, subJudger} 63 | } 64 | 65 | func (j *sliceJudger) Judge(input []string, output, actualOutput string) JudgeResult { 66 | if output == actualOutput { 67 | return accepted() 68 | } 69 | 70 | a, err1 := goutils.SplitArray(output) 71 | b, err2 := goutils.SplitArray(actualOutput) 72 | if err1 != nil || err2 != nil { 73 | return failed(fmt.Sprintf("expected %q, got %q", output, actualOutput)) 74 | } 75 | if len(a) != len(b) { 76 | return failed(fmt.Sprintf("expected %d elements, got %d", len(a), len(b))) 77 | } 78 | 79 | if j.ignoreOrder { 80 | if !j.compareIgnoringOrder(a, b) { 81 | return failed(fmt.Sprintf("expected %q, got %q", output, actualOutput)) 82 | } 83 | return accepted() 84 | } 85 | 86 | for i := range a { 87 | if r := j.subJudger.Judge(input, a[i], b[i]); !r.IsAccepted() { 88 | rr := failed(r.GetInfo() + " at index " + strconv.Itoa(i)) 89 | return rr 90 | } 91 | } 92 | return accepted() 93 | } 94 | 95 | var anyOrderRe = regexp.MustCompile(`(?i)return.* in any order`) 96 | 97 | // TODO improve the detection of "any order" 98 | func shouldIgnoreOrder(q *leetcode.QuestionData) bool { 99 | content := strip.StripTags(q.Content) 100 | if anyOrderRe.MatchString(content) { 101 | return true 102 | } 103 | 104 | // try translated content 105 | content = strip.StripTags(q.TranslatedContent) 106 | // nolint: gosimple 107 | if strings.Contains(content, "任意顺序返回答案") { 108 | return true 109 | } 110 | return false 111 | } 112 | 113 | func (j *sliceJudger) compareIgnoringOrder(actual, expected []string) bool { 114 | cnt := map[string]int{} 115 | for _, v := range actual { 116 | cnt[v]++ 117 | } 118 | for _, v := range expected { 119 | cnt[v]-- 120 | if cnt[v] < 0 { 121 | return false 122 | } 123 | } 124 | for _, v := range cnt { 125 | if v != 0 { 126 | return false 127 | } 128 | } 129 | return true 130 | } 131 | 132 | type floatJudger struct{} 133 | 134 | func (floatJudger) Judge(input []string, output, actualOutput string) JudgeResult { 135 | a, _ := strconv.ParseFloat(output, 64) 136 | b, _ := strconv.ParseFloat(actualOutput, 64) 137 | if math.Abs(a-b) >= 1e-5 { 138 | return failed(fmt.Sprintf("expected %f, got %f", a, b)) 139 | } 140 | return accepted() 141 | } 142 | 143 | type systemDesignJudger struct { 144 | judgers map[string]Judger 145 | } 146 | 147 | func newSystemDesignJudger(q *leetcode.QuestionData) *systemDesignJudger { 148 | judgers := map[string]Judger{} 149 | for _, m := range q.MetaData.Methods { 150 | // NOTE: if two functions both return a slice, we can't distinguish them 151 | // We just compare both function results ignoring order. 152 | judgers[m.Name] = getJudger(q, m.Return.Type, true) 153 | } 154 | return &systemDesignJudger{judgers} 155 | } 156 | 157 | func (s systemDesignJudger) Judge(input []string, output, actualOutput string) JudgeResult { 158 | // First line of the input is the function names 159 | var funcs []string 160 | _ = json.Unmarshal([]byte(input[0]), &funcs) 161 | inputs, _ := goutils.SplitArray(input[1]) 162 | a, _ := goutils.SplitArray(output) 163 | b, _ := goutils.SplitArray(actualOutput) 164 | 165 | if len(a) != len(b) || len(a) != len(funcs) { 166 | panic("system design input/output not match") 167 | } 168 | 169 | // i == 0 is the constructor, its output is always "null", skip it. 170 | for i := 1; i < len(a); i++ { 171 | judger := s.judgers[funcs[i]] 172 | if judger == nil { 173 | panic(fmt.Sprintf("judger for %s not found", funcs[i])) 174 | } 175 | if r := judger.Judge(input, a[i], b[i]); !r.IsAccepted() { 176 | param := inputs[i][1 : len(inputs[i])-1] // remove [] 177 | rr := failed(fmt.Sprintf("%s at index %d [%s(%s)]", r.GetInfo(), i, funcs[i], param)) 178 | return rr 179 | } 180 | } 181 | return accepted() 182 | } 183 | 184 | func GetJudger(q *leetcode.QuestionData) Judger { 185 | if q.MetaData.SystemDesign { 186 | return newSystemDesignJudger(q) 187 | } 188 | resultType := q.MetaData.ResultType() 189 | return getJudger(q, resultType, true) 190 | } 191 | 192 | func getJudger(q *leetcode.QuestionData, tp string, topLevel bool) Judger { 193 | switch tp { 194 | case "double": 195 | return floatJudger{} 196 | case "string": 197 | return stringJudger{} 198 | default: 199 | if strings.HasSuffix(tp, "[]") { 200 | // Support top-level slice out-of-order comparison only. 201 | ignoreOrder := topLevel && shouldIgnoreOrder(q) 202 | subJudger := getJudger(q, tp[:len(tp)-2], false) 203 | return newSliceJudger(ignoreOrder, subJudger) 204 | } 205 | // void, bool, int, long, TreeNode, etc. 206 | return stringJudger{} 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /testutils/go/predefined.go: -------------------------------------------------------------------------------- 1 | package goutils 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var ErrInfiniteLoop = errors.New("infinite loop detected") 11 | 12 | /* 13 | Much appreciated to EndlessCheng 14 | Adapted from https://github.com/EndlessCheng/codeforces-go/blob/ae5b312f3f/leetcode/testutil/leetcode.go 15 | */ 16 | 17 | type ListNode struct { 18 | Val int 19 | Next *ListNode 20 | } 21 | 22 | func DeserializeListNode(s string) (*ListNode, error) { 23 | var res []*int 24 | err := json.Unmarshal([]byte(s), &res) 25 | if err != nil { 26 | return nil, err 27 | } 28 | if len(res) == 0 { 29 | return nil, nil 30 | } 31 | root := &ListNode{} 32 | n := root 33 | for i := 0; i < len(res)-1; i++ { 34 | n.Val = *res[i] 35 | n.Next = &ListNode{} 36 | n = n.Next 37 | } 38 | n.Val = *res[len(res)-1] 39 | return root, nil 40 | } 41 | 42 | // ToString is deprecated, use String() 43 | func (l *ListNode) ToString() string { 44 | return l.String() 45 | } 46 | 47 | // String returns a string representation of the linked list. 48 | // It panics with ErrInfiniteLoop if a cycle is detected. 49 | func (l *ListNode) String() string { 50 | seen := make(map[*ListNode]bool, 10) 51 | 52 | sb := &strings.Builder{} 53 | sb.WriteByte('[') 54 | for ; l != nil; l = l.Next { 55 | if sb.Len() > 1 { 56 | sb.WriteByte(',') 57 | } 58 | sb.WriteString(strconv.Itoa(l.Val)) 59 | 60 | if seen[l] { 61 | panic(ErrInfiniteLoop) 62 | } 63 | seen[l] = true 64 | } 65 | sb.WriteByte(']') 66 | return sb.String() 67 | } 68 | 69 | func (l *ListNode) Values() []int { 70 | vals := []int{} 71 | for ; l != nil; l = l.Next { 72 | vals = append(vals, l.Val) 73 | } 74 | return vals 75 | } 76 | 77 | func (l *ListNode) Nodes() []*ListNode { 78 | nodes := []*ListNode{} 79 | for ; l != nil; l = l.Next { 80 | nodes = append(nodes, l) 81 | } 82 | return nodes 83 | } 84 | 85 | type TreeNode struct { 86 | Val int 87 | Left *TreeNode 88 | Right *TreeNode 89 | } 90 | 91 | func DeserializeTreeNode(s string) (*TreeNode, error) { 92 | var res []*int 93 | err := json.Unmarshal([]byte(s), &res) 94 | if err != nil { 95 | return nil, err 96 | } 97 | if len(res) == 0 { 98 | return nil, nil 99 | } 100 | nodes := make([]*TreeNode, len(res)) 101 | for i := 0; i < len(res); i++ { 102 | if res[i] != nil { 103 | nodes[i] = &TreeNode{Val: *res[i]} 104 | } 105 | } 106 | root := nodes[0] 107 | for i, j := 0, 1; j < len(res); i++ { 108 | if nodes[i] != nil { 109 | nodes[i].Left = nodes[j] 110 | j++ 111 | if j >= len(res) { 112 | break 113 | } 114 | nodes[i].Right = nodes[j] 115 | j++ 116 | if j >= len(res) { 117 | break 118 | } 119 | } 120 | } 121 | return root, nil 122 | } 123 | 124 | // ToString is deprecated, use String() 125 | func (t *TreeNode) ToString() string { 126 | return t.String() 127 | } 128 | 129 | // String returns a string representation of the binary tree. 130 | // It panics with ErrInfiniteLoop if a cycle is detected. 131 | func (t *TreeNode) String() string { 132 | nodes := []*TreeNode{} 133 | queue := []*TreeNode{t} 134 | seen := make(map[*TreeNode]bool, 10) 135 | for len(queue) > 0 { 136 | t, queue = queue[0], queue[1:] 137 | nodes = append(nodes, t) 138 | if t != nil { 139 | if seen[t] { 140 | panic(ErrInfiniteLoop) 141 | } 142 | seen[t] = true 143 | 144 | queue = append(queue, t.Left, t.Right) 145 | } 146 | } 147 | 148 | for len(nodes) > 0 && nodes[len(nodes)-1] == nil { 149 | nodes = nodes[:len(nodes)-1] 150 | } 151 | 152 | sb := &strings.Builder{} 153 | sb.WriteByte('[') 154 | for _, node := range nodes { 155 | if sb.Len() > 1 { 156 | sb.WriteByte(',') 157 | } 158 | if node != nil { 159 | sb.WriteString(strconv.Itoa(node.Val)) 160 | } else { 161 | sb.WriteString("null") 162 | } 163 | } 164 | sb.WriteByte(']') 165 | return sb.String() 166 | } 167 | 168 | type NaryTreeNode struct { 169 | Val int 170 | Children []*NaryTreeNode 171 | } 172 | 173 | func DeserializeNaryTreeNode(s string) (*NaryTreeNode, error) { 174 | var res []*int 175 | if err := json.Unmarshal([]byte(s), &res); err != nil { 176 | return nil, err 177 | } 178 | if len(res) == 0 { 179 | return nil, nil 180 | } 181 | // 用一个伪的头结点 182 | root := &NaryTreeNode{} 183 | q := []*NaryTreeNode{root} 184 | for i := 0; i < len(res); i++ { 185 | node := q[0] 186 | q = q[1:] 187 | for ; i < len(res) && res[i] != nil; i++ { 188 | n := &NaryTreeNode{Val: *res[i]} 189 | node.Children = append(node.Children, n) 190 | q = append(q, n) 191 | } 192 | } 193 | 194 | return root.Children[0], nil 195 | } 196 | 197 | // ToString is deprecated, use String 198 | func (t *NaryTreeNode) ToString() string { 199 | return t.String() 200 | } 201 | 202 | // String returns a string representation of the nary tree. 203 | // It panics with ErrInfiniteLoop if a cycle is detected. 204 | func (t *NaryTreeNode) String() string { 205 | nodes := []*NaryTreeNode{} 206 | q := []*NaryTreeNode{{Children: []*NaryTreeNode{t}}} 207 | seen := make(map[*NaryTreeNode]bool, 10) 208 | 209 | for len(q) > 0 { 210 | node := q[0] 211 | q = q[1:] 212 | nodes = append(nodes, node) 213 | 214 | if node != nil { 215 | if seen[node] { 216 | panic(ErrInfiniteLoop) 217 | } 218 | seen[node] = true 219 | 220 | if len(node.Children) > 0 { 221 | q = append(q, node.Children...) 222 | } 223 | q = append(q, nil) 224 | } 225 | } 226 | // 去除头结点 227 | nodes = nodes[1:] 228 | // 去除末尾的 null 229 | for len(nodes) > 0 && nodes[len(nodes)-1] == nil { 230 | nodes = nodes[:len(nodes)-1] 231 | } 232 | 233 | sb := strings.Builder{} 234 | sb.WriteByte('[') 235 | for _, node := range nodes { 236 | if sb.Len() > 1 { 237 | sb.WriteByte(',') 238 | } 239 | if node == nil { 240 | sb.WriteString("null") 241 | } else { 242 | sb.WriteString(strconv.Itoa(node.Val)) 243 | } 244 | } 245 | sb.WriteByte(']') 246 | return sb.String() 247 | } 248 | 249 | type Node struct { 250 | Val int 251 | Prev *Node 252 | Next *Node 253 | Child *Node 254 | } 255 | -------------------------------------------------------------------------------- /lang/list.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | var ( 4 | golangGen = golang{ 5 | baseLang{ 6 | name: "Go", 7 | slug: "golang", 8 | shortName: "go", 9 | extension: ".go", 10 | lineComment: "//", 11 | blockCommentStart: "/*", 12 | blockCommentEnd: "*/", 13 | }, 14 | } 15 | python3Gen = python{ 16 | baseLang{ 17 | name: "Python", 18 | slug: "python3", 19 | shortName: "py", 20 | extension: ".py", 21 | lineComment: "#", 22 | blockCommentStart: `"""`, 23 | blockCommentEnd: `"""`, 24 | }, 25 | } 26 | pandasGen = baseLang{ 27 | name: "Pandas", 28 | slug: "pythondata", 29 | shortName: "py", 30 | extension: ".py", 31 | lineComment: "#", 32 | blockCommentStart: `"""`, 33 | blockCommentEnd: `"""`, 34 | } 35 | cppGen = cpp{ 36 | baseLang{ 37 | name: "C++", 38 | slug: "cpp", 39 | shortName: "cpp", 40 | extension: ".cpp", 41 | lineComment: "//", 42 | blockCommentStart: "/*", 43 | blockCommentEnd: "*/", 44 | }, 45 | } 46 | rustGen = rust{ 47 | baseLang{ 48 | name: "Rust", 49 | slug: "rust", 50 | shortName: "rs", 51 | extension: ".rs", 52 | lineComment: "//", 53 | blockCommentStart: "/*", 54 | blockCommentEnd: "*/", 55 | }, 56 | } 57 | javaGen = baseLang{ 58 | name: "Java", 59 | slug: "java", 60 | shortName: "java", 61 | extension: ".java", 62 | lineComment: "//", 63 | blockCommentStart: "/*", 64 | blockCommentEnd: "*/", 65 | } 66 | cGen = baseLang{ 67 | name: "C", 68 | slug: "c", 69 | shortName: "c", 70 | extension: ".c", 71 | lineComment: "//", 72 | blockCommentStart: "/*", 73 | blockCommentEnd: "*/", 74 | } 75 | csharpGen = baseLang{ 76 | name: "C#", 77 | slug: "csharp", 78 | shortName: "cs", 79 | extension: ".cs", 80 | lineComment: "//", 81 | blockCommentStart: "/*", 82 | blockCommentEnd: "*/", 83 | } 84 | jsGen = baseLang{ 85 | name: "JavaScript", 86 | slug: "javascript", 87 | shortName: "js", 88 | extension: ".js", 89 | lineComment: "//", 90 | blockCommentStart: "/*", 91 | blockCommentEnd: "*/", 92 | } 93 | tsGen = baseLang{ 94 | name: "TypeScript", 95 | slug: "typescript", 96 | shortName: "ts", 97 | extension: ".ts", 98 | lineComment: "//", 99 | blockCommentStart: "/*", 100 | blockCommentEnd: "*/", 101 | } 102 | phpGen = baseLang{ 103 | name: "PHP", 104 | slug: "php", 105 | shortName: "php", 106 | extension: ".php", 107 | lineComment: "//", 108 | blockCommentStart: "/*", 109 | blockCommentEnd: "*/", 110 | } 111 | rubyGen = baseLang{ 112 | name: "Ruby", 113 | slug: "ruby", 114 | shortName: "rb", 115 | extension: ".rb", 116 | lineComment: "#", 117 | blockCommentStart: "=begin", 118 | blockCommentEnd: "=end", 119 | } 120 | swiftGen = baseLang{ 121 | name: "Swift", 122 | slug: "swift", 123 | shortName: "swift", 124 | extension: ".swift", 125 | lineComment: "//", 126 | blockCommentStart: "/*", 127 | blockCommentEnd: "*/", 128 | } 129 | kotlinGen = baseLang{ 130 | name: "Kotlin", 131 | slug: "kotlin", 132 | shortName: "kt", 133 | extension: ".kt", 134 | lineComment: "//", 135 | blockCommentStart: "/*", 136 | blockCommentEnd: "*/", 137 | } 138 | mysqlGen = baseLang{ 139 | name: "MySQL", 140 | slug: "mysql", 141 | shortName: "sql", 142 | extension: ".sql", 143 | lineComment: "--", 144 | blockCommentStart: "/*", 145 | blockCommentEnd: "*/", 146 | } 147 | mssqlGen = baseLang{ 148 | name: "MSSQL", 149 | slug: "mssql", 150 | shortName: "sql", 151 | extension: ".sql", 152 | lineComment: "--", 153 | blockCommentStart: "/*", 154 | blockCommentEnd: "*/", 155 | } 156 | oraclesqlGen = baseLang{ 157 | name: "Oracle", 158 | slug: "oraclesql", 159 | shortName: "sql", 160 | extension: ".sql", 161 | lineComment: "--", 162 | blockCommentStart: "/*", 163 | blockCommentEnd: "*/", 164 | } 165 | bashGen = baseLang{ 166 | name: "Bash", 167 | slug: "bash", 168 | shortName: "sh", 169 | extension: ".sh", 170 | lineComment: "#", 171 | blockCommentStart: ">>COMMENT", 172 | blockCommentEnd: "\nCOMMENT", 173 | } 174 | erlangGen = baseLang{ 175 | name: "Erlang", 176 | slug: "erlang", 177 | shortName: "erl", 178 | extension: ".erl", 179 | lineComment: "%", 180 | // TODO erlang does not support multiline comments really 181 | blockCommentStart: "%%%", 182 | blockCommentEnd: "%%%", 183 | } 184 | racketGen = baseLang{ 185 | name: "Racket", 186 | slug: "racket", 187 | shortName: "rkt", 188 | extension: ".rkt", 189 | lineComment: ";", 190 | blockCommentStart: "#|", 191 | blockCommentEnd: "|#", 192 | } 193 | scalaGen = baseLang{ 194 | name: "Scala", 195 | slug: "scala", 196 | shortName: "scala", 197 | extension: ".scala", 198 | lineComment: "//", 199 | blockCommentStart: "/*", 200 | blockCommentEnd: "*/", 201 | } 202 | elixirGen = baseLang{ 203 | name: "Elixir", 204 | slug: "elixir", 205 | shortName: "exs", 206 | extension: ".exs", 207 | lineComment: "#", 208 | blockCommentStart: `"""`, 209 | blockCommentEnd: `"""`, 210 | } 211 | dartGen = baseLang{ 212 | name: "Dart", 213 | slug: "dart", 214 | shortName: "dart", 215 | extension: ".dart", 216 | lineComment: "//", 217 | blockCommentStart: "/*", 218 | blockCommentEnd: "*/", 219 | } 220 | 221 | SupportedLangs = []Lang{ 222 | golangGen, 223 | python3Gen, 224 | pandasGen, 225 | cppGen, 226 | rustGen, 227 | javaGen, 228 | jsGen, 229 | tsGen, 230 | phpGen, 231 | cGen, 232 | csharpGen, 233 | rubyGen, 234 | swiftGen, 235 | kotlinGen, 236 | bashGen, 237 | mysqlGen, 238 | mssqlGen, 239 | oraclesqlGen, 240 | erlangGen, 241 | racketGen, 242 | scalaGen, 243 | elixirGen, 244 | dartGen, 245 | } 246 | ) 247 | -------------------------------------------------------------------------------- /testutils/cpp/LC_IO.h: -------------------------------------------------------------------------------- 1 | #ifndef LC_IO_H 2 | #define LC_IO_H 3 | 4 | #include 5 | #include 6 | 7 | /** 8 | * Definition for a singly-linked list. 9 | */ 10 | struct ListNode { 11 | int val; 12 | ListNode *next; 13 | ListNode() : val(0), next(nullptr) {} 14 | ListNode(int x) : val(x), next(nullptr) {} 15 | ListNode(int x, ListNode *next) : val(x), next(next) {} 16 | }; 17 | 18 | /** 19 | * Definition for a binary tree node. 20 | */ 21 | struct TreeNode { 22 | int val; 23 | TreeNode *left; 24 | TreeNode *right; 25 | TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} 26 | TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} 27 | }; 28 | 29 | namespace LeetCodeIO { 30 | namespace Helper { 31 | /** 32 | * Function for deserializing a singly-linked list. 33 | */ 34 | inline void scan_list(std::istream &is, ListNode *&node) { 35 | node = nullptr; 36 | ListNode *now = nullptr; 37 | [[maybe_unused]] 38 | L0: is.ignore(); 39 | L1: switch (is.peek()) { 40 | case ' ': 41 | case ',': is.ignore(); goto L1; 42 | case ']': is.ignore(); goto L2; 43 | default : int x; is >> x; 44 | now = (now ? now->next : node) = new ListNode(x); 45 | goto L1; 46 | } 47 | L2: return; 48 | } 49 | 50 | /** 51 | * Function for deserializing a binary tree. 52 | */ 53 | inline void scan_tree(std::istream &is, TreeNode *&node) { 54 | std::deque dq; 55 | [[maybe_unused]] 56 | L0: is.ignore(); 57 | L1: switch (is.peek()) { 58 | case ' ': 59 | case ',': is.ignore(); goto L1; 60 | case 'n': is.ignore(4); dq.emplace_back(nullptr); 61 | goto L1; 62 | case ']': is.ignore(); goto L2; 63 | default : int x; is >> x; 64 | dq.emplace_back(new TreeNode(x)); 65 | goto L1; 66 | } 67 | L2: int n = dq.size(); 68 | for (int i = 0, j = 1; i < n; ++i) { 69 | auto root = dq[i]; 70 | if (root == nullptr) { continue; } 71 | root->left = j < n ? dq[j] : nullptr; 72 | root->right = j + 1 < n ? dq[j + 1] : nullptr; 73 | j += 2; 74 | } 75 | node = n ? dq[0] : nullptr; 76 | return; 77 | } 78 | 79 | /** 80 | * Function for serializing a singly-linked list. 81 | */ 82 | inline void print_list(std::ostream &os, ListNode *node) { 83 | os.put('['); 84 | if (node != nullptr) { 85 | do { 86 | os << node->val; os.put(','); 87 | node = node->next; 88 | } while(node != nullptr); 89 | os.seekp(-1, std::ios_base::end); 90 | } 91 | os.put(']'); 92 | return; 93 | } 94 | 95 | /** 96 | * Function for serializing a binary tree. 97 | */ 98 | inline void print_tree(std::ostream &os, TreeNode *node) { 99 | std::queue q; 100 | int cnt_not_null_nodes = 0; 101 | auto push = [&](TreeNode *node) { 102 | q.emplace(node); 103 | if (node != nullptr) { ++cnt_not_null_nodes; } 104 | }; 105 | auto pop = [&]() { 106 | auto front = q.front(); q.pop(); 107 | if (front != nullptr) { 108 | --cnt_not_null_nodes; 109 | push(front->left); 110 | push(front->right); 111 | os << front->val; os.put(','); 112 | } else { 113 | os << "null,"; 114 | } 115 | }; 116 | os.put('['); 117 | if (node != nullptr) { 118 | push(node); 119 | while (cnt_not_null_nodes > 0) { pop(); } 120 | os.seekp(-1, std::ios_base::end); 121 | } 122 | os.put(']'); 123 | return; 124 | } 125 | } 126 | 127 | /** 128 | * Function for scanning a variable. 129 | */ 130 | template 131 | void scan(std::istream &is, T &x) { 132 | /** 133 | * operator >> discards leading whitespaces by default 134 | * when not using operator >>, they must be discarded explicitly 135 | */ 136 | if constexpr (std::is_same_v) { 137 | is >> std::quoted(x); 138 | } else if constexpr (std::is_same_v) { 139 | is >> std::ws; x = is.get() == 't'; is.ignore(4 - x); 140 | } else if constexpr (std::is_same_v) { 141 | is >> std::ws; is.ignore(); x = is.get(); is.ignore(); 142 | } else if constexpr (std::is_same_v) { 143 | is >> std::ws; Helper::scan_list(is, x); 144 | } else if constexpr (std::is_same_v) { 145 | is >> std::ws; Helper::scan_tree(is, x); 146 | } else { 147 | is >> x; 148 | } 149 | } 150 | 151 | /** 152 | * Function for deserializing an array. 153 | */ 154 | template 155 | void scan(std::istream &is, std::vector &v) { 156 | [[maybe_unused]] 157 | L0: is >> std::ws; 158 | is.ignore(); 159 | L1: switch (is.peek()) { 160 | case ' ': 161 | case ',': is.ignore(); goto L1; 162 | case ']': is.ignore(); goto L2; 163 | default : v.emplace_back(); 164 | scan(is, v.back()); 165 | goto L1; 166 | } 167 | L2: return; 168 | } 169 | 170 | /** 171 | * Function for printing a variable. 172 | */ 173 | template 174 | void print(std::ostream &os, const T& x) { 175 | if constexpr (std::is_same_v) { 176 | os.put('"'); os << x; os.put('"'); 177 | } else if constexpr (std::is_same_v) { 178 | constexpr int siz = 320; 179 | char buf[siz]; snprintf(buf, siz, "%.5f", x); os << buf; 180 | } else if constexpr (std::is_same_v) { 181 | static const char tab[2][8] = {"false", "true"}; 182 | os.write(tab[x], x ? 4 : 5); 183 | } else if constexpr (std::is_same_v) { 184 | os.put('"'); os.put(x); os.put('"'); 185 | } else if constexpr (std::is_same_v) { 186 | Helper::print_list(os, x); 187 | } else if constexpr (std::is_same_v) { 188 | Helper::print_tree(os, x); 189 | } else { 190 | os << x; 191 | } 192 | } 193 | 194 | /** 195 | * Function for serializing an array. 196 | */ 197 | template 198 | void print(std::ostream &os, const std::vector &v) { 199 | os.put('['); 200 | for (auto &&x : v) { 201 | print(os, x); 202 | os.put(','); 203 | } 204 | os.seekp(-!v.empty(), std::ios_base::end); 205 | os.put(']'); 206 | } 207 | }; 208 | 209 | #endif 210 | -------------------------------------------------------------------------------- /cmd/test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "time" 8 | 9 | "github.com/briandowns/spinner" 10 | "github.com/charmbracelet/log" 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/j178/leetgo/config" 14 | "github.com/j178/leetgo/lang" 15 | "github.com/j178/leetgo/leetcode" 16 | "github.com/j178/leetgo/utils" 17 | ) 18 | 19 | var ( 20 | runLocally bool 21 | runRemotely bool = true 22 | runBoth bool 23 | autoSubmit bool 24 | targetCase string 25 | forceSubmit bool 26 | ) 27 | 28 | func init() { 29 | testCmd.Flags().BoolVarP( 30 | &runLocally, 31 | "local", 32 | "L", 33 | false, 34 | "run test locally", 35 | ) 36 | testCmd.Flags().BoolVarP( 37 | &runBoth, 38 | "both", 39 | "B", 40 | false, 41 | "run test both locally and remotely", 42 | ) 43 | testCmd.Flags().BoolVarP(&autoSubmit, "submit", "s", false, "auto submit if all tests passed") 44 | testCmd.Flags().BoolVarP(&forceSubmit, "force", "f", false, "force submit even if local test failed") 45 | testCmd.Flags().StringVarP(&targetCase, "target", "t", "-", "only run the specified test case, e.g. 1, 1-3, -1, 1-") 46 | } 47 | 48 | var testCmd = &cobra.Command{ 49 | Use: "test qid", 50 | Aliases: []string{"t"}, 51 | Args: cobra.ExactArgs(1), 52 | ValidArgs: []string{"today", "last", "last/"}, 53 | Short: "Run question test cases", 54 | Example: `leetgo test 244 55 | leetgo test last 56 | leetgo test w330/1 57 | leetgo test w330/`, 58 | RunE: func(cmd *cobra.Command, args []string) error { 59 | if runLocally { 60 | runRemotely = false 61 | } 62 | if runBoth { 63 | runLocally = true 64 | runRemotely = true 65 | } 66 | 67 | cfg := config.Get() 68 | c := leetcode.NewClient(leetcode.ReadCredentials()) 69 | qs, err := leetcode.ParseQID(args[0], c) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | gen, err := lang.GetGenerator(cfg.Code.Lang) 75 | if err != nil { 76 | return err 77 | } 78 | _, supportLocalTest := gen.(lang.LocalTestable) 79 | if runLocally && !supportLocalTest { 80 | return fmt.Errorf("local test not supported for %s", cfg.Code.Lang) 81 | } 82 | 83 | user, err := c.GetUserStatus() 84 | if err != nil { 85 | user = &leetcode.UserStatus{} 86 | } 87 | testLimiter := newLimiter(user) 88 | submitLimiter := newLimiter(user) 89 | 90 | var hasFailedCase bool 91 | var hasSubmitted bool 92 | for _, q := range qs { 93 | var ( 94 | localPassed = true 95 | remotePassed = true 96 | submitAccepted = true 97 | ) 98 | if runLocally { 99 | log.Info("running test locally", "question", q.TitleSlug) 100 | localPassed, err = lang.RunLocalTest(q, targetCase) 101 | if err != nil { 102 | log.Error("failed to run test locally", "err", err) 103 | } 104 | } 105 | if runRemotely { 106 | log.Info("running test remotely", "question", q.TitleSlug) 107 | result, err := runTestRemotely(cmd, q, c, gen, testLimiter) 108 | if err != nil { 109 | log.Error("failed to run test remotely", "err", err) 110 | remotePassed = false 111 | } else { 112 | cmd.Print(result.Display(q)) 113 | remotePassed = result.CorrectAnswer 114 | } 115 | } 116 | 117 | if autoSubmit && remotePassed && (localPassed || forceSubmit) { 118 | log.Info("submitting solution", "user", user.Whoami(c)) 119 | hasSubmitted = true 120 | result, err := submitSolution(cmd, q, c, gen, submitLimiter) 121 | if err != nil { 122 | submitAccepted = false 123 | log.Error("failed to submit solution", "err", err) 124 | } else { 125 | cmd.Print(result.Display(q)) 126 | if !result.Accepted() { 127 | submitAccepted = false 128 | added, _ := appendToTestCases(q, result) 129 | if added { 130 | log.Info("added failed cases to `testcases.txt`") 131 | } 132 | } 133 | } 134 | } 135 | 136 | if !localPassed || !remotePassed || !submitAccepted { 137 | hasFailedCase = true 138 | } 139 | } 140 | 141 | if hasSubmitted { 142 | err = showTodayStreak(c, cmd) 143 | if err != nil { 144 | log.Debug("failed to show today's streak", "err", err) 145 | } 146 | } 147 | 148 | if hasFailedCase { 149 | return exitCode(1) 150 | } 151 | return nil 152 | }, 153 | } 154 | 155 | func runTestRemotely( 156 | cmd *cobra.Command, 157 | q *leetcode.QuestionData, 158 | c leetcode.Client, 159 | gen lang.Lang, 160 | limiter *utils.RateLimiter, 161 | ) ( 162 | *leetcode.RunCheckResult, 163 | error, 164 | ) { 165 | solution, err := lang.GetSolutionCode(q) 166 | if err != nil { 167 | return nil, fmt.Errorf("failed to get solution code: %w", err) 168 | } 169 | err = q.Fulfill() 170 | if err != nil { 171 | return nil, fmt.Errorf("failed to fetch question: %s", err) 172 | } 173 | 174 | exampleCases := q.GetExampleTestCases() 175 | casesStr := strings.Join(exampleCases, "\n") 176 | 177 | var ( 178 | cases lang.TestCases 179 | fromTestCasesFile bool 180 | ) 181 | testCasesFile, err := lang.GetFileOutput(q, lang.TestCasesFile) 182 | if err == nil { 183 | cases, err = lang.ParseTestCases(q, testCasesFile) 184 | if err == nil && len(cases.Cases) > 0 { 185 | fromTestCasesFile = true 186 | casesStr = cases.InputString() 187 | } 188 | } 189 | 190 | spin := newSpinner(cmd.ErrOrStderr()) 191 | spin.Suffix = " Running tests..." 192 | spin.Reverse() 193 | spin.Start() 194 | defer spin.Stop() 195 | 196 | limiter.Take() 197 | spin.Reverse() 198 | 199 | interResult, err := c.RunCode(q, gen.Slug(), solution, casesStr) 200 | if err != nil { 201 | return nil, fmt.Errorf("failed to run test: %w", err) 202 | } 203 | 204 | spin.Lock() 205 | spin.Suffix = " Waiting for result..." 206 | spin.Unlock() 207 | 208 | testResult, err := waitResult(c, interResult.InterpretId) 209 | if err != nil { 210 | return nil, fmt.Errorf("failed to wait test result: %w", err) 211 | } 212 | r := testResult.(*leetcode.RunCheckResult) 213 | r.InputData = interResult.TestCase 214 | 215 | if r.Accepted() && fromTestCasesFile { 216 | updated, err := cases.UpdateOutputs(r.ExpectedCodeAnswer) 217 | if err != nil { 218 | log.Debug("failed to update test cases", "err", err) 219 | } else if updated { 220 | content := []byte(cases.String()) 221 | err = utils.WriteFile(testCasesFile.GetPath(), content) 222 | if err != nil { 223 | log.Debug("failed to update test cases", "err", err) 224 | } else { 225 | log.Info("`testcases.txt` updated") 226 | } 227 | } 228 | } 229 | 230 | return r, nil 231 | } 232 | 233 | func waitResult(c leetcode.Client, submissionId string) ( 234 | leetcode.CheckResult, 235 | error, 236 | ) { 237 | for { 238 | result, err := c.CheckResult(submissionId) 239 | if err != nil { 240 | return nil, err 241 | } 242 | if result.GetState() == "SUCCESS" { 243 | return result, nil 244 | } 245 | time.Sleep(1 * time.Second) 246 | } 247 | } 248 | 249 | func newLimiter(user *leetcode.UserStatus) *utils.RateLimiter { 250 | if user.IsPremium { 251 | return utils.NewRateLimiter(1 * time.Second) 252 | } 253 | return utils.NewRateLimiter(10 * time.Second) 254 | } 255 | 256 | func newSpinner(w io.Writer) *spinner.Spinner { 257 | spin := spinner.New( 258 | spinner.CharSets[11], 259 | 125*time.Millisecond, 260 | spinner.WithHiddenCursor(false), 261 | spinner.WithWriter(w), 262 | spinner.WithColor("fgHiCyan"), 263 | ) 264 | return spin 265 | } 266 | -------------------------------------------------------------------------------- /leetcode/credential.go: -------------------------------------------------------------------------------- 1 | package leetcode 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "sync" 10 | "time" 11 | 12 | "github.com/charmbracelet/log" 13 | "github.com/j178/kooky" 14 | _ "github.com/j178/kooky/browser/chrome" 15 | _ "github.com/j178/kooky/browser/edge" 16 | _ "github.com/j178/kooky/browser/firefox" 17 | _ "github.com/j178/kooky/browser/safari" 18 | 19 | "github.com/j178/leetgo/config" 20 | ) 21 | 22 | type CredentialsProvider interface { 23 | Source() string 24 | AddCredentials(req *http.Request) error 25 | } 26 | 27 | type ResettableProvider interface { 28 | Reset() 29 | } 30 | 31 | type NeedClient interface { 32 | SetClient(c Client) 33 | } 34 | 35 | type nonAuth struct{} 36 | 37 | func NonAuth() CredentialsProvider { 38 | return &nonAuth{} 39 | } 40 | 41 | func (n *nonAuth) Source() string { 42 | return "none" 43 | } 44 | 45 | func (n *nonAuth) AddCredentials(req *http.Request) error { 46 | return errors.New("no credentials provided") 47 | } 48 | 49 | func (n *nonAuth) Reset() {} 50 | 51 | type cookiesAuth struct { 52 | LeetCodeSession string 53 | CsrfToken string 54 | CfClearance string // Cloudflare cookie, US only 55 | } 56 | 57 | func NewCookiesAuth(session, csrftoken, cfClearance string) CredentialsProvider { 58 | return &cookiesAuth{LeetCodeSession: session, CsrfToken: csrftoken, CfClearance: cfClearance} 59 | } 60 | 61 | func (c *cookiesAuth) Source() string { 62 | return "cookies" 63 | } 64 | 65 | func (c *cookiesAuth) AddCredentials(req *http.Request) error { 66 | if !c.hasAuth() { 67 | return errors.New("cookies not found") 68 | } 69 | req.AddCookie(&http.Cookie{Name: "LEETCODE_SESSION", Value: c.LeetCodeSession}) 70 | req.AddCookie(&http.Cookie{Name: "csrftoken", Value: c.CsrfToken}) 71 | req.AddCookie(&http.Cookie{Name: "cf_clearance", Value: c.CfClearance}) 72 | 73 | req.Header.Add("x-csrftoken", c.CsrfToken) 74 | return nil 75 | } 76 | 77 | func (c *cookiesAuth) Reset() {} 78 | 79 | func (c *cookiesAuth) hasAuth() bool { 80 | return c.LeetCodeSession != "" && c.CsrfToken != "" 81 | } 82 | 83 | type passwordAuth struct { 84 | cookiesAuth 85 | mu sync.Mutex 86 | c Client 87 | username string 88 | password string 89 | } 90 | 91 | func NewPasswordAuth(username, passwd string) CredentialsProvider { 92 | return &passwordAuth{username: username, password: passwd} 93 | } 94 | 95 | func (p *passwordAuth) Source() string { 96 | return "password" 97 | } 98 | 99 | func (p *passwordAuth) SetClient(c Client) { 100 | p.c = c 101 | } 102 | 103 | func (p *passwordAuth) AddCredentials(req *http.Request) error { 104 | if p.username == "" || p.password == "" { 105 | return errors.New("username or password is empty") 106 | } 107 | 108 | p.mu.Lock() 109 | defer p.mu.Unlock() 110 | 111 | if !p.hasAuth() { 112 | log.Info("logging in with username and password") 113 | resp, err := p.c.Login(p.username, p.password) 114 | if err != nil { 115 | return err 116 | } 117 | cookies := resp.Cookies() 118 | for _, cookie := range cookies { 119 | if cookie.Name == "LEETCODE_SESSION" { 120 | p.LeetCodeSession = cookie.Value 121 | } 122 | if cookie.Name == "csrftoken" { 123 | p.CsrfToken = cookie.Value 124 | } 125 | } 126 | if !p.hasAuth() { 127 | return errors.New("login failed") 128 | } 129 | } 130 | return p.cookiesAuth.AddCredentials(req) 131 | } 132 | 133 | func (p *passwordAuth) Reset() { 134 | p.mu.Lock() 135 | defer p.mu.Unlock() 136 | p.LeetCodeSession = "" 137 | p.CsrfToken = "" 138 | } 139 | 140 | type browserAuth struct { 141 | cookiesAuth 142 | mu sync.Mutex 143 | c Client 144 | browsers []string 145 | } 146 | 147 | func NewBrowserAuth(browsers []string) CredentialsProvider { 148 | return &browserAuth{browsers: browsers} 149 | } 150 | 151 | func (b *browserAuth) Source() string { 152 | return "browser" 153 | } 154 | 155 | func (b *browserAuth) SetClient(c Client) { 156 | b.c = c 157 | } 158 | 159 | func (b *browserAuth) AddCredentials(req *http.Request) error { 160 | b.mu.Lock() 161 | defer b.mu.Unlock() 162 | 163 | var errs []error 164 | if !b.hasAuth() { 165 | u, _ := url.Parse(b.c.BaseURI()) 166 | domain := u.Host 167 | 168 | defer func(start time.Time) { 169 | log.Debug("finished reading cookies", "elapsed", time.Since(start)) 170 | }(time.Now()) 171 | 172 | cookieStores := kooky.FindCookieStores(b.browsers...) 173 | filters := []kooky.Filter{ 174 | kooky.DomainHasSuffix(domain), 175 | kooky.FilterFunc( 176 | func(cookie *kooky.Cookie) bool { 177 | return kooky.Name("LEETCODE_SESSION").Filter(cookie) || 178 | kooky.Name("csrftoken").Filter(cookie) || 179 | kooky.Name("cf_clearance").Filter(cookie) 180 | }, 181 | ), 182 | } 183 | 184 | for _, store := range cookieStores { 185 | log.Debug("reading cookies", "browser", store.Browser(), "file", store.FilePath()) 186 | cookies, err := store.ReadCookies(filters...) 187 | if err != nil { 188 | errs = append(errs, err) 189 | continue 190 | } 191 | for _, cookie := range cookies { 192 | if cookie.Name == "LEETCODE_SESSION" { 193 | b.LeetCodeSession = cookie.Value 194 | } 195 | if cookie.Name == "csrftoken" { 196 | b.CsrfToken = cookie.Value 197 | } 198 | if cookie.Name == "cf_clearance" { 199 | b.CfClearance = cookie.Value 200 | } 201 | } 202 | if b.LeetCodeSession == "" || b.CsrfToken == "" { 203 | errs = append(errs, fmt.Errorf("LeetCode cookies not found in %s", store.FilePath())) 204 | continue 205 | } 206 | log.Info("reading leetcode cookies", "browser", store.Browser(), "domain", domain) 207 | break 208 | } 209 | } 210 | if !b.hasAuth() { 211 | if len(errs) > 0 { 212 | return fmt.Errorf("failed to read cookies: %w", errors.Join(errs...)) 213 | } 214 | return errors.New("no cookies found in browsers") 215 | } 216 | 217 | return b.cookiesAuth.AddCredentials(req) 218 | } 219 | 220 | func (b *browserAuth) Reset() { 221 | b.mu.Lock() 222 | defer b.mu.Unlock() 223 | b.LeetCodeSession = "" 224 | b.CsrfToken = "" 225 | } 226 | 227 | type combinedAuth struct { 228 | providers []CredentialsProvider 229 | } 230 | 231 | func NewCombinedAuth(providers ...CredentialsProvider) CredentialsProvider { 232 | return &combinedAuth{providers: providers} 233 | } 234 | 235 | func (c *combinedAuth) Source() string { 236 | return "combined sources" 237 | } 238 | 239 | func (c *combinedAuth) AddCredentials(req *http.Request) error { 240 | for _, p := range c.providers { 241 | if err := p.AddCredentials(req); err == nil { 242 | return nil 243 | } else { 244 | log.Debug("read credentials from %s failed: %v", p.Source(), err) 245 | } 246 | } 247 | return errors.New("no credentials provided") 248 | } 249 | 250 | func (c *combinedAuth) SetClient(client Client) { 251 | for _, p := range c.providers { 252 | if r, ok := p.(NeedClient); ok { 253 | r.SetClient(client) 254 | } 255 | } 256 | } 257 | 258 | func (c *combinedAuth) Reset() { 259 | for _, p := range c.providers { 260 | if r, ok := p.(ResettableProvider); ok { 261 | r.Reset() 262 | } 263 | } 264 | } 265 | 266 | func ReadCredentials() CredentialsProvider { 267 | cfg := config.Get() 268 | var providers []CredentialsProvider 269 | for _, from := range cfg.LeetCode.Credentials.From { 270 | switch from { 271 | case "browser": 272 | providers = append(providers, NewBrowserAuth(cfg.LeetCode.Credentials.Browsers)) 273 | case "password": 274 | username := os.Getenv("LEETCODE_USERNAME") 275 | password := os.Getenv("LEETCODE_PASSWORD") 276 | providers = append(providers, NewPasswordAuth(username, password)) 277 | case "cookies": 278 | session := os.Getenv("LEETCODE_SESSION") 279 | csrfToken := os.Getenv("LEETCODE_CSRFTOKEN") 280 | cfClearance := os.Getenv("LEETCODE_CFCLEARANCE") 281 | providers = append(providers, NewCookiesAuth(session, csrfToken, cfClearance)) 282 | } 283 | } 284 | if len(providers) == 0 { 285 | return NonAuth() 286 | } 287 | if len(providers) == 1 { 288 | return providers[0] 289 | } 290 | return NewCombinedAuth(providers...) 291 | } 292 | -------------------------------------------------------------------------------- /cmd/contest.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/AlecAivazis/survey/v2" 11 | "github.com/briandowns/spinner" 12 | "github.com/charmbracelet/lipgloss" 13 | "github.com/charmbracelet/log" 14 | "github.com/cli/browser" 15 | "github.com/hako/durafmt" 16 | "github.com/spf13/cobra" 17 | "github.com/spf13/viper" 18 | 19 | "github.com/j178/leetgo/config" 20 | "github.com/j178/leetgo/editor" 21 | "github.com/j178/leetgo/lang" 22 | "github.com/j178/leetgo/leetcode" 23 | ) 24 | 25 | var ( 26 | // https://robotmoon.com/256-colors/ 27 | contestTitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("47")).Bold(true) 28 | nameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("47")) 29 | timeStyle = lipgloss.NewStyle().Faint(true) 30 | checkDuration = 10 * time.Second 31 | openInBrowser bool 32 | ) 33 | 34 | func selectUpcomingContest(c leetcode.Client, registeredOnly bool) (string, error) { 35 | contestList, err := c.GetUpcomingContests() 36 | if err != nil { 37 | return "", err 38 | } 39 | if registeredOnly { 40 | var list []*leetcode.Contest 41 | for _, ct := range contestList { 42 | if ct.Registered { 43 | list = append(list, ct) 44 | } 45 | } 46 | contestList = list 47 | } 48 | 49 | if len(contestList) == 0 { 50 | msg := "no upcoming contest" 51 | if registeredOnly { 52 | msg = "no registered contest" 53 | } 54 | return "", errors.New(msg) 55 | } 56 | 57 | contestNames := make([]string, len(contestList)) 58 | for i, ct := range contestList { 59 | mark := " " 60 | if ct.Registered { 61 | mark = "✔" 62 | } 63 | contestNames[i] = fmt.Sprintf( 64 | "%s %s at %s", 65 | mark, 66 | ct.Title, 67 | time.Unix(ct.StartTime, 0).Format("2006/01/02 15:04:05"), 68 | ) 69 | } 70 | var idx int 71 | prompt := &survey.Select{ 72 | Message: "Select a contest:", 73 | Options: contestNames, 74 | } 75 | err = survey.AskOne(prompt, &idx) 76 | if err != nil { 77 | return "", err 78 | } 79 | return contestList[idx].TitleSlug, nil 80 | } 81 | 82 | func waitContestStart(cmd *cobra.Command, ct *leetcode.Contest) error { 83 | if ct.HasStarted() { 84 | return nil 85 | } 86 | 87 | var mu sync.Mutex 88 | spin := newSpinner(cmd.ErrOrStderr()) 89 | spin.PreUpdate = func(s *spinner.Spinner) { 90 | mu.Lock() 91 | defer mu.Unlock() 92 | s.Suffix = fmt.Sprintf( 93 | " %s begins in %s, waiting...", 94 | contestTitleStyle.Render(ct.Title), 95 | timeStyle.Render(durafmt.Parse(ct.TimeTillStart()).LimitFirstN(2).String()), 96 | ) 97 | } 98 | spin.Start() 99 | defer spin.Stop() 100 | 101 | for { 102 | if ct.HasStarted() { 103 | return nil 104 | } 105 | wait := ct.TimeTillStart() 106 | if wait > checkDuration { 107 | wait = checkDuration 108 | } 109 | time.Sleep(wait) 110 | 111 | mu.Lock() 112 | err := ct.Refresh() 113 | mu.Unlock() 114 | if err != nil { 115 | return err 116 | } 117 | } 118 | } 119 | 120 | func getUpcomingContests() ([]string, error) { 121 | c := leetcode.NewClient(leetcode.NonAuth()) 122 | contestList, err := c.GetUpcomingContests() 123 | if err != nil { 124 | return nil, err 125 | } 126 | var list []string 127 | for _, ct := range contestList { 128 | list = append(list, ct.TitleSlug) 129 | } 130 | return list, nil 131 | } 132 | 133 | var contestCmd = &cobra.Command{ 134 | Use: "contest [qid]", 135 | Short: "Generate contest questions", 136 | Example: `leetgo contest 137 | leetgo contest w330 138 | leetgo contest left w330 139 | `, 140 | Aliases: []string{"c"}, 141 | Args: cobra.MaximumNArgs(1), 142 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 143 | list, err := getUpcomingContests() 144 | if err != nil { 145 | return nil, cobra.ShellCompDirectiveError 146 | } 147 | return list, cobra.ShellCompDirectiveNoFileComp 148 | }, 149 | RunE: func(cmd *cobra.Command, args []string) error { 150 | c := leetcode.NewClient(leetcode.ReadCredentials()) 151 | cfg := config.Get() 152 | 153 | var qid string 154 | var err error 155 | if len(args) == 0 { 156 | qid, err = selectUpcomingContest(c, false) 157 | if err != nil { 158 | return err 159 | } 160 | } else { 161 | qid = args[0] 162 | } 163 | if slash := strings.Index(qid, "/"); slash > 0 && slash != len(qid)-1 { 164 | log.Warn("ignore question ID part in qid", "qid", qid) 165 | } 166 | if !strings.Contains(qid, "/") { 167 | qid += "/" 168 | } 169 | 170 | contest, _, err := leetcode.ParseContestQID(qid, c, false) 171 | if err != nil { 172 | return err 173 | } 174 | user, err := c.GetUserStatus() 175 | if err != nil { 176 | user = &leetcode.UserStatus{} 177 | } 178 | 179 | if !contest.HasFinished() && !contest.Registered { 180 | register := true 181 | if !viper.GetBool("yes") { 182 | prompt := survey.Confirm{ 183 | Message: fmt.Sprintf( 184 | "Register for %s as %s?", 185 | contestTitleStyle.Render(contest.Title), 186 | nameStyle.Render(user.Whoami(c)), 187 | ), 188 | } 189 | err := survey.AskOne(&prompt, ®ister) 190 | if err != nil { 191 | return err 192 | } 193 | } 194 | if register { 195 | err = c.RegisterContest(contest.TitleSlug) 196 | if err != nil { 197 | return err 198 | } 199 | log.Info("registered", "contest", contest.Title, "user", user.Whoami(c)) 200 | } else { 201 | return nil 202 | } 203 | } 204 | 205 | err = waitContestStart(cmd, contest) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | generated, err := lang.GenerateContest(contest) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | isSet := cmd.Flags().Lookup("browser").Changed 216 | if (isSet && openInBrowser) || (!isSet && cfg.Contest.OpenInBrowser) { 217 | for _, r := range generated { 218 | _ = browser.OpenURL(r.Question.ContestUrl()) 219 | } 220 | } 221 | err = editor.Open(generated[0]) 222 | return err 223 | }, 224 | } 225 | 226 | var unregisterCmd = &cobra.Command{ 227 | Use: "unregister [qid]", 228 | Short: "Unregister from contest", 229 | Aliases: []string{"un", "left"}, 230 | Args: cobra.MaximumNArgs(1), 231 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 232 | list, err := getUpcomingContests() 233 | if err != nil { 234 | return nil, cobra.ShellCompDirectiveError 235 | } 236 | return list, cobra.ShellCompDirectiveNoFileComp 237 | }, 238 | RunE: func(cmd *cobra.Command, args []string) error { 239 | c := leetcode.NewClient(leetcode.ReadCredentials()) 240 | 241 | var qid string 242 | var err error 243 | if len(args) == 0 { 244 | qid, err = selectUpcomingContest(c, true) 245 | if err != nil { 246 | return err 247 | } 248 | } else { 249 | qid = args[0] 250 | } 251 | if !strings.HasSuffix(qid, "/") { 252 | qid += "/" 253 | } 254 | 255 | contest, _, err := leetcode.ParseContestQID(qid, c, false) 256 | if err != nil { 257 | return err 258 | } 259 | if !contest.Registered { 260 | return fmt.Errorf("you are not registered for %s", contest.Title) 261 | } 262 | if contest.HasFinished() { 263 | return fmt.Errorf("contest %s has finished", contest.Title) 264 | } 265 | user, err := c.GetUserStatus() 266 | if err != nil { 267 | return err 268 | } 269 | unregister := true 270 | if !viper.GetBool("yes") { 271 | prompt := survey.Confirm{ 272 | Message: fmt.Sprintf( 273 | "Unregister from %s as %s?", 274 | contestTitleStyle.Render(contest.Title), 275 | nameStyle.Render(user.Whoami(c)), 276 | ), 277 | } 278 | err = survey.AskOne(&prompt, &unregister) 279 | if err != nil { 280 | return err 281 | } 282 | } 283 | if unregister { 284 | err = c.UnregisterContest(contest.TitleSlug) 285 | if err != nil { 286 | return err 287 | } 288 | log.Info("unregistered", "contest", contest.Title, "user", user.Whoami(c)) 289 | } 290 | 291 | return nil 292 | }, 293 | } 294 | 295 | func init() { 296 | contestCmd.Flags().BoolVarP(&openInBrowser, "browser", "b", false, "open question page in browser") 297 | contestCmd.AddCommand(unregisterCmd) 298 | } 299 | -------------------------------------------------------------------------------- /lang/gen.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/AlecAivazis/survey/v2" 10 | "github.com/AlecAivazis/survey/v2/terminal" 11 | "github.com/charmbracelet/log" 12 | "github.com/spf13/viper" 13 | 14 | "github.com/j178/leetgo/config" 15 | "github.com/j178/leetgo/constants" 16 | "github.com/j178/leetgo/leetcode" 17 | "github.com/j178/leetgo/utils" 18 | ) 19 | 20 | // GetGenerator returns the generator for the given language. If the language is not supported, an error will be returned. 21 | // The language can be specified by slug, short name or prefix of full name. 22 | func GetGenerator(lang string) (Lang, error) { 23 | lang = strings.ToLower(lang) 24 | for _, l := range SupportedLangs { 25 | if l.Slug() == lang { 26 | return l, nil 27 | } 28 | } 29 | for _, l := range SupportedLangs { 30 | if l.ShortName() == lang { 31 | return l, nil 32 | } 33 | } 34 | for _, l := range SupportedLangs { 35 | if strings.HasPrefix(strings.ToLower(l.Name()), lang) { 36 | return l, nil 37 | } 38 | } 39 | return nil, fmt.Errorf("language %s is not supported yet, welcome to send a PR", lang) 40 | } 41 | 42 | func generate(q *leetcode.QuestionData) (Lang, *GenerateResult, error) { 43 | cfg := config.Get() 44 | gen, err := GetGenerator(cfg.Code.Lang) 45 | if err != nil { 46 | return nil, nil, err 47 | } 48 | 49 | err = q.Fulfill() 50 | if err != nil { 51 | return nil, nil, fmt.Errorf("failed to get question data: %w", err) 52 | } 53 | 54 | codeSnippet := q.GetCodeSnippet(gen.Slug()) 55 | if codeSnippet == "" { 56 | if len(q.CodeSnippets) <= 3 { 57 | langs := make([]string, 0, len(q.CodeSnippets)) 58 | for _, snippet := range q.CodeSnippets { 59 | langs = append(langs, snippet.Lang) 60 | } 61 | return nil, nil, fmt.Errorf( 62 | `question %q doesn't support language %s, it only supports %s`, 63 | q.TitleSlug, 64 | gen.Slug(), 65 | strings.Join(langs, ","), 66 | ) 67 | } 68 | return nil, nil, fmt.Errorf(`question %q doesn't support language %q`, q.TitleSlug, cfg.Code.Lang) 69 | } 70 | 71 | outDir := getOutDir(q, gen) 72 | err = utils.CreateIfNotExists(outDir, true) 73 | if err != nil { 74 | return nil, nil, err 75 | } 76 | 77 | err = gen.InitWorkspace(outDir) 78 | if err != nil { 79 | return nil, nil, err 80 | } 81 | 82 | // Generate files 83 | result, err := gen.Generate(q) 84 | if err != nil { 85 | return nil, nil, err 86 | } 87 | result.SetOutDir(outDir) 88 | 89 | for _, hook := range result.ResultHooks { 90 | err := hook(result) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | } 95 | 96 | // Write files 97 | for i, file := range result.Files { 98 | written, err := tryWrite(file.GetPath(), file.Content) 99 | if errors.Is(err, terminal.InterruptErr) { 100 | return nil, nil, err 101 | } 102 | if err != nil { 103 | log.Error("failed to write file", "path", utils.RelToCwd(file.GetPath()), "err", err) 104 | continue 105 | } 106 | result.Files[i].Written = written 107 | } 108 | return gen, result, nil 109 | } 110 | 111 | // Generate generates the code for the given question. 112 | func Generate(q *leetcode.QuestionData) (*GenerateResult, error) { 113 | gen, result, err := generate(q) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | state := config.LoadState() 119 | state.LastQuestion = config.LastQuestion{ 120 | Slug: q.TitleSlug, 121 | FrontendID: q.QuestionFrontendId, 122 | Gen: gen.Slug(), 123 | } 124 | config.SaveState(state) 125 | 126 | return result, nil 127 | } 128 | 129 | // GenerateContest generates the code for all questions in the given contest. 130 | func GenerateContest(ct *leetcode.Contest) ([]*GenerateResult, error) { 131 | qs, err := ct.GetAllQuestions() 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | var results []*GenerateResult 137 | for _, q := range qs { 138 | _, result, err := generate(q) 139 | if err != nil { 140 | log.Error("failed to generate", "question", q.TitleSlug, "err", err) 141 | continue 142 | } 143 | results = append(results, result) 144 | } 145 | if len(results) == 0 { 146 | return nil, fmt.Errorf("no question generated") 147 | } 148 | 149 | state := config.LoadState() 150 | state.LastContest = ct.TitleSlug 151 | config.SaveState(state) 152 | 153 | return results, nil 154 | } 155 | 156 | func tryWrite(file string, content string) (bool, error) { 157 | write := true 158 | relPath := utils.RelToCwd(file) 159 | if utils.IsExist(file) { 160 | if !viper.GetBool("yes") { 161 | prompt := &survey.Confirm{Message: fmt.Sprintf("File \"%s\" already exists, overwrite?", relPath)} 162 | err := survey.AskOne(prompt, &write) 163 | if err != nil { 164 | return false, err 165 | } 166 | } 167 | } 168 | if !write { 169 | return false, nil 170 | } 171 | 172 | err := utils.WriteFile(file, []byte(content)) 173 | if err != nil { 174 | return false, err 175 | } 176 | 177 | log.Info("generated", "file", relPath) 178 | return true, nil 179 | } 180 | 181 | // GeneratePathsOnly runs generate process but only returns the paths of generated files, without writing them. 182 | func GeneratePathsOnly(q *leetcode.QuestionData) (*GenerateResult, error) { 183 | cfg := config.Get() 184 | gen, err := GetGenerator(cfg.Code.Lang) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | result, err := gen.GeneratePaths(q) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | outDir := getOutDir(q, gen) 195 | result.SetOutDir(outDir) 196 | return result, nil 197 | } 198 | 199 | // GetSolutionCode retrieves the solution code from the generated code file. 200 | func GetSolutionCode(q *leetcode.QuestionData) (string, error) { 201 | codeFile, err := GetFileOutput(q, CodeFile) 202 | if err != nil { 203 | return "", errors.New("code file not found") 204 | } 205 | code, err := codeFile.GetContent() 206 | if err != nil { 207 | return "", err 208 | } 209 | codeLines := strings.Split(code, "\n") 210 | var codeLinesToKeep []string 211 | inCode := false 212 | for _, line := range codeLines { 213 | if !inCode && strings.Contains(line, constants.CodeBeginMarker) { 214 | inCode = true 215 | continue 216 | } 217 | if inCode && strings.Contains(line, constants.CodeEndMarker) { 218 | break 219 | } 220 | if inCode { 221 | codeLinesToKeep = append(codeLinesToKeep, line) 222 | } 223 | } 224 | 225 | nonEmptyLines := 0 226 | for _, line := range codeLinesToKeep { 227 | if strings.TrimSpace(line) != "" { 228 | nonEmptyLines++ 229 | } 230 | } 231 | if nonEmptyLines == 0 { 232 | return "", fmt.Errorf("no code found in %s", codeFile.GetPath()) 233 | } 234 | 235 | return strings.Join(codeLinesToKeep, "\n"), nil 236 | } 237 | 238 | // UpdateSolutionCode updates the solution code in the generated code file. 239 | func UpdateSolutionCode(q *leetcode.QuestionData, newCode string) error { 240 | codeFile, err := GetFileOutput(q, CodeFile) 241 | if err != nil { 242 | return errors.New("code file not found") 243 | } 244 | code, err := codeFile.GetContent() 245 | if err != nil { 246 | return err 247 | } 248 | lines := strings.Split(code, "\n") 249 | var newLines []string 250 | skip := false 251 | for _, line := range lines { 252 | if strings.Contains(line, constants.CodeBeginMarker) { 253 | newLines = append(newLines, line+"\n") 254 | newLines = append(newLines, newCode) 255 | skip = true 256 | } else if strings.Contains(line, constants.CodeEndMarker) { 257 | newLines = append(newLines, line) 258 | skip = false 259 | } else if !skip { 260 | newLines = append(newLines, line) 261 | } 262 | } 263 | 264 | newContent := strings.Join(newLines, "\n") 265 | err = os.WriteFile(codeFile.GetPath(), []byte(newContent), 0o644) 266 | if err != nil { 267 | return err 268 | } 269 | log.Info("updated", "file", utils.RelToCwd(codeFile.GetPath())) 270 | return nil 271 | } 272 | 273 | // GetFileOutput returns the file output for the given question and file type. 274 | func GetFileOutput(q *leetcode.QuestionData, fileType FileType) (*FileOutput, error) { 275 | result, err := GeneratePathsOnly(q) 276 | if err != nil { 277 | return nil, err 278 | } 279 | f := result.GetFile(fileType) 280 | if f == nil { 281 | return nil, errors.New("file not found") 282 | } 283 | return f, nil 284 | } 285 | -------------------------------------------------------------------------------- /lang/testcase.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "slices" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/j178/leetgo/leetcode" 12 | goutils "github.com/j178/leetgo/testutils/go" 13 | "github.com/j178/leetgo/utils" 14 | ) 15 | 16 | type TestCase struct { 17 | Question *leetcode.QuestionData 18 | No int 19 | Input []string 20 | Output string 21 | } 22 | 23 | func (c *TestCase) Check() error { 24 | q := c.Question 25 | err := q.Fulfill() 26 | if err != nil { 27 | return fmt.Errorf("failed to get question data: %w", err) 28 | } 29 | narg := q.MetaData.NArg() 30 | if q.MetaData.SystemDesign { 31 | // System design questions have two inputs, the first one is a list of strings, but the second is a list of 32 | // different types. We just check if it's a valid list. 33 | // input: 34 | // ["LRUCache","put","put","get","put","get","put","get","get","get"] 35 | // [[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]] 36 | // output: 37 | // [null,null,null,1,null,-1,null,-1,3,4] 38 | if len(c.Input) != narg { 39 | return fmt.Errorf("should have %d arguments, got %d", narg, len(c.Input)) 40 | } 41 | l1, err := deserialize("[]string", c.Input[0]) 42 | if err != nil { 43 | return fmt.Errorf("cannot parse %s as []string", c.Input[0]) 44 | } 45 | l2, err := goutils.SplitArray(c.Input[1]) 46 | if err != nil { 47 | return fmt.Errorf("%s is not a valid list", c.Input[0]) 48 | } 49 | if l1.Len() != len(l2) { 50 | return fmt.Errorf("input[0] and input[1] should have the same length") 51 | } 52 | if c.HasOutput() { 53 | l3, err := goutils.SplitArray(c.Output) 54 | if err != nil { 55 | return fmt.Errorf("%s is not a valid list", c.Input[0]) 56 | } 57 | if l1.Len() != len(l3) { 58 | return fmt.Errorf("input and output should have the same length") 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | resultType := q.MetaData.ResultType() 65 | if len(c.Input) != narg { 66 | return fmt.Errorf("should have %d arguments, got %d", narg, len(c.Input)) 67 | } 68 | for j, arg := range c.Input { 69 | tp := q.MetaData.Params[j].Type 70 | if _, err := deserialize(tp, arg); err != nil { 71 | return fmt.Errorf("cannot parse %s as %s", arg, tp) 72 | } 73 | } 74 | if c.HasOutput() { 75 | if _, err := deserialize(resultType, c.Output); err != nil { 76 | return fmt.Errorf("cannot parse %s as %s", c.Output, resultType) 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (c *TestCase) InputString() string { 84 | return utils.EnsureTrailingNewline(strings.Join(c.Input, "\n")) 85 | } 86 | 87 | func (c *TestCase) HasOutput() bool { 88 | return c.Output != "" 89 | } 90 | 91 | type TestCases struct { 92 | Cases []TestCase 93 | Question *leetcode.QuestionData 94 | } 95 | 96 | func (tc *TestCases) AddCase(c TestCase) { 97 | c.No = len(tc.Cases) + 1 98 | c.Question = tc.Question 99 | tc.Cases = append(tc.Cases, c) 100 | } 101 | 102 | func (tc *TestCases) Contains(c TestCase) bool { 103 | for _, tc := range tc.Cases { 104 | if reflect.DeepEqual(c.Input, tc.Input) { 105 | return true 106 | } 107 | } 108 | return false 109 | } 110 | 111 | func (tc *TestCases) String() string { 112 | buf := new(strings.Builder) 113 | for i, c := range tc.Cases { 114 | buf.WriteString(testCaseInputMark + "\n") 115 | buf.WriteString(c.InputString()) 116 | buf.WriteString(testCaseOutputMark + "\n") 117 | buf.WriteString(c.Output + "\n") 118 | if i != len(tc.Cases)-1 { 119 | buf.WriteString("\n") 120 | } 121 | } 122 | return buf.String() 123 | } 124 | 125 | func (tc *TestCases) InputString() string { 126 | buf := new(strings.Builder) 127 | for _, c := range tc.Cases { 128 | buf.WriteString(c.InputString()) 129 | } 130 | return buf.String() 131 | } 132 | 133 | func (tc *TestCases) Check() error { 134 | for i, c := range tc.Cases { 135 | if err := c.Check(); err != nil { 136 | return fmt.Errorf("case %d: %w", i, err) 137 | } 138 | } 139 | return nil 140 | } 141 | 142 | // UpdateOutputs updates the output of the test cases with the result of the last run. 143 | func (tc *TestCases) UpdateOutputs(answers []string) (bool, error) { 144 | if len(answers) != len(tc.Cases) { 145 | return false, fmt.Errorf("expected %d answers, got %d", len(tc.Cases), len(answers)) 146 | } 147 | updated := false 148 | for i, c := range tc.Cases { 149 | if c.Output != answers[i] { 150 | c.Output = answers[i] 151 | tc.Cases[i] = c 152 | updated = true 153 | } 154 | } 155 | return updated, nil 156 | } 157 | 158 | func ParseTestCases(q *leetcode.QuestionData, f *FileOutput) (TestCases, error) { 159 | tc := TestCases{Question: q} 160 | 161 | content, err := f.GetContent() 162 | if err != nil { 163 | return tc, err 164 | } 165 | var ( 166 | inputLines []string 167 | output string 168 | inputStarted bool 169 | outputStarted bool 170 | ) 171 | lines := utils.SplitLines(content) 172 | for _, line := range lines { 173 | line = strings.TrimSpace(line) 174 | switch { 175 | case line == "": 176 | continue 177 | case strings.HasPrefix(line, testCaseInputMark): 178 | inputStarted = true 179 | outputStarted = false 180 | if len(inputLines) > 0 { 181 | tc.AddCase( 182 | TestCase{ 183 | Input: slices.Clone(inputLines), 184 | Output: output, 185 | }, 186 | ) 187 | inputLines = inputLines[:0] 188 | output = "" 189 | } 190 | case strings.HasPrefix(line, testCaseOutputMark): 191 | outputStarted = true 192 | inputStarted = false 193 | case inputStarted: 194 | inputLines = append(inputLines, line) 195 | case outputStarted: 196 | if len(output) > 0 { 197 | return tc, errors.New("invalid test case: output should be a single line") 198 | } 199 | output = line 200 | } 201 | } 202 | if len(inputLines) > 0 { 203 | tc.AddCase( 204 | TestCase{ 205 | Input: slices.Clone(inputLines), 206 | Output: output, 207 | }, 208 | ) 209 | } 210 | 211 | if err := tc.Check(); err != nil { 212 | return tc, fmt.Errorf("invalid test case: %w", err) 213 | } 214 | 215 | return tc, nil 216 | } 217 | 218 | type Range struct { 219 | whole bool 220 | max int 221 | ranges [][2]int 222 | } 223 | 224 | func (r *Range) Contains(idx int) bool { 225 | if r.whole { 226 | return true 227 | } 228 | for _, rg := range r.ranges { 229 | if idx >= rg[0] && idx <= rg[1] { 230 | return true 231 | } 232 | } 233 | return false 234 | } 235 | 236 | func ParseRange(expr string, max int) (*Range, error) { 237 | r := &Range{max: max} 238 | 239 | if expr == "" || expr == "-" { 240 | r.whole = true 241 | return r, nil 242 | } 243 | 244 | parts := strings.Split(expr, ",") 245 | for _, part := range parts { 246 | var start, end int 247 | var startNegative bool 248 | if strings.HasPrefix(part, "-") { 249 | startNegative = true 250 | part = part[1:] 251 | } 252 | rangeParts := strings.SplitN(part, "-", 2) 253 | switch len(rangeParts) { 254 | case 1: 255 | idx, err := strconv.Atoi(rangeParts[0]) 256 | if err != nil { 257 | return nil, fmt.Errorf("invalid range: %s", part) 258 | } 259 | if startNegative && idx < 0 { 260 | return nil, fmt.Errorf("invalid range: %s", part) 261 | } 262 | if startNegative { 263 | idx = -idx 264 | } 265 | start, end = idx, idx 266 | case 2: 267 | var err error 268 | start, err = strconv.Atoi(rangeParts[0]) 269 | if err != nil { 270 | return nil, fmt.Errorf("invalid range: %s", part) 271 | } 272 | if startNegative && start < 0 { 273 | return nil, fmt.Errorf("invalid range: %s", part) 274 | } 275 | if startNegative { 276 | start = -start 277 | } 278 | endStr := rangeParts[1] 279 | if endStr == "" { 280 | end = -1 281 | } else { 282 | end, err = strconv.Atoi(rangeParts[1]) 283 | if err != nil { 284 | return nil, fmt.Errorf("invalid range: %s", part) 285 | } 286 | } 287 | default: 288 | return nil, fmt.Errorf("invalid range: %s", part) 289 | } 290 | 291 | if start < 0 { 292 | start = max + start + 1 293 | } 294 | if end < 0 { 295 | end = max + end + 1 296 | } 297 | if start <= 0 || start > max { 298 | return nil, fmt.Errorf("invalid range: %s", part) 299 | } 300 | if end <= 0 || end > max { 301 | return nil, fmt.Errorf("invalid range: %s", part) 302 | } 303 | if start > end { 304 | return nil, fmt.Errorf("invalid range: %s", part) 305 | } 306 | r.ranges = append(r.ranges, [2]int{start, end}) 307 | } 308 | 309 | return r, nil 310 | } 311 | --------------------------------------------------------------------------------