はじめに
このところAtCoderの過去問をじわじわと解いている私です。
最近OnlineJudgeHelperというツールの存在を知り「これ使えばすごく捗るじゃん?」と衝撃を受けたわけですが、
「いや、これくらいのツールは自分で作れないと!」という変な意地もあって、
結局オレオレAtCoderツールを自作してしまいましたよ、という話です。
テストの自動実行
テストの自動実行だけはわりと昔からやっていましたが、仕掛けは至ってシンプルなものです。
- 入力データ例を "data/A_001.in" といったファイル名で置いておく
- 出力データ例を "data/A_001.exp" といったファイル名で置いておく
- プログラムを実行して、出力結果を "data/A_001.out" といったファイル名で保存
- "data/A_001.out" と "data/A_001.exp" の中身を文字列比較
- 上記をテストケースの番号順に一括実行
def test_auto(prog)
1.upto(999) do |i|
break unless test(prog, i)
end
end
def test(prog, n)
base, ext = prog.split('.')
q = base.split('_')[0]
cs = format('%s_%03d', q, n)
infile = "data/#{cs}.in"
outfile = "data/#{cs}.out"
expfile = "data/#{cs}.exp"
return false unless File.exist?(infile)
return false unless File.exist?(expfile)
cmd =
case ext
# 拡張子(使用言語)によって起動コマンドを変える
end
ec = system("#{cmd} < #{infile} > #{outfile}")
input = File.read(infile)
result = File.read(outfile)
expected = File.read(expfile)
puts "==== #{cs} ===="
puts '-- input --'
print input
puts '-- expected --'
print expected
puts '-- result --'
print result
if !ec
puts '!!!!! RE !!!!!'
elsif result != expected
puts '!!!!! WA !!!!!'
else
puts '<< OK >>'
end
true
end
ちょっとうれしいポイントとしては、
例えばD問題で最初に書いた”D.cxx”が通らなかった!となって別の方法を試してみる時に、
別バージョンをアンスコ付きの"D_v2.cxx"といったファイル名にしておくと、
どちらのプログラムも同じテストデータ(data/D_00X.in)でテストできるようになっています。
## テストデータのダウンロード
しばらくはテストデータを問題ページから1つ1つ手でコピーしていましたが、
ある時、「スクレイピングで自動化できるんじゃね?」と気がつきました。
require 'mechanize'
def _fetch_all
url = @url + 'assignments'
puts "fetch from #{url} ..."
sleep 0.1
page = @agent.get(url)
('A'..'Z').each do |q|
link = page.link_with(text: q)
next unless link
_fetch(q, link.href)
end
end
def _fetch(q, url)
puts "fetch from #{url} ..."
sleep 0.1
page = @agent.get(url)
fmt = smp = desc = ''
page.search("//div[contains(@class,'part')]").each do |section|
h3 = section.search('h3')
next if h3.empty?
title = h3[0].content
pre = section.search('pre')
next if pre.empty?
data = pre[0].content.lstrip.gsub("\r\n", "\n")
case title
when /^入力$/
desc = section.content
fmt = data
when /^入力例\s*(\d+)$/
_out_data(q, $1, 'in', data)
smp = data if data.size > smp.size
when /^出力例\s*(\d+)$/
_out_data(q, $1, 'exp', data)
end
end
_out_src(q, 'rb', src_gen_ruby(fmt, smp))
_out_src(q, 'cxx', src_gen_c(desc, fmt, smp))
end
問題によってページの構成にバリエーションがあるため、
使っていてうまく取れないケースが出てきたらその都度修正するということを繰り返していましたが、
最近は安定感が出てきました。
ログイン機能
過去問ばかり解いていたのでしばらく気が付きませんでしたが、
あるときコンテスト中にテストデータを落とそうとしたら動かなくて、
コンテスト中はこういうのは禁止なのかな?と思っていたら、
どうやらスクレイピングツールからAtCoderにログインしておけば
コンテスト中でもダウンロードできるらしいということがわかり、
ログインしてからダウンロードする機能をつけました。
この部分は自力で作れなくて、他所のAtCoderツールを大いに参考にさせて頂きました。
ソース提出機能
ログインのスクリプトができると、それを少し改造するだけでソースの提出もできるようになりました。
実は提出フォームから提出しても大した手間じゃないと思っていたので、
あまり自動化する必要性を感じていなかったんですが、
できてしまうとそれなりに便利ですね。
ソースの拡張子から言語を自動判別できるので、
言語の選択ミスで余計なペナルティをくらうことがなくなるのもいいです。
オレオレツールなので、言語はRubyとC++しか対応してないです。
ソース生成機能
たぶんOnlineJudgeHelperが話題になったときに、誰かが
「入力部分のソースの自動生成くらいできないと」
みたいな発言をしていたんだと思いますが、それを思い切り真に受けてしまい、
問題ページの入力データの説明から入力部分のソースを生成することにチャレンジしました。
RubyとC++の両方でソースを出したかったので、データフォーマットをパースした結果を
一旦共通の入力データ定義的な構造体に突っ込むようにしました。
InputDef = Struct.new(:type, :size, :fmt, :vars)
def parse_fmt(fmt)
inpdefs = []
fmt = fmt
.gsub(/-1/, '') # N-1 -> N
.gsub(/(-| )/, ' ') # a-b -> a b
.gsub(/[_,\\\(\)\{\}]/, '')
.split("\n")
fmt << ''
re = prev = nil
fmt.each do |f|
if re
if f =~ re
prev = f
next
end
inpdef = inpdefs.last
case inpdef.type
when :matrix
inpdef.size = prev[-2..-1].chars.to_a
when :varray
inpdef.size = prev =~ /(\d+)$/ ? $1 : prev[-1]
end
re = prev = nil
end
case f
when /^([a-z]+).(\s+\1.)+\s+[\.|…]+\s+\1.$/i, /^([a-z]+)[01](\s+\1.)+$/i
inpdefs << InputDef.new(:harray, f[-1], :number, $1)
when /^([a-z]+)..(\s+\1..)+\s+[\.|…]+\s+\1..$/i
inpdefs << InputDef.new(:matrix, nil, :number, $1)
re = /(^#{$1}..(\s+#{$1}..)+\s+[\.|…]+\s+#{$1}..|:|:|\.+)$/
prev = f
when /^([a-z]+)..(\1..)+\s+[\.|…]+\s+\1..$/i
inpdefs << InputDef.new(:matrix, nil, :char, $1)
re = /(^#{$1}..(#{$1}..)+\s+[\.|…]+\s+#{$1}..|:|:|\.+)$/
prev = f
when /^([a-z]+)[01][01](\s+\1..)+$/i
inpdefs << InputDef.new(:matrix, nil, :number, $1)
re = /(^#{$1}..(\s+#{$1}..)+|:|:|\.+)$/
prev = f
when /^[a-z]+(\d)(\s+[a-z]+\1)*$/i
vars = f.split.map { |v| v[0..-2] }
inpdefs << InputDef.new(:varray, nil, :number, vars)
pat = vars.map { |v| v + '.+' }.join('\s+')
re = Regexp.new("^(#{pat}|:|:|\\.+)$")
prev = f
when /^[a-z]+(\s+[a-z]+)*$/i
inpdefs << InputDef.new(:single, nil, :number, f.split)
end
end
inpdefs
end
上記の処理で
- 単独の変数
- スペース区切りで横に並んだ単独の変数
- 横方向の配列
- 縦方向の配列
- 行列
のフォーマットをパースしようとしていますが、まだ改良中です。
データ型はフォーマットの情報だけでは分からないので、一旦全て整数型として、
後で入力データ例と突き合わせてそれ以外の型を設定するようにしています。
とはいっても今のところ、整数じゃなさそうなものは全て文字列という扱いになっています。
使用イメージ
コンテスト名のフォルダを作って、その中に以下のファイルを置きます。
require '../../AtCoderFriends/helper.rb'
# start
# test_auto 'A.rb'
# submit 'A.rb'
# test_auto 'B.rb'
# submit 'B.rb'
# test_auto 'C.rb'
# submit 'C.rb'
# test_auto 'D.cxx'
# submit 'D.cxx'
Sublime Textの左側にソース、右側にhelper.rbを開いて、
適宜helper.rbのコメントを解除して動かすという使い方をしています。
ソース・テストデータ生成→コーディング→コンパイル→テスト→提出
の作業が全てSublime Textの中で完結できるので、自分としてはかなり快適です。
オレオレツールなのでCLIとかGUIの類は一切ありません。
ソース一式はこちら
追記(2018/02/24)
相対パスでソースをrequireするやり方だと
コンテストのフォルダが増えてきても階層化できなくて、だんだんつらくなってきたので
ソースをRubyGem化して、Sublime Textのプラグインから呼ぶ方式に作り変えました。
うまくいけばさらに快適になるはずですが、しばらくはバグ取りが必要そうです。。。
追記(2018/06/14)
Sublime Textプラグインの操作イメージはこんな感じです。
だいぶ快適になりました。