at_coder_friends 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|