at_coder_friends 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +27 -0
- data/.rubocop_todo.yml +8 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +21 -0
- data/README.md +114 -0
- data/Rakefile +8 -0
- data/at_coder_friends.gemspec +38 -0
- data/bin/console +15 -0
- data/bin/setup +10 -0
- data/config/.at_coder_friends.yml.sample +2 -0
- data/exe/at_coder_friends +8 -0
- data/lib/at_coder_friends.rb +17 -0
- data/lib/at_coder_friends/cli.rb +122 -0
- data/lib/at_coder_friends/config_loader.rb +35 -0
- data/lib/at_coder_friends/cxx_generator.rb +179 -0
- data/lib/at_coder_friends/emitter.rb +43 -0
- data/lib/at_coder_friends/errors.rb +7 -0
- data/lib/at_coder_friends/format_parser.rb +148 -0
- data/lib/at_coder_friends/judge_test_runner.rb +34 -0
- data/lib/at_coder_friends/path_util.rb +33 -0
- data/lib/at_coder_friends/problem.rb +48 -0
- data/lib/at_coder_friends/ruby_generator.rb +97 -0
- data/lib/at_coder_friends/sample_test_runner.rb +31 -0
- data/lib/at_coder_friends/scraping_agent.rb +135 -0
- data/lib/at_coder_friends/test_runner.rb +82 -0
- data/lib/at_coder_friends/verifier.rb +33 -0
- data/lib/at_coder_friends/version.rb +5 -0
- metadata +166 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtCoderFriends
|
4
|
+
# run test cases for the specified program with actual input/output.
|
5
|
+
class JudgeTestRunner < TestRunner
|
6
|
+
include PathUtil
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
super(path)
|
10
|
+
@cases_dir = cases_dir(@dir)
|
11
|
+
@smp_dir = smp_dir(@dir)
|
12
|
+
end
|
13
|
+
|
14
|
+
def judge_all
|
15
|
+
puts "***** judge_all #{@prg} *****"
|
16
|
+
Dir["#{@cases_dir}/#{@q}/in/*.txt"].sort.each do |infile|
|
17
|
+
id = File.basename(infile, '.txt')
|
18
|
+
judge(id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def judge_one(id)
|
23
|
+
puts "***** judge_one #{@prg} *****"
|
24
|
+
judge(id)
|
25
|
+
end
|
26
|
+
|
27
|
+
def judge(id)
|
28
|
+
infile = "#{@cases_dir}/#{@q}/in/#{id}.txt"
|
29
|
+
outfile = "#{@smp_dir}/#{@q}_#{id}.out"
|
30
|
+
expfile = "#{@cases_dir}/#{@q}/out/#{id}.txt"
|
31
|
+
run_test(id, infile, outfile, expfile)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtCoderFriends
|
4
|
+
# Common methods and behaviors for dealing with paths.
|
5
|
+
module PathUtil
|
6
|
+
module_function
|
7
|
+
|
8
|
+
SMP_DIR = 'data'
|
9
|
+
CASES_DIR = 'cases'
|
10
|
+
|
11
|
+
def contest_name(path)
|
12
|
+
path = File.expand_path(path)
|
13
|
+
dir = File.file?(path) ? File.dirname(path) : path
|
14
|
+
File.basename(dir).delete('#').downcase
|
15
|
+
end
|
16
|
+
|
17
|
+
def split_prg_path(path)
|
18
|
+
path = File.expand_path(path)
|
19
|
+
dir, prg = File.split(path)
|
20
|
+
base, ext = prg.split('.')
|
21
|
+
q = base.split('_')[0]
|
22
|
+
[path, dir, prg, base, ext, q]
|
23
|
+
end
|
24
|
+
|
25
|
+
def smp_dir(dir)
|
26
|
+
File.join(dir, SMP_DIR)
|
27
|
+
end
|
28
|
+
|
29
|
+
def cases_dir(dir)
|
30
|
+
File.join(dir, CASES_DIR)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtCoderFriends
|
4
|
+
DataSample = Struct.new(:no, :ext, :txt) do
|
5
|
+
def initialize(no, ext, txt)
|
6
|
+
no = no.tr('0-9', '0-9').to_i
|
7
|
+
txt = txt.lstrip.gsub("\r\n", "\n")
|
8
|
+
super(no, ext, txt)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
InputDef = Struct.new(:container, :item, :names, :size) do
|
13
|
+
def initialize(container, item, names, size = [])
|
14
|
+
super(container, item, names, size)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
SourceSample = Struct.new(:ext, :txt)
|
19
|
+
|
20
|
+
# holds problem information
|
21
|
+
class Problem
|
22
|
+
attr_reader :q, :fmt, :smps, :srcs
|
23
|
+
attr_accessor :html, :desc, :defs
|
24
|
+
|
25
|
+
def initialize(q)
|
26
|
+
@q = q
|
27
|
+
@html = ''
|
28
|
+
@desc = ''
|
29
|
+
@fmt = ''
|
30
|
+
@smps = []
|
31
|
+
@defs = []
|
32
|
+
@srcs = []
|
33
|
+
yield self if block_given?
|
34
|
+
end
|
35
|
+
|
36
|
+
def fmt=(f)
|
37
|
+
@fmt = f.lstrip.gsub("\r\n", "\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_smp(no, ext, txt)
|
41
|
+
@smps << DataSample.new(no, ext, txt)
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_src(ext, txt)
|
45
|
+
@srcs << SourceSample.new(ext, txt)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtCoderFriends
|
4
|
+
# generates C++ source code from definition
|
5
|
+
class RubyGenerator
|
6
|
+
TEMPLATE = <<~TEXT
|
7
|
+
### DCLS ###
|
8
|
+
|
9
|
+
puts ans
|
10
|
+
TEXT
|
11
|
+
|
12
|
+
def process(pbm)
|
13
|
+
src = generate(pbm.defs)
|
14
|
+
pbm.add_src(:rb, src)
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate(defs)
|
18
|
+
dcls = gen_decls(defs).join("\n")
|
19
|
+
TEMPLATE.sub('### DCLS ###', dcls)
|
20
|
+
end
|
21
|
+
|
22
|
+
def gen_decls(defs)
|
23
|
+
defs.map { |inpdef| gen_decl(inpdef) }.flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
def gen_decl(inpdef)
|
27
|
+
case inpdef.container
|
28
|
+
when :single
|
29
|
+
gen_single_decl(inpdef)
|
30
|
+
when :harray
|
31
|
+
gen_harray_decl(inpdef)
|
32
|
+
when :varray
|
33
|
+
if inpdef.names.size == 1
|
34
|
+
gen_varray_1_decl(inpdef)
|
35
|
+
else
|
36
|
+
gen_varray_n_decl(inpdef)
|
37
|
+
end
|
38
|
+
when :matrix
|
39
|
+
gen_matrix_decl(inpdef)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def gen_single_decl(inpdef)
|
44
|
+
names = inpdef.names
|
45
|
+
dcl = names.join(', ')
|
46
|
+
expr = gen_expr(inpdef.item, names.size > 1)
|
47
|
+
"#{dcl} = #{expr}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def gen_harray_decl(inpdef)
|
51
|
+
v = inpdef.names[0]
|
52
|
+
dcl = "#{v}s"
|
53
|
+
expr = gen_expr(inpdef.item, true)
|
54
|
+
"#{dcl} = #{expr}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def gen_varray_1_decl(inpdef)
|
58
|
+
v = inpdef.names[0]
|
59
|
+
sz = inpdef.size[0]
|
60
|
+
dcl = "#{v}s"
|
61
|
+
expr = gen_expr(inpdef.item, false)
|
62
|
+
"#{dcl} = Array.new(#{sz}) { #{expr} }"
|
63
|
+
end
|
64
|
+
|
65
|
+
def gen_varray_n_decl(inpdef)
|
66
|
+
names = inpdef.names
|
67
|
+
sz = inpdef.size[0]
|
68
|
+
dcl = names.map { |v| "#{v}s[i]" }.join(', ')
|
69
|
+
expr = gen_expr(inpdef.item, true)
|
70
|
+
ret = []
|
71
|
+
ret += names.map { |v| "#{v}s = Array.new(#{sz})" }
|
72
|
+
ret << "#{sz}.times do |i|"
|
73
|
+
ret << " #{dcl} = #{expr}"
|
74
|
+
ret << 'end'
|
75
|
+
ret
|
76
|
+
end
|
77
|
+
|
78
|
+
def gen_matrix_decl(inpdef)
|
79
|
+
v = inpdef.names[0]
|
80
|
+
sz = inpdef.size[0]
|
81
|
+
decl = "#{v}ss"
|
82
|
+
expr = gen_expr(inpdef.item, true)
|
83
|
+
"#{decl} = Array.new(#{sz}) { #{expr} }"
|
84
|
+
end
|
85
|
+
|
86
|
+
def gen_expr(item, split)
|
87
|
+
case item
|
88
|
+
when :number
|
89
|
+
split ? 'gets.split.map(&:to_i)' : 'gets.to_i'
|
90
|
+
when :string
|
91
|
+
split ? 'gets.chomp.split' : 'gets.chomp'
|
92
|
+
when :char
|
93
|
+
'gets.chomp'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtCoderFriends
|
4
|
+
# run test cases for the specified program with sample input/output.
|
5
|
+
class SampleTestRunner < TestRunner
|
6
|
+
include PathUtil
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
super(path)
|
10
|
+
@smp_dir = smp_dir(@dir)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_all
|
14
|
+
puts "***** test_all #{@prg} *****"
|
15
|
+
1.upto(999) do |i|
|
16
|
+
break unless test(i)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_one(n)
|
21
|
+
puts "***** test_one #{@prg} *****"
|
22
|
+
test(n)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test(n)
|
26
|
+
id = format('%<q>s_%<n>03d', q: @q, n: n)
|
27
|
+
files = %w[in out exp].map { |ext| "#{@smp_dir}/#{id}.#{ext}" }
|
28
|
+
run_test(id, *files)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'mechanize'
|
5
|
+
require 'logger'
|
6
|
+
require 'English'
|
7
|
+
|
8
|
+
module AtCoderFriends
|
9
|
+
# scrapes AtCoder contest site and
|
10
|
+
# - fetches problems
|
11
|
+
# - submits sources
|
12
|
+
class ScrapingAgent
|
13
|
+
include PathUtil
|
14
|
+
|
15
|
+
BASE_URL = 'https://atcoder.jp/'
|
16
|
+
XPATH_SECTION = '//h3[.="%<title>s"]/following-sibling::section'
|
17
|
+
LANG_TBL = {
|
18
|
+
'cxx' => '3003',
|
19
|
+
'cs' => '3006',
|
20
|
+
'java' => '3016',
|
21
|
+
'rb' => '3024'
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
attr_reader :contest, :config, :agent
|
25
|
+
|
26
|
+
def initialize(contest, config)
|
27
|
+
@contest = contest
|
28
|
+
@config = config
|
29
|
+
@agent = Mechanize.new
|
30
|
+
# @agent.log = Logger.new(STDERR)
|
31
|
+
end
|
32
|
+
|
33
|
+
def common_url(path)
|
34
|
+
File.join(BASE_URL, path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def contest_url(path)
|
38
|
+
File.join(BASE_URL, 'contests', contest, path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch_all
|
42
|
+
puts "***** fetch_all #{@contest} *****"
|
43
|
+
login
|
44
|
+
fetch_assignments.map do |q, url|
|
45
|
+
pbm = fetch_problem(q, url)
|
46
|
+
yield pbm if block_given?
|
47
|
+
pbm
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def submit(path)
|
52
|
+
path, _dir, prg, _base, ext, q = split_prg_path(path)
|
53
|
+
puts "***** submit #{prg} *****"
|
54
|
+
src = File.read(path, encoding: Encoding::UTF_8)
|
55
|
+
login
|
56
|
+
post_src(q, ext, src)
|
57
|
+
end
|
58
|
+
|
59
|
+
def login
|
60
|
+
sleep 0.1
|
61
|
+
page = agent.get(common_url('login'))
|
62
|
+
form = page.forms[1]
|
63
|
+
form.field_with(name: 'username').value = config['user']
|
64
|
+
form.field_with(name: 'password').value = config['password']
|
65
|
+
sleep 0.1
|
66
|
+
form.submit
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch_assignments
|
70
|
+
url = contest_url('tasks')
|
71
|
+
puts "fetch list from #{url} ..."
|
72
|
+
sleep 0.1
|
73
|
+
page = agent.get(url)
|
74
|
+
('A'..'Z').each_with_object({}) do |q, h|
|
75
|
+
link = page.link_with(text: q)
|
76
|
+
link && h[q] = link.href
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def fetch_problem(q, url)
|
81
|
+
puts "fetch problem from #{url} ..."
|
82
|
+
sleep 0.1
|
83
|
+
page = agent.get(url)
|
84
|
+
Problem.new(q) do |pbm|
|
85
|
+
pbm.html = page.body
|
86
|
+
if contest == 'arc001'
|
87
|
+
page.search('//h3').each do |h3|
|
88
|
+
query = format(XPATH_SECTION, title: h3.content)
|
89
|
+
sections = page.search(query)
|
90
|
+
sections[0] && parse_section(pbm, h3, sections[0])
|
91
|
+
end
|
92
|
+
else
|
93
|
+
page.search('//*[./h3]').each do |section|
|
94
|
+
h3 = section.search('h3')[0]
|
95
|
+
parse_section(pbm, h3, section)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_section(pbm, h3, section)
|
102
|
+
title = h3.content.strip
|
103
|
+
title.delete!("\u008f\u0090") # agc002
|
104
|
+
text = section.content
|
105
|
+
code = section.search('pre')[0]&.content || ''
|
106
|
+
case title
|
107
|
+
when /^制約$/
|
108
|
+
pbm.desc += text
|
109
|
+
when /^入出?力$/
|
110
|
+
pbm.desc += text
|
111
|
+
pbm.fmt = code
|
112
|
+
when /^入力例\s*(?<no>[\d0-9]+)$/
|
113
|
+
pbm.add_smp($LAST_MATCH_INFO[:no], :in, code)
|
114
|
+
when /^出力例\s*(?<no>[\d0-9]+)$/
|
115
|
+
pbm.add_smp($LAST_MATCH_INFO[:no], :exp, code)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def post_src(q, ext, src)
|
120
|
+
lang_id = LANG_TBL[ext.downcase]
|
121
|
+
raise AppError, ".#{ext} is not available." unless lang_id
|
122
|
+
sleep 0.1
|
123
|
+
page = agent.get(contest_url('submit'))
|
124
|
+
form = page.forms[1]
|
125
|
+
form.field_with(name: 'data.TaskScreenName') do |sel|
|
126
|
+
option = sel.options.find { |op| op.text.start_with?(q) }
|
127
|
+
option&.select || (raise AppError, "unknown problem:#{q}.")
|
128
|
+
end
|
129
|
+
form.add_field!('data.LanguageId', lang_id)
|
130
|
+
form.field_with(name: 'sourceCode').value = src
|
131
|
+
sleep 0.1
|
132
|
+
form.submit
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rbconfig'
|
4
|
+
|
5
|
+
module AtCoderFriends
|
6
|
+
# run tests for the specified program.
|
7
|
+
class TestRunner
|
8
|
+
include PathUtil
|
9
|
+
|
10
|
+
def initialize(path)
|
11
|
+
@path, @dir, @prg, @base, @ext, @q = split_prg_path(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
# rubocop:disable Metrics/MethodLength
|
15
|
+
def run_test(id, infile, outfile, expfile)
|
16
|
+
return false unless File.exist?(infile) && File.exist?(expfile)
|
17
|
+
|
18
|
+
puts "==== #{id} ===="
|
19
|
+
ec = system("#{edit_cmd} < #{infile} > #{outfile}")
|
20
|
+
|
21
|
+
input, result, expected =
|
22
|
+
[infile, outfile, expfile].map { |file| File.read(file) }
|
23
|
+
puts '-- input --'
|
24
|
+
print input
|
25
|
+
puts '-- expected --'
|
26
|
+
print expected
|
27
|
+
puts '-- result --'
|
28
|
+
print result
|
29
|
+
if !ec
|
30
|
+
puts '!!!!! RE !!!!!'
|
31
|
+
elsif result != expected
|
32
|
+
puts '!!!!! WA !!!!!'
|
33
|
+
else
|
34
|
+
puts '<< OK >>'
|
35
|
+
end
|
36
|
+
true
|
37
|
+
end
|
38
|
+
# rubocop:enable Metrics/MethodLength
|
39
|
+
|
40
|
+
# rubocop:disable Metrics/MethodLength
|
41
|
+
def edit_cmd
|
42
|
+
case @ext
|
43
|
+
when 'java'
|
44
|
+
"java -cp #{@dir} Main"
|
45
|
+
when 'rb'
|
46
|
+
"ruby #{@dir}/#{@base}.rb"
|
47
|
+
when 'cs'
|
48
|
+
case which_os
|
49
|
+
when :windows
|
50
|
+
"#{@dir}/#{@base}.exe"
|
51
|
+
else
|
52
|
+
"mono #{@dir}/#{@base}.exe"
|
53
|
+
end
|
54
|
+
else # c, cxx
|
55
|
+
case which_os
|
56
|
+
when :windows
|
57
|
+
"#{@dir}/#{@base}.exe"
|
58
|
+
else
|
59
|
+
"#{@dir}/#{@base}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
# rubocop:enable Metrics/MethodLength
|
64
|
+
|
65
|
+
def which_os
|
66
|
+
@os ||= begin
|
67
|
+
case RbConfig::CONFIG['host_os']
|
68
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
69
|
+
:windows
|
70
|
+
when /darwin|mac os/
|
71
|
+
:macosx
|
72
|
+
when /linux/
|
73
|
+
:linux
|
74
|
+
when /solaris|bsd/
|
75
|
+
:unix
|
76
|
+
else
|
77
|
+
:unknown
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|