Emacsで競技プログラミング | ojのラッパーパッケージを書いた話

## Intro

競技プログラミングコミュニティが年々大きくなっています。日本では[AOJ](http://judge.u-aizu.ac.jp/onlinejudge/index.jsp)や[AtCoder](https://atcoder.jp/)、世界では[TopCoder](https://www.topcoder.com/challenges/)や[Codeforces](https://codeforces.com/contest/)などのオンラインジャッジが人気です。

オンラインジャッジはコンパイル、実行を行い、あらかじめ用意されているテストケースをパスするかどうか確認してくれます。

プログラミング初心者のプログラミング導入にも最適ですし、プログラマの力試し/筋力向上としても、とても良い教材です。

ただ、サンプルケースをダウンロードしたり、解答をいい感じに管理したりと定型作業も多いです。

[oj](https://github.com/online-judge-tools/oj)と[oj-template](https://github.com/online-judge-tools/template-generator)というツールで、その定型作業を自動化できます。そのojをEmacsと連携させた [oj.el](https://github.com/conao3/oj.el) というパッケージを紹介します。

![0211d0a3-1e4b-431a-a898-00571c8a16e4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9567/4043de91-3bbf-c373-4884-a0628d2fb18d.gif)


なお、このojは私が作ったパッケージなので、技術的な話は「[oj.el - Conao3 note](https://conao3.com/blog/2020-db66-daf0/)」に書いてあります。


## Install

レポジトリはこちら
[![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9567/85723530-f433-1ad3-3fb5-b9165f5bd93d.png)](https://github.com/conao3/oj.el)


```el
(leaf oj
  :doc "Competitive programming tools client for AtCoder, Codeforces"
  :req "emacs-26.1" "quickrun-2.2"
  :tag "convenience" "emacs>=26.1"
  :url "https://github.com/conao3/oj.el"
  :emacs>= 26.1
  :ensure t
  :custom ((oj-compiler-c . "clang")
           (oj-compiler-python . "cpython")
           (oj-default-online-judge . 'atcoder)))
```


## Usage


### oj-install-package

このパッケージは[oj](https://github.com/online-judge-tools/oj), [oj-template](https://github.com/online-judge-tools/template-generator), [selenium](https://github.com/SeleniumHQ/selenium)に依存しています。

これらのソフトは `M-x oj-install-packages` でインストールできます。

```el
(defun oj-install-packages ()
  "Install `oj', `oj-template', `selenium' pip package via `pip3'."
  (interactive)
  (unless (yes-or-no-p "Install `oj', `oj-template', `selenium' via pip3?")
    (error "Abort install"))
  (dolist (elm '(("oj" . "online-judge-tools")
                 ("oj-template" . "online-judge-template-generator")
                 ("selenium" . "selenium")))
    (unless (executable-find (car elm))
      (unless (executable-find "python3")
        (error "Missing `python3'.  Please ensure Emacs's PATH and the installing"))
      (unless (executable-find "pip3")
        (error "Missing `pip3'.  Please ensure Emacs's PATH and the installing"))
      (oj--exec-script (format "pip3 install %s" (cdr elm))))))
```

もしこの関数によってインストールしたくない場合は、ターミナルで次のコマンドでインストールします。

```shell
pip3 install online-judge-tools
pip3 install online-judge-template-generator
pip3 install selenium
```

さらにEmacsの `PATH` が適切に設定されていることを確認してください。`M-! which oj` が `oj` へのパスを返せば、Emacsからojを利用できます。出力がない場合は、[Q&A](https://emacs.stackexchange.com/questions/550/exec-path-and-path)を参照するか、[exec-path-from-shell](https://github.com/purcell/exec-path-from-shell)を導入することを検討して下さい。


### oj-prepare

`M-x oj-prepare` はこのパッケージを使うためのファーストステップです。

このコマンドは次のようなフォーマットを受理します。

-   Problem URL: "https://codeforces.com/contest/1349/problem/A"
-   Contest URL: "https://codeforces.com/contest/1349/"
-   Problem shortcode (for `oj-defualt-online-judge`): "1349/A"
-   Contest shortcode (for `oj-default-online-judge`): "1349"


### Edit your code

`oj-home-dir` にファイルが生成されるので、ファイルを開いて編集して下さい。

```shell
$ tree codeforces/
codeforces/
└── 1349
    ├── A
    │   ├── a.out
    │   ├── geckodriver.log
    │   ├── generate.py
    │   ├── main.cpp
    │   ├── main.py
    │   └── test
    │       ├── sample-1.in
    │       ├── sample-1.out
    │       ├── sample-2.in
    │       ├── sample-2.out
    │       ├── sample-3.in
    │       └── sample-3.out
    ...
    └── F2
        ├── generate.py
        ├── main.cpp
        ├── main.py
        └── test
            ├── sample-1.in
            ├── sample-1.out
            ├── sample-2.in
            ├── sample-2.out
            ├── sample-3.in
            └── sample-3.out

$ cat codeforces/1349/A/main.cpp
#include <bits/stdc++.h>

using namespace std;

#define ll long long
#define ld long double
#define v vector

#define rep(i, n)      for (int i = 0; i < (int)(n); ++i)
#define rep3(i, m, n)  for (int i = (m); i < (int)(n); ++i)
#define rrep(i, n)     for (int i = (int)(n)-1; i >= 0; --i)
#define rrep3(i, m, n) for (int i = (int)(n)-1; i >= (m); --i)
#define all(x) x.begin(), x.end()
#define rall(x) x.end(x), x.begin()

#define endl '\n'


ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }

ll solve(int ebd, const vector<ll> & zdf) {
    // TODO: edit here
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int ebd;
    cin >> ebd;
    vector<ll> zdf(ebd);
    rep (i, ebd) {
        cin >> zdf[i];
    }

    auto ans = solve(ebd, zdf);
    cout << ans << endl;

    return 0;
}
```


### oj-test

`M-x oj-test` は `compile` と `test` を行います。

もしテストケースをパスしたら、 `*oj*` バッファに次のように出力されます。

```shell
[*] 3 cases found
[!] GNU time is not available: time

[*] sample-1
[x] time: 0.001666 sec
[+] AC

[*] sample-2
[x] time: 0.002213 sec
[+] AC

[*] sample-3
[x] time: 0.001923 sec
[+] AC

[x] slowest: 0.002213 sec  (for sample-2)
[+] test success: 3 cases
```

コンパイルコマンドは自動的に `quickrun` パッケージの定義から推測されます。`quickrun-add-command` を使用することで、追加や上書きができます。


### oj-submit

オンラインジャッジに提出するには `M-x oj-submit` を実行します。(初回にはオンラインジャッジごとに `M-x oj-login` が必要です)


## Customize


### Variables

-   **oj-shell-program:** `*oj*` バッファで使われるシェル  (default `shell-file-name` (`bash` or some customized shell))
-   **oj-home-dir:** ファイルが生成されるパス  (default `"~/.emacs.d/oj/"`)
-   **oj-default-online-judge:** 規定のオンラインジャッジ  (default `'codeforces`)
-   **oj-compiler-c:** C/C++の提出で使われるコンパイラ  (default `gcc`)
    
    Clangをローカルで使いたい場合は次のスニペットをinit.elに追加します。
    
    ```el
    (quickrun-set-default "c" "c/clang")
    (quickrun-set-default "c++" "c++/clang++")
    ```
-   **oj-compiler-python:** Pythonの提出で使われるコンパイラ  (default `cpython`)
-   **oj-login-args:** `oj login` のオプション引数  (default `nil`)
    
    ```text
    usage: oj login [-h] [-u USERNAME] [-p PASSWORD]
    [--check] [--use-browser {always,auto,never}] url
    
    positional arguments:
      url
    
    optional arguments:
      -h, --help            show this help message and exit
      -u USERNAME, --username USERNAME
      -p PASSWORD, --password PASSWORD
      --check               check whether you are logged in or not
      --use-browser {always,auto,never}
                            specify whether it uses a GUI web browser
                            to login or not  (default: auto)
    ```
-   **oj-prepare-args:** `oj-prepare` のオプション引数  (default `nil`)
    
    ```text
    Args for `oj-prepare'.
    
    usage: oj-prepare [-h] [-v] [-c COOKIE] [--config-file CONFIG_FILE] url
    
    positional arguments:
      url
    
    optional arguments:
      -h, --help            show this help message and exit
      -v, --verbose
      -c COOKIE, --cookie COOKIE
      --config-file CONFIG_FILE
                 default: ~/.config/online-judge-tools/prepare.config.toml
    ```
-   **oj-test-args:** `oj test` のオプション引数  (default `nil`)
    
    ```text
    Args for `oj-test'. Note that the runtime command (`-c') is detected automatically.
    
    usage: oj test [-h] [-c COMMAND] [-f FORMAT] [-d DIRECTORY] [-m
                   {simple,side-by-side}] [-S] [--no-rstrip]
                   [--rstrip] [-s] [-e ERROR] [-t TLE] [--mle MLE]
                   [-i] [-j N] [--print-memory] [--gnu-time GNU_TIME]
                   [--no-ignore-backup] [--ignore-backup] [--json]
                   [--judge-command JUDGE] [test [test ...]]
    
    positional arguments:
      test                  paths of test cases. (if empty: globbed from --format)
    
    optional arguments:
      -h, --help            show this help message and exit
      -c COMMAND, --command COMMAND
                            your solution to be tested.  (default: "./a.out")
      -f FORMAT, --format FORMAT
                            a format string to recognize the relationship of
                            test cases.  (default: "%s.%e")
      -d DIRECTORY, --directory DIRECTORY
                            a directory name for test cases (default: test/)
      -m {simple,side-by-side}, --display-mode {simple,side-by-side}
                            mode to display an output with the correct answer
                            (default: simple)
      -S, --side-by-side    display an output and the correct answer with
                            side byside mode
                            (equivalent to --display-mode side-by-side)
      --no-rstrip
      --rstrip              rstrip output before compare (default)
      -s, --silent          don't report output and correct answer even if not AC
                            (for --mode all)
      -e ERROR, --error ERROR
                            check as floating point number: correct if its absolute
                            or relative error doesn't exceed it
      -t TLE, --tle TLE     set the time limit (in second) (default: inf)
      --mle MLE             set the memory limit (in megabyte) (default: inf)
      -i, --print-input     print input cases if not AC
      -j N, --jobs N        specifies the number of jobs to run simultaneously
                            (default: no parallelization)
      --print-memory        print the amount of memory which your program used,
                            even if it is small enough
      --gnu-time GNU_TIME   used to measure memory consumption (default: "time")
      --no-ignore-backup
      --ignore-backup       ignore backup files and hidden files
                            (i.e. files like "*~", "\#*\#" and ".*")
                            (default)
      --json
      --judge-command JUDGE
                            specify judge command instead of default diff judge.
                            See https://online-judge-tools.readthedocs.io/en/
                                  master/introduction.en.html
                                  #test-for-special-forms-of-problem for details
    
    format string for --format:
      %s                    name
      %e                    extension: "in" or "out"
      (both %s and %e are required.)
    
    tips:
      You can do similar things with shell: e.g.
     `for f in test/*.in ; do echo $f ; diff <(./a.out < $f) ${f/.in/.out} ; done`
    ```
-   **oj-submit-args:** `oj submit` のオプション引数  (default `'("-y")`)
    
    ```text
    Args for `oj-submit'.
    
    usage: oj submit [-h] [-l LANGUAGE] [--no-guess] [-g]
                     [--no-guess-latest] [--guess-cxx-latest]
                     [--guess-cxx-compiler {gcc,clang,all}]
                     [--guess-python-version {2,3,auto,all}]
                     [--guess-python-interpreter {cpython,pypy,all}]
                     [--format-dos2unix] [--format-rstrip] [-G]
                     [--no-open] [--open] [-w SECOND] [-y] [url] file
    
    positional arguments:
      url                   the URL of the problem to submit.
                            if not given, guessed from history of download command.
      file
    
    optional arguments:
      -h, --help            show this help message and exit
      -l LANGUAGE, --language LANGUAGE
                            narrow down language choices if ambiguous
      --no-guess
      -g, --guess           guess the language for your file (default)
      --no-guess-latest
      --guess-cxx-latest    use the lasest version for C++ (default)
      --guess-cxx-compiler {gcc,clang,all}
                            use the specified C++ compiler if both of GCC and
                            Clang are available (default: gcc)
      --guess-python-version {2,3,auto,all}
                            default: auto
      --guess-python-interpreter {cpython,pypy,all}
                            use the specified Python interpreter if both of CPython
                            and PyPy are available (default: cpython)
      --format-dos2unix     replace CRLF with LF for given file
      --format-rstrip       remove trailing newlines from given file
      -G, --golf            now equivalent to --format-dos2unix --format-rstrip
      --no-open
      --open                open the result page after submission (default)
      -w SECOND, --wait SECOND
                            sleep before submitting
      -y, --yes             don't confirm
    ```


### Template file

[oj](https://github.com/online-judge-tools/template-generator#settings)において、自動生成されるソースコードにはテンプレートファイルを使うことができます。

テンプレートをカスタマイズするには、下記の様なファイルを `~/.config/online-judge-tools/template/template-ext.cpp` として保存します。

```cpp
<%!
    import onlinejudge_template.generator.cplusplus as cplusplus
    import onlinejudge_template.generator.about as about
%>\
<%
    data['config']['rep_macro'] = 'rep'
    data['config']['using_namespace_std'] = True
    data['config']['long_long_int'] = 'll'
%>\
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <utility>
#include <tuple>
#include <cstdint>
#include <cstdio>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <deque>
#include <unordered_map>
#include <unordered_set>
#include <bitset>
#include <cctype>

using namespace std;

#define ll long long
#define ld long double
#define v vector

#define rep(i, n)      for (int i = 0; i < (int)(n); ++i)
#define rep3(i, m, n)  for (int i = (m); i < (int)(n); ++i)
#define rrep(i, n)     for (int i = (int)(n)-1; i >= 0; --i)
#define rrep3(i, m, n) for (int i = (int)(n)-1; i >= (m); --i)
#define all(x) x.begin(), x.end()
#define rall(x) x.end(x), x.begin()

#define endl '\n'
${cplusplus.declare_constants(data)}

ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }

${cplusplus.return_type(data)} solve(${cplusplus.formal_arguments(data)}) {
    // TODO: edit here
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

${cplusplus.read_input(data)}

    auto ${cplusplus.return_value(data)} = solve(${cplusplus.actual_arguments(data)});
${cplusplus.write_output(data)}

    return 0;
}
```

さらに下記の様なtomlを `~/.config/online-judge-tools/prepare.config.toml` として保存します。

```toml
[templates]
"main.py" = "main.py"
"main.cpp" = "template-ext.cpp"
"generate.py" = "generate.py"
```