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.
@@ -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