├── .github └── workflows │ ├── bump-formula.yml │ └── unit-test.yml ├── .gitignore ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── AtCoderCLI │ └── main.swift └── AtCoderLibrary │ ├── API │ ├── OjApiCommand.swift │ └── Response.swift │ ├── Command │ ├── New.swift │ ├── New │ │ ├── CodeTemplate.swift │ │ └── Templates │ │ │ ├── PackageSwift.swift │ │ │ ├── Readme.swift │ │ │ ├── Source.swift │ │ │ ├── Test.swift │ │ │ ├── TestLibrary.swift │ │ │ └── XCScheme.swift │ ├── Submit.swift │ └── Submit │ │ └── RunTest.swift │ └── Error.swift ├── Tests └── AtCoderLibraryTests │ ├── Command │ └── New │ │ └── Generator │ │ ├── PackageTests.swift │ │ ├── ReadmeTests.swift │ │ ├── SourceTests.swift │ │ └── TestTests.swift │ └── Helper.swift ├── doc └── getting_started.md └── misc ├── copy_sha256.sh ├── open_first_task.png ├── select_target.png ├── submit_page.png ├── test_done.png └── top.png /.github/workflows/bump-formula.yml: -------------------------------------------------------------------------------- 1 | name: Bump Homebrew accs formula 2 | 3 | on: 4 | push: 5 | tags: 6 | - "[0-9]+.[0-9]+.[0-9]+" 7 | 8 | jobs: 9 | homebrew: 10 | name: Bump Homebrew formula 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: dawidd6/action-homebrew-bump-formula@v3 14 | with: 15 | token: ${{ secrets.TOKEN }} 16 | tap: ShotaKashihara/homebrew-tap 17 | formula: accs 18 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | unit-test: 11 | runs-on: ubuntu-latest 12 | container: swift:5.3 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Run Unit Test 16 | run: make test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | /.swiftpm 7 | Package.resolved 8 | abc190/ 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=/usr/local 2 | 3 | build: 4 | swift build -c release --disable-sandbox 5 | 6 | test: 7 | swift test --enable-test-discovery 8 | 9 | install: build 10 | mkdir -p "$(PREFIX)/bin" 11 | cp -f ".build/release/accs" "$(PREFIX)/bin/accs" 12 | 13 | uninstall: 14 | rm "$(PREFIX)/bin/accs" 15 | 16 | docker-run-it: 17 | docker run --rm -it \ 18 | --volume "$(pwd):/src" \ 19 | --workdir "/src" \ 20 | swift:5.3 21 | 22 | .PHONY: build test install uninstall docker-run-it 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | import class Foundation.ProcessInfo 4 | 5 | let macOSPlatform: SupportedPlatform 6 | if let deploymentTarget = ProcessInfo.processInfo.environment["SWIFTPM_MACOS_DEPLOYMENT_TARGET"] { 7 | macOSPlatform = .macOS(deploymentTarget) 8 | } else { 9 | macOSPlatform = .macOS(.v10_10) 10 | } 11 | 12 | let package = Package( 13 | name: "AtCoderCLI", 14 | platforms: [macOSPlatform], 15 | products: [ 16 | .executable(name: "accs", targets: ["AtCoderCLI"]), 17 | .library(name: "AtCoderLibrary", targets: ["AtCoderLibrary"]), 18 | ], 19 | dependencies: [ 20 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.0"), 21 | .package(url: "https://github.com/kareman/SwiftShell", from: "5.1.0") 22 | ], 23 | targets: [ 24 | .target( 25 | name: "AtCoderCLI", 26 | dependencies: ["AtCoderLibrary"] 27 | ), 28 | .target( 29 | name: "AtCoderLibrary", 30 | dependencies: [ 31 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 32 | "SwiftShell", 33 | ] 34 | ), 35 | .testTarget( 36 | name: "AtCoderLibraryTests", 37 | dependencies: ["AtCoderLibrary"] 38 | ), 39 | ], 40 | swiftLanguageVersions: [.v5] 41 | ) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AtCoderCLI for Swift (accs) 2 | 3 | [![Unit Test](https://github.com/ShotaKashihara/atcoder-cli-swift/workflows/Unit%20Test/badge.svg?event=push)](https://github.com/ShotaKashihara/atcoder-cli-swift/actions) 4 | 5 | [Tatamo/atcoder-cli](https://github.com/Tatamo/atcoder-cli) にインスパイアされた Swift-er 向けの AtCoder CLI です。 6 | 7 | コンテストに対応する Swift Package プロジェクトを作成します。 8 | 9 | 10 | - Swift Package プロジェクトには各問題に対応する Target と TestTarget が含まれます。 11 | - XCTest のテストケースを問題のサンプル入出力から自動で作成します。 12 | 13 | 14 | 15 | ## Install 16 | 17 | ```bash 18 | brew install ShotaKashihara/tap/accs 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```bash 24 | $ accs new abc001 # "abc001/" directory will be created 25 | $ cd abc001/ 26 | $ open Package.swift # Launch Swift Package project with Xcode 27 | # ... write your solution ... 28 | $ accs submit {:problem_alphabet} # to use submit function 29 | ``` 30 | 31 | ## Requirements 32 | 33 | [online-judge-tools](https://github.com/online-judge-tools/oj) 34 | コンテスト情報の取得のため、`oj-api` というコマンドに依存しています 35 | 36 | - インストール 37 | - https://github.com/online-judge-tools/oj#how-to-install 38 | - ログイン 39 | 40 | ```bash 41 | oj login https://atcoder.jp/ 42 | > Username: kashihararara 43 | > Password: 44 | ``` 45 | 46 | ## Documents 47 | 48 | [Getting Started](doc/getting_started.md) 49 | -------------------------------------------------------------------------------- /Sources/AtCoderCLI/main.swift: -------------------------------------------------------------------------------- 1 | import AtCoderLibrary 2 | import ArgumentParser 3 | import SwiftShell 4 | 5 | struct Command: ParsableCommand { 6 | static var configuration = CommandConfiguration( 7 | abstract: "AtCoder CLI for Swift.", 8 | version: "1.0.16", 9 | subcommands: [ 10 | New.self, 11 | Submit.self, 12 | ] 13 | ) 14 | } 15 | 16 | Command.main() 17 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/API/OjApiCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftShell 3 | 4 | enum OjApiCommand { 5 | static func getAllTasks(url: String, ojApiPath: String) throws -> (contest: Contest, problems: [Problem]) { 6 | let contest = try OjApiCommand.getContest(url: url, ojApiPath: ojApiPath) 7 | var problems = [Problem]() 8 | for problem in contest.problems { 9 | let problem = try OjApiCommand.getProblem(url: problem.url, ojApiPath: ojApiPath) 10 | .apply(context: problem.context) 11 | problems.append(problem) 12 | } 13 | return (contest, problems) 14 | } 15 | 16 | static func submitCode(contestName: String, task: String, ojApiPath: String) throws -> URL { 17 | try precheck(path: ojApiPath) 18 | let contestURL = "https://atcoder.jp/contests/\(contestName)" 19 | let contest = try getContest(url: contestURL, ojApiPath: ojApiPath) 20 | guard let problem = contest.problems.first(where: { 21 | $0.context.alphabet.uppercased() == task.uppercased() 22 | }) else { 23 | throw "The contest name or the task name is invalid." 24 | } 25 | let filePath = "Sources/\(task.uppercased())/main.swift" 26 | let language = try guessLanguage(url: problem.url, filePath: filePath, ojApiPath: ojApiPath) 27 | return try submitCode(url: problem.url, filePath: filePath, language: language, ojApiPath: ojApiPath) 28 | } 29 | } 30 | 31 | private extension OjApiCommand { 32 | static func precheck(path: String) throws { 33 | guard !run("which", path).stdout.isEmpty else { 34 | /// Debug で `oj-api` にパスが通ってない場合は、[Edit Scheme] - [Run] - [Arguments Passed on Launch] に 35 | /// `new abc190 --oj-api-path ` を追加してください 36 | throw "command not found: `\(path)`" 37 | } 38 | } 39 | 40 | static func getContest(url: String, ojApiPath: String) throws -> Contest { 41 | try precheck(path: ojApiPath) 42 | let result = run(ojApiPath, "get-contest", url) 43 | guard result.succeeded else { 44 | throw result.stderror 45 | } 46 | print(result.stdout) 47 | let response = try JSONDecoder().decode(GetContestResponse.self, from: result.stdout.data(using: .utf8)!) 48 | return response.result 49 | } 50 | 51 | static func getProblem(url: URL, ojApiPath: String) throws -> Problem { 52 | try precheck(path: ojApiPath) 53 | let result = run(ojApiPath, "get-problem", url.absoluteString) 54 | guard result.succeeded else { 55 | throw result.stderror 56 | } 57 | print(result.stdout) 58 | let response = try JSONDecoder().decode(GetProblemResponse.self, from: result.stdout.data(using: .utf8)!) 59 | return response.result 60 | } 61 | 62 | static func guessLanguage(url: URL, filePath: String, ojApiPath: String) throws -> Language { 63 | try precheck(path: ojApiPath) 64 | let result = run(ojApiPath, "guess-language-id", url.absoluteString, "--file=\(filePath)") 65 | guard result.succeeded else { 66 | throw result.stderror 67 | } 68 | print(result.stdout) 69 | let response = try JSONDecoder().decode(GuessLanguageResponse.self, from: result.stdout.data(using: .utf8)!) 70 | return response.result 71 | } 72 | 73 | static func submitCode(url: URL, filePath: String, language: Language, ojApiPath: String) throws -> URL { 74 | let result = run( 75 | ojApiPath, 76 | "submit-code", 77 | "--file", 78 | filePath, 79 | "--language", 80 | language.id, 81 | url.absoluteString 82 | ) 83 | guard result.succeeded else { 84 | throw result.stderror 85 | } 86 | print(result.stdout) 87 | let response = try JSONDecoder().decode(SubmitCodeResponse.self, from: result.stdout.data(using: .utf8)!) 88 | return response.result.url 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/API/Response.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension OjApiCommand { 4 | struct GetContestResponse: Decodable { 5 | let result: Contest 6 | } 7 | 8 | struct GetProblemResponse: Decodable { 9 | let result: Problem 10 | } 11 | 12 | struct GuessLanguageResponse: Decodable { 13 | let result: Language 14 | } 15 | 16 | struct SubmitCodeResponse: Decodable { 17 | let result: Result 18 | 19 | struct Result: Decodable { 20 | let url: URL 21 | } 22 | } 23 | } 24 | 25 | struct Contest: Decodable { 26 | let url: URL 27 | let problems: [Problem] 28 | let name: String 29 | 30 | struct Problem: Decodable { 31 | let url: URL 32 | let name: String 33 | let context: Context 34 | } 35 | } 36 | 37 | struct Problem: Decodable { 38 | let url: URL 39 | let tests: [Test] 40 | let name: String 41 | let context: Context 42 | let memoryLimit: Int 43 | let timeLimit: Int 44 | 45 | struct Test: Decodable { 46 | let input: String 47 | let output: String 48 | } 49 | 50 | func apply(context: Context) -> Self { 51 | .init(url: url, tests: tests, name: name, context: context, memoryLimit: memoryLimit, timeLimit: timeLimit) 52 | } 53 | } 54 | 55 | struct Context: Decodable { 56 | let contest: Contest 57 | let alphabet: String 58 | 59 | struct Contest: Decodable { 60 | let name: String 61 | let url: URL 62 | } 63 | } 64 | 65 | struct Language: Decodable { 66 | let id: String 67 | let description: String 68 | } 69 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/New.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ArgumentParser 3 | import SwiftShell 4 | 5 | public struct New: ParsableCommand { 6 | public init() {} 7 | public static var configuration = CommandConfiguration( 8 | abstract: "Create a new contest project." 9 | ) 10 | 11 | @Argument(help: "Name of the contest to create.\nex. 'abc123'") 12 | var contestName: String 13 | 14 | @Option(name: .shortAndLong, help: "Contest URL. \nIf no value is given, then 'https://atcoder.jp/contests/:contest_name' is used.") 15 | var contestUrl: String? 16 | 17 | @Flag(name: .shortAndLong, help: "Open Package.swift after creation.") 18 | var open: Bool = false 19 | 20 | @Option(help: "Specify the path to oj-api.") 21 | var ojApiPath: String = "oj-api" 22 | 23 | public mutating func run() throws { 24 | let contestUrl = self.contestUrl ?? "https://atcoder.jp/contests/\(contestName)" 25 | let (contest, problems) = try OjApiCommand.getAllTasks(url: contestUrl, ojApiPath: ojApiPath) 26 | try FileManager.default.createDirectory(atPath: contestName, withIntermediateDirectories: true) 27 | FileManager.default.changeCurrentDirectoryPath(contestName) 28 | 29 | let alphabets = problems.map(\.context.alphabet) 30 | try PackageSwift(contestName: contestName, alphabets: alphabets).codeGenerate() 31 | try Readme(contest: contest, problems: problems).codeGenerate() 32 | try problems.forEach { 33 | try Source(problem: $0).codeGenerate() 34 | } 35 | try problems.forEach { 36 | try Test(problem: $0).codeGenerate() 37 | } 38 | try TestLibrary().codeGenerate() 39 | try problems.forEach { 40 | try XCScheme(problem: $0).codeGenerate() 41 | } 42 | 43 | if open { 44 | SwiftShell.run("cd", contestName) 45 | SwiftShell.run("open", "Package.swift") 46 | return 47 | } 48 | print(""" 49 | Finished. 50 | $ cd \(contestName) && open Package.swift 51 | """) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/New/CodeTemplate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol CodeTemplate { 4 | var fileName: String { get } 5 | var directory: String? { get } 6 | var source: String { get } 7 | } 8 | 9 | extension CodeTemplate { 10 | func codeGenerate() throws { 11 | if let directory = directory { 12 | try FileManager.default.createDirectory(atPath: directory, withIntermediateDirectories: true) 13 | } 14 | let filePath = "\(directory ?? ".")/\(fileName)" 15 | guard !FileManager.default.fileExists(atPath: filePath) else { 16 | print("Skip file creation because the file exists.", "path: ", filePath) 17 | return 18 | } 19 | try source.write(toFile: filePath, atomically: true, encoding: .utf8) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/New/Templates/PackageSwift.swift: -------------------------------------------------------------------------------- 1 | struct PackageSwift: CodeTemplate { 2 | let contestName: String 3 | let alphabets: [String] 4 | let fileName = "Package.swift" 5 | let directory: String? = nil 6 | var source: String { 7 | """ 8 | // swift-tools-version:5.3 9 | import PackageDescription 10 | 11 | let package = Package( 12 | name: "\(contestName.uppercased())", 13 | dependencies: [], 14 | targets: [ 15 | \(alphabets.map { 16 | """ 17 | .target(name: "\($0)"), 18 | .testTarget(name: "\($0)Tests", dependencies: ["\($0)", "TestLibrary"]), 19 | """ 20 | } 21 | .joined(separator: "\n")) 22 | .target(name: "TestLibrary", path: "Tests/TestLibrary"), 23 | ] 24 | ) 25 | """ 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/New/Templates/Readme.swift: -------------------------------------------------------------------------------- 1 | struct Readme: CodeTemplate { 2 | let contest: Contest 3 | let problems: [Problem] 4 | let fileName = "README.md" 5 | let directory: String? = nil 6 | var source: String { 7 | let problems = self.problems.sorted(by: { 8 | $0.context.alphabet < $1.context.alphabet 9 | }) 10 | return """ 11 | # [\(contest.name)](\(contest.url)) 12 | 13 | 問題名 | 実行時間制限 | メモリ制限 14 | :-- | --: | --: 15 | \(problems.map { problem in """ 16 | [\(problem.context.alphabet) \(problem.name)](\(problem.url)) | \(Double(problem.timeLimit)/1000) sec | \(problem.memoryLimit) MB 17 | """}.joined(separator: "\n")) 18 | 19 | """ 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/New/Templates/Source.swift: -------------------------------------------------------------------------------- 1 | struct Source: CodeTemplate { 2 | let problem: Problem 3 | let fileName = "main.swift" 4 | var directory: String? { "Sources/\(problem.context.alphabet)" } 5 | var source: String { 6 | """ 7 | // \(problem.context.alphabet) - \(problem.name) 8 | // \(problem.url) 9 | // 実行制限時間: \(Double(problem.timeLimit)/1000) sec 10 | import Foundation 11 | 12 | """ 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/New/Templates/Test.swift: -------------------------------------------------------------------------------- 1 | struct Test: CodeTemplate { 2 | let problem: Problem 3 | var className: String { 4 | if Int(problem.context.alphabet) != nil { 5 | // [!] Class name can not start with a number. 6 | return "_\(problem.context.alphabet)Tests" 7 | } 8 | return "\(problem.context.alphabet)Tests" 9 | } 10 | var fileName: String { "\(problem.context.alphabet)Tests.swift" } 11 | var directory: String? { "Tests/\(problem.context.alphabet)Tests" } 12 | var source: String { 13 | """ 14 | import XCTest 15 | import TestLibrary 16 | 17 | let cases: [TestCase] = [ 18 | \(problem.tests.map(Self.testCase).joined(separator: "\n")) 19 | ] 20 | 21 | final class \(className): XCTestCase, TimeLimit { 22 | let timeLimit: TimeInterval = \(Double(problem.timeLimit)/1000) 23 | 24 | func testExample() throws { 25 | try cases.forEach(solve) 26 | } 27 | } 28 | """ 29 | } 30 | 31 | private static func testCase(_ test: Problem.Test) -> String { 32 | let inputs = test.input.split(separator: "\n") 33 | let outputs = test.output.split(separator: "\n") 34 | return """ 35 | (#filePath, #line, 36 | \""" 37 | \(inputs.map { "\($0)" }.joined(separator: "\n")) 38 | \""", \""" 39 | \(outputs.map { "\($0)" }.joined(separator: "\n")) 40 | \"""), 41 | """ 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/New/Templates/TestLibrary.swift: -------------------------------------------------------------------------------- 1 | struct TestLibrary: CodeTemplate { 2 | let directory: String? = "Tests/TestLibrary" 3 | let fileName = "TestLibrary.swift" 4 | var source: String { 5 | """ 6 | import XCTest 7 | import class Foundation.Bundle 8 | 9 | public protocol TimeLimit { 10 | var timeLimit: TimeInterval { get } 11 | } 12 | 13 | public typealias TestCase = (file: StaticString, line: UInt, input: String, expected: String) 14 | 15 | public extension TimeLimit where Self: XCTestCase { 16 | func solve(file: StaticString, line: UInt, input: String, expected: String) throws { 17 | // Some of the APIs that we use below are available in macOS 10.13 and above. 18 | guard #available(macOS 10.13, *) else { 19 | return 20 | } 21 | let expected = expected.last == "\\n" ? expected : expected + "\\n" 22 | var error = "" 23 | let exp = expectation(description: "") 24 | let testTarget = String(describing: type(of: self)) 25 | .replacingOccurrences(of: "Tests", with: "") 26 | .replacingOccurrences(of: "_", with: "") 27 | let binary = productsDirectory.appendingPathComponent(testTarget) 28 | let process = Process() 29 | 30 | addTeardownBlock { 31 | if process.isRunning { 32 | process.terminate() 33 | } 34 | } 35 | 36 | process.executableURL = binary 37 | let pipeInput = Pipe() 38 | process.standardInput = pipeInput 39 | let pipeOutput = Pipe() 40 | process.standardOutput = pipeOutput 41 | let pipeError = Pipe() 42 | process.standardError = pipeError 43 | 44 | DispatchQueue.global().async { 45 | do { 46 | try process.run() 47 | pipeInput.fileHandleForWriting.write(input.data(using: .utf8)!) 48 | pipeInput.fileHandleForWriting.closeFile() 49 | process.waitUntilExit() 50 | exp.fulfill() 51 | error = String(data: pipeError.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)! 52 | } catch (let e) { 53 | error = e.localizedDescription 54 | } 55 | } 56 | let result = XCTWaiter.wait(for: [exp], timeout: timeLimit) 57 | switch result { 58 | case .completed: 59 | if error.isEmpty { 60 | let data = pipeOutput.fileHandleForReading.readDataToEndOfFile() 61 | let output = String(data: data, encoding: .utf8)! 62 | XCTAssertEqual(output, expected, file: file, line: line) 63 | } else { 64 | XCTFail("RE: \\(error)", file: file, line: line) 65 | } 66 | case .timedOut: 67 | XCTFail("TLE: Exceeded timeout of \\(timeLimit) seconds", file: file, line: line) 68 | default: 69 | XCTFail("Unrecognized error.", file: file, line: line) 70 | } 71 | } 72 | 73 | private var productsDirectory: URL { 74 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 75 | return bundle.bundleURL.deletingLastPathComponent() 76 | } 77 | fatalError("couldn't find the products directory") 78 | } 79 | } 80 | """ 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/New/Templates/XCScheme.swift: -------------------------------------------------------------------------------- 1 | struct XCScheme: CodeTemplate { 2 | let problem: Problem 3 | var fileName: String { "\(problem.context.alphabet).xcscheme" } 4 | let directory: String? = ".swiftpm/xcode/xcshareddata/xcschemes" 5 | var source: String { 6 | """ 7 | 8 | 11 | 14 | 15 | 21 | 27 | 28 | 29 | 35 | 41 | 42 | 43 | 44 | 45 | 50 | 51 | 53 | 59 | 60 | 61 | 62 | 63 | 73 | 75 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | """ 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/Submit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ArgumentParser 3 | import SwiftShell 4 | 5 | public struct Submit: ParsableCommand { 6 | public init() {} 7 | public static var configuration = CommandConfiguration( 8 | abstract: "Submit a your code." 9 | ) 10 | 11 | @Argument(help: "Alphabet of the problem to be submitted.") 12 | var task: String 13 | 14 | @Flag(name: .shortAndLong, help: "Run a UnitTest before submitting.") 15 | var runTest: Bool = false 16 | 17 | @Option(help: "Specify the path to oj-api.") 18 | var ojApiPath: String = "oj-api" 19 | 20 | public mutating func run() throws { 21 | guard FileManager.default.fileExists(atPath: "./Package.swift") else { 22 | throw """ 23 | Could not resolve the contest name. 24 | Please move Package.swift directory. 25 | """ 26 | } 27 | let currentDirectory = FileManager.default.currentDirectoryPath 28 | .split(separator: "/") 29 | .last! 30 | let contestName = String(currentDirectory) 31 | if runTest { 32 | try RunTest.sampleCase(task: task) 33 | } 34 | let submitUrl = try OjApiCommand.submitCode(contestName: contestName, task: task, ojApiPath: ojApiPath) 35 | SwiftShell.run("open", submitUrl.absoluteString) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Command/Submit/RunTest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftShell 3 | 4 | enum RunTest { 5 | static func sampleCase(task: String) throws { 6 | try runAndPrint("swift", "test", "--filter", "\(task.uppercased())Tests/testExample") 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/AtCoderLibrary/Error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String: Error {} 4 | extension String: LocalizedError { 5 | public var errorDescription: String? { return self } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/AtCoderLibraryTests/Command/New/Generator/PackageTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import AtCoderLibrary 4 | 5 | final class PackageTests: XCTestCase { 6 | func test() throws { 7 | let contestName = "abc001" 8 | let alphabets = ["A", "B", "C"] 9 | let package = PackageSwift(contestName: contestName, alphabets: alphabets) 10 | let expected = """ 11 | // swift-tools-version:5.3 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "ABC001", 16 | dependencies: [], 17 | targets: [ 18 | .target(name: "A"), 19 | .testTarget(name: "ATests", dependencies: ["A", "TestLibrary"]), 20 | .target(name: "B"), 21 | .testTarget(name: "BTests", dependencies: ["B", "TestLibrary"]), 22 | .target(name: "C"), 23 | .testTarget(name: "CTests", dependencies: ["C", "TestLibrary"]), 24 | .target(name: "TestLibrary", path: "Tests/TestLibrary"), 25 | ] 26 | ) 27 | """ 28 | XCTAssertEqual(package.source, expected) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/AtCoderLibraryTests/Command/New/Generator/ReadmeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import AtCoderLibrary 4 | 5 | final class ReadmeTests: XCTestCase { 6 | func test() throws { 7 | let url = "https://example.com/contest".url 8 | let name = "Example Contest 001" 9 | let contest = Contest(url: url, problems: [], name: name) 10 | let problems: [Problem] = [ 11 | .init(url: "https://example.com/p_1".url, 12 | tests: [], 13 | name: "task1", 14 | context: .init(contest: .empty, alphabet: "A"), 15 | memoryLimit: 1000, 16 | timeLimit: 1000), 17 | .init(url: "https://example.com/p_2".url, 18 | tests: [], 19 | name: "task2", 20 | context: .init(contest: .empty, alphabet: "B"), 21 | memoryLimit: 1500, 22 | timeLimit: 1500), 23 | .init(url: "https://example.com/p_3".url, 24 | tests: [], 25 | name: "task3", 26 | context: .init(contest: .empty, alphabet: "C"), 27 | memoryLimit: 3000, 28 | timeLimit: 3000), 29 | ] 30 | let readme = Readme(contest: contest, problems: problems) 31 | let expected = """ 32 | # [Example Contest 001](https://example.com/contest) 33 | 34 | 問題名 | 実行時間制限 | メモリ制限 35 | :-- | --: | --: 36 | [A task1](https://example.com/p_1) | 1.0 sec | 1000 MB 37 | [B task2](https://example.com/p_2) | 1.5 sec | 1500 MB 38 | [C task3](https://example.com/p_3) | 3.0 sec | 3000 MB 39 | 40 | """ 41 | XCTAssertEqual(readme.source, expected) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/AtCoderLibraryTests/Command/New/Generator/SourceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import AtCoderLibrary 4 | 5 | final class SourceTests: XCTestCase { 6 | func test() throws { 7 | let problem = Problem( 8 | url: "https://example.com/p_1".url, 9 | tests: [], 10 | name: "task1", 11 | context: .init(contest: .empty, alphabet: "A"), 12 | memoryLimit: 1000, 13 | timeLimit: 1000 14 | ) 15 | let source = Source(problem: problem) 16 | let expected = """ 17 | // A - task1 18 | // https://example.com/p_1 19 | // 実行制限時間: 1.0 sec 20 | import Foundation 21 | 22 | """ 23 | XCTAssertEqual(source.source, expected) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/AtCoderLibraryTests/Command/New/Generator/TestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import AtCoderLibrary 4 | 5 | final class TestTests: XCTestCase { 6 | func test() throws { 7 | let problem = Problem( 8 | url: "https://example.com/p_1".url, 9 | tests: [ 10 | .init(input: "10 2\n20 3\n30 4\n", output: "5\n20\n3\n"), 11 | .init(input: "199 2\n", output: "1992\n"), 12 | ], 13 | name: "", 14 | context: .init(contest: .empty, alphabet: "A"), 15 | memoryLimit: 1000, 16 | timeLimit: 2000 17 | ) 18 | let test = Test(problem: problem) 19 | let expected = """ 20 | import XCTest 21 | import TestLibrary 22 | 23 | let cases: [TestCase] = [ 24 | (#filePath, #line, 25 | \""" 26 | 10 2 27 | 20 3 28 | 30 4 29 | \""", \""" 30 | 5 31 | 20 32 | 3 33 | \"""), 34 | (#filePath, #line, 35 | \""" 36 | 199 2 37 | \""", \""" 38 | 1992 39 | \"""), 40 | ] 41 | 42 | final class ATests: XCTestCase, TimeLimit { 43 | let timeLimit: TimeInterval = 2.0 44 | 45 | func testExample() throws { 46 | try cases.forEach(solve) 47 | } 48 | } 49 | """ 50 | XCTAssertEqual(test.source, expected) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/AtCoderLibraryTests/Helper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import AtCoderLibrary 3 | 4 | extension String { 5 | var url: URL { 6 | URL(string: self)! 7 | } 8 | } 9 | 10 | extension Context.Contest { 11 | static let empty: Context.Contest = .init(name: "", url: "file://".url) 12 | } 13 | -------------------------------------------------------------------------------- /doc/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## コンテストの始め方 4 | 5 | ### 1. コンテスト名から Xcode プロジェクトの作成 6 | 7 | ```bash 8 | # コンテスト名はコンテストページURLの末尾の文字列を参照します 9 | # ex. http://atcoder.jp/contests/abc190 -> abc190 10 | accs new abc190 11 | ``` 12 | 13 | ### 2. 作成したプロジェクトを開く 14 | 15 | ``` 16 | cd abc190 17 | open Package.swift 18 | ``` 19 | 20 | ### 3. `Sources/A/main.swift` を開いて問題を解きます 21 | 22 | 23 | 24 | ### 4. 問題が解けたら ⌘(command)-U でテストを実行します 25 | 26 | 27 | 28 | 29 | *ビルドターゲットを問題のアルファベットに切り替えておくと良いです 30 | 31 | 32 | 33 | - ショートカットキー: ⌃(control)-⌘(command)-] 34 | 35 | ### 5. テストが通ったらコードを提出します 36 | 37 | ```bash 38 | # 提出コードがA問題なら には `a` を指定します 39 | accs submit -r 40 | ``` 41 | 42 | 43 | -------------------------------------------------------------------------------- /misc/copy_sha256.sh: -------------------------------------------------------------------------------- 1 | if [ $# -eq 0 ]; then 2 | echo "A tag argument is needed!(ex: ./copy_sha256 1.2.3)" 3 | exit 1 4 | fi 5 | tag=$1 6 | echo "Tag: '${tag}'" 7 | filename="${tag}.tar.gz" 8 | echo "Filename: '${filename}'" 9 | curl -LOk "https://github.com/ShotaKashihara/atcoder-cli-swift/archive/${filename}" 10 | result=$(shasum -a 256 $filename) 11 | echo "Result: '${result}'" 12 | sha256=$(echo ${result} | cut -d ' ' -f 1) 13 | echo $sha256 | tr -d '\n' | pbcopy 14 | echo "sha256('${sha256}') was copied to your clipboard🎉" 15 | rm $filename 16 | -------------------------------------------------------------------------------- /misc/open_first_task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotaKashihara/atcoder-cli-swift/a355669cc3d70bc5d48fd62491733435e9035adb/misc/open_first_task.png -------------------------------------------------------------------------------- /misc/select_target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotaKashihara/atcoder-cli-swift/a355669cc3d70bc5d48fd62491733435e9035adb/misc/select_target.png -------------------------------------------------------------------------------- /misc/submit_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotaKashihara/atcoder-cli-swift/a355669cc3d70bc5d48fd62491733435e9035adb/misc/submit_page.png -------------------------------------------------------------------------------- /misc/test_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotaKashihara/atcoder-cli-swift/a355669cc3d70bc5d48fd62491733435e9035adb/misc/test_done.png -------------------------------------------------------------------------------- /misc/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShotaKashihara/atcoder-cli-swift/a355669cc3d70bc5d48fd62491733435e9035adb/misc/top.png --------------------------------------------------------------------------------