at_coder_friends 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ user: <user>
2
+ password: <password>
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'at_coder_friends'
5
+
6
+ ec = AtCoderFriends::CLI.new.run
7
+
8
+ exit ec
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'at_coder_friends/version'
4
+ require 'at_coder_friends/errors'
5
+ require 'at_coder_friends/path_util'
6
+ require 'at_coder_friends/config_loader'
7
+ require 'at_coder_friends/verifier'
8
+ require 'at_coder_friends/test_runner'
9
+ require 'at_coder_friends/sample_test_runner'
10
+ require 'at_coder_friends/judge_test_runner'
11
+ require 'at_coder_friends/problem'
12
+ require 'at_coder_friends/scraping_agent'
13
+ require 'at_coder_friends/format_parser'
14
+ require 'at_coder_friends/ruby_generator'
15
+ require 'at_coder_friends/cxx_generator'
16
+ require 'at_coder_friends/emitter'
17
+ require 'at_coder_friends/cli'
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module AtCoderFriends
6
+ # command line interface
7
+ class CLI
8
+ include PathUtil
9
+
10
+ EXITING_OPTIONS = %i[version].freeze
11
+ OPTION_BANNER =
12
+ <<~TEXT
13
+ Usage:
14
+ at_coder_friends setup path/contest # setup contest folder
15
+ at_coder_friends test-one path/contest/src # run 1st test case
16
+ at_coder_friends test-all path/contest/src # run all test cases
17
+ at_coder_friends submit path/contest/src # submit source code
18
+ Options:
19
+ TEXT
20
+ STATUS_SUCCESS = 0
21
+ STATUS_ERROR = 1
22
+
23
+ def run(args = ARGV)
24
+ parse_options!(args)
25
+ handle_exiting_option
26
+ raise ParamError, 'command or path is not specified.' if args.size < 2
27
+ @config = ConfigLoader.load_config(args[1])
28
+ exec_command(*args)
29
+ STATUS_SUCCESS
30
+ rescue AtCoderFriends::ParamError => e
31
+ warn @usage
32
+ warn "error: #{e.message}"
33
+ STATUS_ERROR
34
+ rescue AtCoderFriends::AppError => e
35
+ warn e.message
36
+ STATUS_ERROR
37
+ rescue SystemExit => e
38
+ e.status
39
+ end
40
+
41
+ def parse_options!(args)
42
+ op = OptionParser.new do |opts|
43
+ opts.banner = OPTION_BANNER
44
+ opts.on('-v', '--version', 'Display version.') do
45
+ @options[:version] = true
46
+ end
47
+ end
48
+ @usage = op.to_s
49
+ @options = {}
50
+ op.parse!(args)
51
+ rescue OptionParser::InvalidOption => e
52
+ raise ParamError, e.message
53
+ end
54
+
55
+ def handle_exiting_option
56
+ return unless EXITING_OPTIONS.any? { |o| @options.key? o }
57
+ puts AtCoderFriends::VERSION if @options[:version]
58
+ exit STATUS_SUCCESS
59
+ end
60
+
61
+ def exec_command(command, path, id = nil)
62
+ case command
63
+ when 'setup'
64
+ setup(path)
65
+ when 'test-one'
66
+ test_one(path, id)
67
+ when 'test-all'
68
+ test_all(path)
69
+ when 'submit'
70
+ submit(path)
71
+ when 'judge-one'
72
+ judge_one(path, id)
73
+ when 'judge-all'
74
+ judge_all(path)
75
+ else
76
+ raise ParamError, "unknown command: #{command}"
77
+ end
78
+ end
79
+
80
+ def setup(path)
81
+ raise AppError, "#{path} is not empty." \
82
+ if Dir.exist?(path) && !Dir["#{path}/*"].empty?
83
+ agent = ScrapingAgent.new(contest_name(path), @config)
84
+ parser = FormatParser.new
85
+ rb_gen = RubyGenerator.new
86
+ cxx_gen = CxxGenerator.new
87
+ emitter = Emitter.new(path)
88
+ agent.fetch_all do |pbm|
89
+ parser.process(pbm)
90
+ rb_gen.process(pbm)
91
+ cxx_gen.process(pbm)
92
+ emitter.emit(pbm)
93
+ end
94
+ end
95
+
96
+ def test_one(path, id)
97
+ id ||= 1
98
+ SampleTestRunner.new(path).test_one(id)
99
+ end
100
+
101
+ def test_all(path)
102
+ SampleTestRunner.new(path).test_all
103
+ Verifier.new(path).verify
104
+ end
105
+
106
+ def submit(path)
107
+ vf = Verifier.new(path)
108
+ raise AppError, "#{vf.file} has not been tested." unless vf.verified?
109
+ ScrapingAgent.new(contest_name(path), @config).submit(path)
110
+ vf.unverify
111
+ end
112
+
113
+ def judge_one(path, id)
114
+ id ||= ''
115
+ JudgeTestRunner.new(path).judge_one(id)
116
+ end
117
+
118
+ def judge_all(path)
119
+ JudgeTestRunner.new(path).judge_all
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'yaml'
5
+
6
+ module AtCoderFriends
7
+ # loads configuration file from the specified directory.
8
+ class ConfigLoader
9
+ DOTFILE = '.at_coder_friends.yml'
10
+
11
+ class << self
12
+ def load_config(target_dir)
13
+ path = find_file_upwards(DOTFILE, target_dir)
14
+ load_yaml(path)
15
+ end
16
+
17
+ def find_file_upwards(filename, start_dir)
18
+ Pathname.new(start_dir).expand_path.ascend do |dir|
19
+ file = dir + filename
20
+ return file.to_s if file.exist?
21
+ end
22
+ raise ConfigNotFoundError,
23
+ "Configuration file not found: #{start_dir}"
24
+ end
25
+
26
+ def load_yaml(path)
27
+ yaml = IO.read(path, encoding: Encoding::UTF_8)
28
+ YAML.safe_load(yaml, [], [], false, path)
29
+ rescue Errno::ENOENT
30
+ raise ConfigNotFoundError,
31
+ "Configuration file not found: #{path}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtCoderFriends
4
+ # generates C++ source code from definition
5
+ class CxxGenerator
6
+ # rubocop:disable Style/FormatStringToken
7
+ TEMPLATE = <<~TEXT
8
+ #include <cstdio>
9
+
10
+ using namespace std;
11
+
12
+ #define REP(i,n) for(int i=0; i<(int)(n); i++)
13
+ #define FOR(i,b,e) for(int i=(b); i<=(int)(e); i++)
14
+
15
+ /*** CONSTS ***/
16
+
17
+ /*** DCLS ***/
18
+
19
+ void solve() {
20
+ int ans = 0;
21
+ printf("%d\\n", ans);
22
+ }
23
+
24
+ void input() {
25
+ /*** READS ***/
26
+ }
27
+
28
+ int main() {
29
+ input();
30
+ solve();
31
+ return 0;
32
+ }
33
+ TEXT
34
+ # rubocop:enable Style/FormatStringToken
35
+
36
+ SCANF_FMTS = [
37
+ 'scanf("%<fmt>s", %<addr>s);',
38
+ 'REP(i, %<sz1>s) scanf("%<fmt>s", %<addr>s);',
39
+ 'REP(i, %<sz1>s) REP(j, %<sz2>s) scanf("%<fmt>s", %<addr>s);'
40
+ ].freeze
41
+
42
+ # rubocop:disable Style/FormatStringToken
43
+ FMT_FMTS = { number: '%d', string: '%s', char: '%s' }.freeze
44
+ # rubocop:enable Style/FormatStringToken
45
+
46
+ ADDR_FMTS = {
47
+ single: {
48
+ number: '&%<v>s',
49
+ string: '%<v>s'
50
+ },
51
+ harray: {
52
+ number: '%<v>s + i',
53
+ string: '%<v>s[i]',
54
+ char: '%<v>s'
55
+ },
56
+ varray: {
57
+ number: '%<v>s + i',
58
+ string: '%<v>s[i]'
59
+ },
60
+ matrix: {
61
+ number: '&%<v>s[i][j]',
62
+ string: '%<v>s[i][j]',
63
+ char: '%<v>s[i]'
64
+ }
65
+ }.freeze
66
+
67
+ def process(pbm)
68
+ src = generate(pbm.defs, pbm.desc)
69
+ pbm.add_src(:cxx, src)
70
+ end
71
+
72
+ def generate(defs, desc)
73
+ consts = gen_consts(desc)
74
+ dcls = gen_decls(defs)
75
+ reads = gen_reads(defs)
76
+ TEMPLATE
77
+ .sub('/*** CONSTS ***/', consts.join("\n"))
78
+ .sub('/*** DCLS ***/', dcls.join("\n"))
79
+ .sub('/*** READS ***/', reads.map { |s| ' ' + s }.join("\n"))
80
+ end
81
+
82
+ def gen_consts(desc)
83
+ desc
84
+ .gsub(/[,\\\(\)\{\}\|]/, '')
85
+ .gsub(/(≤|leq)/i, '≦')
86
+ .scan(/([\da-z_]+)\s*≦\s*(\d+)(?:\^(\d+))?/i)
87
+ .map do |v, sz, k|
88
+ sz = sz.to_i
89
+ sz **= k.to_i if k
90
+ "const int #{v.upcase}_MAX = #{sz};"
91
+ end
92
+ end
93
+
94
+ def gen_decls(defs)
95
+ defs.map { |inpdef| gen_decl(inpdef) }.flatten
96
+ end
97
+
98
+ def gen_decl(inpdef)
99
+ case inpdef.container
100
+ when :single
101
+ gen_single_decl(inpdef)
102
+ when :harray
103
+ gen_harray_decl(inpdef)
104
+ when :varray
105
+ gen_varray_decl(inpdef)
106
+ when :matrix
107
+ gen_matrix_decl(inpdef)
108
+ end
109
+ end
110
+
111
+ def gen_single_decl(inpdef)
112
+ names = inpdef.names
113
+ case inpdef.item
114
+ when :number
115
+ dcl = names.join(', ')
116
+ "int #{dcl};"
117
+ when :string
118
+ names.map { |v| "char #{v}[#{v.upcase}_MAX + 1];" }
119
+ end
120
+ end
121
+
122
+ def gen_harray_decl(inpdef)
123
+ v = inpdef.names[0]
124
+ sz = gen_arr_size(inpdef.size)[0]
125
+ case inpdef.item
126
+ when :number
127
+ "int #{v}[#{sz}];"
128
+ when :string
129
+ "char #{v}[#{sz}][#{v.upcase}_MAX + 1];"
130
+ when :char
131
+ "char #{v}[#{sz} + 1];"
132
+ end
133
+ end
134
+
135
+ def gen_varray_decl(inpdef)
136
+ names = inpdef.names
137
+ sz = gen_arr_size(inpdef.size)[0]
138
+ case inpdef.item
139
+ when :number
140
+ names.map { |v| "int #{v}[#{sz}];" }
141
+ when :string
142
+ names.map { |v| "char #{v}[#{sz}][#{v.upcase}_MAX + 1];" }
143
+ end
144
+ end
145
+
146
+ def gen_matrix_decl(inpdef)
147
+ v = inpdef.names[0]
148
+ sz1, sz2 = gen_arr_size(inpdef.size)
149
+ case inpdef.item
150
+ when :number
151
+ "int #{v}[#{sz1}][#{sz2}];"
152
+ when :string
153
+ "char #{v}[#{sz1}][#{sz2}][#{v.upcase}_MAX + 1];"
154
+ when :char
155
+ "char #{v}[#{sz1}][#{sz2} + 1];"
156
+ end
157
+ end
158
+
159
+ def gen_arr_size(szs)
160
+ szs.map { |sz| sz =~ /\D/ ? "#{sz.upcase}_MAX" : sz }
161
+ end
162
+
163
+ def gen_reads(defs)
164
+ defs.map { |inpdef| gen_read(inpdef) }.flatten
165
+ end
166
+
167
+ # rubocop:disable Metrics/AbcSize
168
+ def gen_read(inpdef)
169
+ dim = inpdef.size.size - (inpdef.item == :char ? 1 : 0)
170
+ scanf = SCANF_FMTS[dim]
171
+ sz1, sz2 = inpdef.size
172
+ fmt = FMT_FMTS[inpdef.item] * inpdef.names.size
173
+ addr_fmt = ADDR_FMTS[inpdef.container][inpdef.item]
174
+ addr = inpdef.names.map { |v| format(addr_fmt, v: v) }.join(', ')
175
+ format(scanf, sz1: sz1, sz2: sz2, fmt: fmt, addr: addr)
176
+ end
177
+ # rubocop:enable Metrics/AbcSize
178
+ end
179
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module AtCoderFriends
6
+ # emits source skeletons and sample input/output(s)
7
+ # of a problem to the specified directory.
8
+ class Emitter
9
+ include PathUtil
10
+
11
+ def initialize(dir)
12
+ @src_dir = dir
13
+ @smp_dir = smp_dir(dir)
14
+ end
15
+
16
+ def emit(pbm)
17
+ pbm.smps.each { |smp| emit_sample(pbm, smp) }
18
+ pbm.srcs.each { |src| emit_source(pbm, src) }
19
+ end
20
+
21
+ def emit_sample(pbm, smp)
22
+ makedirs_unless @smp_dir
23
+ smp_file = format(
24
+ '%<q>s_%<n>03d.%<ext>s', q: pbm.q, n: smp.no, ext: smp.ext
25
+ )
26
+ smp_path = File.join(@smp_dir, smp_file)
27
+ File.write(smp_path, smp.txt)
28
+ puts smp_file
29
+ end
30
+
31
+ def emit_source(pbm, src)
32
+ makedirs_unless @src_dir
33
+ src_file = format('%<q>s.%<ext>s', q: pbm.q, ext: src.ext)
34
+ src_path = File.join(@src_dir, src_file)
35
+ File.write(src_path, src.txt)
36
+ puts src_file
37
+ end
38
+
39
+ def makedirs_unless(dir)
40
+ FileUtils.makedirs(dir) unless Dir.exist?(dir)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtCoderFriends
4
+ class AppError < StandardError; end
5
+ class ParamError < AppError; end
6
+ class ConfigNotFoundError < AppError; end
7
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtCoderFriends
4
+ # Iterates through elements of an array
5
+ class Iterator
6
+ def initialize(array)
7
+ @array = array
8
+ @i = 0
9
+ end
10
+
11
+ def next?
12
+ @i < @array.size
13
+ end
14
+
15
+ def next
16
+ ret = @array[@i]
17
+ @i += 1
18
+ ret
19
+ end
20
+ end
21
+
22
+ # parses input data format and generates input definitons
23
+ class FormatParser
24
+ PARSERS = [
25
+ {
26
+ container: :harray,
27
+ item: :number,
28
+ pat: /^(?<v>[a-z]+)[01](\s+\k<v>.)*(\s+\.+)?(\s+\k<v>.)+$/i,
29
+ names: ->(m) { [m[:v]] },
30
+ pat2: ->(_) { nil },
31
+ size: ->(f) { [f[-1]] }
32
+ },
33
+ {
34
+ container: :harray,
35
+ item: :char,
36
+ pat: /^(?<v>[a-z]+)[01](\k<v>.)*(\s*\.+\s*)?(\k<v>.)+$/i,
37
+ names: ->(m) { [m[:v]] },
38
+ pat2: ->(_) { nil },
39
+ size: ->(f) { [f[-1]] }
40
+ },
41
+ {
42
+ container: :matrix,
43
+ item: :number,
44
+ pat: /^(?<v>[a-z]+)[01][01](\s+\k<v>..)*(\s+\.+)?(\s+\k<v>..)+$/i,
45
+ names: ->(m) { [m[:v]] },
46
+ pat2: ->(v) { /(^#{v}..(\s+#{v}..)*(\s+\.+)?(\s+#{v}..)+|\.+)$/ },
47
+ size: ->(f) { f[-2..-1].chars.to_a }
48
+ },
49
+ {
50
+ container: :matrix,
51
+ item: :char,
52
+ pat: /^(?<v>[a-z]+)[01][01](\k<v>..)*(\s*\.+\s*)?(\k<v>..)+$/i,
53
+ names: ->(m) { [m[:v]] },
54
+ pat2: ->(v) { /(^#{v}..(#{v}..)*(\s*\.+\s*)?(#{v}..)+|\.+)$/ },
55
+ size: ->(f) { f[-2..-1].chars.to_a }
56
+ },
57
+ {
58
+ container: :varray,
59
+ item: :number,
60
+ pat: /^[a-z]+(?<i>[0-9])(\s+[a-z]+\k<i>)*$/i,
61
+ names: ->(m) { m[0].split.map { |w| w[0..-2] } },
62
+ pat2: lambda { |vs|
63
+ pat = vs.map { |v| v + '.+' }.join('\s+')
64
+ /^(#{pat}|\.+)$/
65
+ },
66
+ size: ->(f) { /(?<sz>\d+)$/ =~ f ? [sz] : [f[-1]] }
67
+ },
68
+ {
69
+ container: :single,
70
+ item: :number,
71
+ pat: /^[a-z]+(\s+[a-z]+)*$/i,
72
+ names: ->(m) { m[0].split },
73
+ pat2: ->(_) { nil },
74
+ size: ->(_) { [] }
75
+ }
76
+ ].freeze
77
+
78
+ def process(pbm)
79
+ defs = parse(pbm.fmt, pbm.smps)
80
+ pbm.defs = defs
81
+ end
82
+
83
+ def parse(fmt, smps)
84
+ lines = split_trim(fmt)
85
+ defs = parse_fmt(lines)
86
+ smpx = max_smp(smps)
87
+ return defs unless smpx
88
+ match_smp!(defs, smpx)
89
+ end
90
+
91
+ def split_trim(fmt)
92
+ fmt
93
+ .gsub(/[+-]1/, '') # N-1, N+1 -> N
94
+ .gsub(%r{[-/ ]}, ' ') # a-b, a/b -> a b
95
+ .gsub(/\{.*?\}/) { |w| w.delete(' ') } # {1, 1} -> {1,1} shortest match
96
+ .gsub(/[_,'\\\(\)\{\}]/, '')
97
+ .gsub(/[::…‥]+/, '..')
98
+ .gsub(/^[\.\s]+$/, '..')
99
+ .split("\n")
100
+ .map(&:strip)
101
+ end
102
+
103
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
104
+ def parse_fmt(lines)
105
+ it = Iterator.new(lines + ['']) # sentinel
106
+ prv = nil
107
+ cur = it.next
108
+ Enumerator.new do |y|
109
+ loop do
110
+ unless (parser = PARSERS.find { |ps| ps[:pat] =~ cur })
111
+ puts "unknown format: #{cur}" unless cur.empty?
112
+ (cur = it.next) ? next : break
113
+ end
114
+ container, item = parser.values_at(:container, :item)
115
+ m = parser[:pat].match(cur)
116
+ names = parser[:names].call(m)
117
+ pat2 = parser[:pat2].call(names)
118
+ loop do
119
+ prv = cur
120
+ cur = it.next
121
+ break unless pat2 && pat2 =~ cur
122
+ end
123
+ size = parser[:size].call(prv)
124
+ y << InputDef.new(container, item, names, size)
125
+ end
126
+ end.to_a
127
+ end
128
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
129
+
130
+ def max_smp(smps)
131
+ smps
132
+ .select { |smp| smp.ext == :in }
133
+ .max_by { |smp| smp.txt.size }
134
+ &.txt
135
+ end
136
+
137
+ def match_smp!(inpdefs, smp)
138
+ lines = smp.split("\n")
139
+ inpdefs.each_with_index do |inpdef, i|
140
+ break if i > lines.size
141
+ next if inpdef.item != :number
142
+ inpdef.item = :string if lines[i].split[0] =~ /[^\-0-9]/
143
+ break if %i[varray matrix].include?(inpdef.container)
144
+ end
145
+ inpdefs
146
+ end
147
+ end
148
+ end