├── .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 | [](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
--------------------------------------------------------------------------------