gettc 1.10 → 2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/gettc +60 -69
- data/dist/config.yml +1 -1
- data/dist/include/cpp/engine.rb +78 -86
- data/dist/include/cpp/topcoder +236 -236
- data/dist/include/go/engine.rb +53 -61
- data/dist/include/haskell/engine.rb +112 -122
- data/dist/include/java/engine.rb +187 -184
- data/dist/include/javascript/engine.rb +26 -30
- data/dist/include/javascript/topcoder.js +3 -3
- data/dist/include/javascript/topcoder/errors.js +5 -5
- data/dist/include/javascript/topcoder/reader.js +188 -165
- data/dist/include/javascript/topcoder/writer.js +37 -33
- data/dist/include/python/engine.rb +43 -52
- data/dist/include/python/topcoder/__init__.pyc +0 -0
- data/dist/include/python/topcoder/__pycache__/__init__.cpython-34.pyc +0 -0
- data/dist/include/python/topcoder/__pycache__/errors.cpython-34.pyc +0 -0
- data/dist/include/python/topcoder/__pycache__/reader.cpython-34.pyc +0 -0
- data/dist/include/python/topcoder/__pycache__/writer.cpython-34.pyc +0 -0
- data/dist/include/python/topcoder/errors.pyc +0 -0
- data/dist/include/python/topcoder/reader.pyc +0 -0
- data/dist/include/python/topcoder/writer.pyc +0 -0
- data/dist/include/ruby/engine.rb +47 -52
- data/dist/include/ruby/topcoder/reader.rb +205 -193
- data/dist/include/ruby/topcoder/writer.rb +39 -37
- data/dist/template/bin/runner.rb +146 -151
- data/dist/template/bin/runner.sh +96 -96
- data/dist/template/prob/{name}.html +1 -1
- data/dist/template/solve/cpp/{name}.cpp +3 -4
- data/dist/template/solve/cpp/{name}Solver.cpp +14 -14
- data/dist/template/solve/go/{name}/{name}.go +3 -3
- data/dist/template/solve/go/{name}Solver.go +2 -2
- data/dist/template/solve/haskell/{name}.hs +6 -6
- data/dist/template/solve/haskell/{name}Solver.hs +10 -10
- data/dist/template/solve/java/{name}.java +4 -4
- data/dist/template/solve/java/{name}Solver.java +4 -4
- data/dist/template/solve/javascript/{name}.js +4 -6
- data/dist/template/solve/javascript/{name}Solver.js +11 -9
- data/dist/template/solve/python/{name}.py +1 -1
- data/dist/template/solve/python/{name}Solver.py +2 -2
- data/dist/template/solve/ruby/{name}.rb +4 -4
- data/dist/template/solve/ruby/{name}Solver.rb +14 -17
- data/dist/template/util/check/check.cpp +19 -19
- data/{core/lib → lib}/gettc.rb +0 -0
- data/lib/gettc/account.rb +14 -0
- data/lib/gettc/download.rb +211 -0
- data/lib/gettc/generate.rb +156 -0
- data/lib/gettc/parse.rb +237 -0
- data/lib/gettc/print.rb +54 -0
- data/lib/gettc/problem.rb +39 -0
- data/lib/gettc/signature.rb +63 -0
- data/lib/gettc/types.rb +93 -0
- data/lib/version.rb +3 -0
- data/test/gettc/download_test.rb +61 -0
- data/test/gettc/generate_test.rb +70 -0
- data/test/gettc/parse_test.rb +78 -0
- data/test/gettc/signature_test.rb +71 -0
- data/test/gettc/types_test.rb +31 -0
- metadata +28 -23
- data/core/lib/gettc/download.rb +0 -130
- data/core/lib/gettc/generate.rb +0 -145
- data/core/lib/gettc/parse.rb +0 -233
- data/core/lib/gettc/print.rb +0 -56
- data/core/lib/gettc/problem.rb +0 -33
- data/core/lib/gettc/signature.rb +0 -55
- data/core/lib/gettc/types.rb +0 -83
- data/core/lib/version.rb +0 -3
- data/core/test/gettc/download_test.rb +0 -29
- data/core/test/gettc/generate_test.rb +0 -31
- data/core/test/gettc/parse_test.rb +0 -104
- data/core/test/gettc/signature_test.rb +0 -54
- data/core/test/gettc/types_test.rb +0 -28
@@ -0,0 +1,156 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "pathname"
|
3
|
+
require "erb"
|
4
|
+
|
5
|
+
require "gettc/problem"
|
6
|
+
require "gettc/signature"
|
7
|
+
require "gettc/print"
|
8
|
+
|
9
|
+
module Gettc
|
10
|
+
GenerateError = Class.new StandardError
|
11
|
+
|
12
|
+
class ProblemDirExists < GenerateError
|
13
|
+
attr_accessor :dir
|
14
|
+
|
15
|
+
def initialize(dir, message = "Problem directory already exists")
|
16
|
+
@dir = dir
|
17
|
+
super "#{message}: (#{dir})"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class SourceDirNotExist < GenerateError
|
22
|
+
attr_accessor :dir
|
23
|
+
|
24
|
+
def initialize(dir, message = "Source directory does not exist")
|
25
|
+
@dir = dir
|
26
|
+
super "#{message}: (#{dir})"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class TargetDirNotExist < GenerateError
|
31
|
+
attr_accessor :dir
|
32
|
+
|
33
|
+
def initialize(dir, message = "Target directory does not exist")
|
34
|
+
@dir = dir
|
35
|
+
super "#{message}: (#{dir})"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class TemplateError < GenerateError
|
40
|
+
attr_accessor :dir
|
41
|
+
|
42
|
+
def initialize(err, source, message = "Template error")
|
43
|
+
@err = err
|
44
|
+
@source = source
|
45
|
+
super "#{message} (#{source})\n#{err}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Generator
|
50
|
+
def initialize(config_dir, target_dir)
|
51
|
+
@source_dir = File.join(config_dir, "template")
|
52
|
+
raise SourceDirNotExist.new(@source_dir) unless File.directory?(@source_dir)
|
53
|
+
|
54
|
+
@target_dir = target_dir
|
55
|
+
raise TargetDirNotExist.new(@target_dir) unless File.directory?(@target_dir)
|
56
|
+
|
57
|
+
load_engines(File.join(config_dir, "include"))
|
58
|
+
end
|
59
|
+
|
60
|
+
def generate(prob)
|
61
|
+
@prob = prob
|
62
|
+
|
63
|
+
@problem_dir = File.join(@target_dir, prob.name)
|
64
|
+
raise ProblemDirExists.new(@problem_dir) if File.exists?(@problem_dir)
|
65
|
+
FileUtils.mkdir(@problem_dir)
|
66
|
+
|
67
|
+
method_sig = @prob.definitions["Method signature"]
|
68
|
+
if method_sig.nil?
|
69
|
+
$stderr.puts "[Warning] No definition for method signature found"
|
70
|
+
else
|
71
|
+
vars = parse_method_signature(method_sig)
|
72
|
+
func = vars.shift
|
73
|
+
end
|
74
|
+
@context = binding
|
75
|
+
|
76
|
+
walk(@source_dir, @problem_dir)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def generate_images(images, images_dir)
|
82
|
+
images.each do |image|
|
83
|
+
filename = File.join(images_dir, image.name)
|
84
|
+
File.open(filename, "wb") { |f| f.write(image.content) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def generate_test_case(cases, data_dir)
|
89
|
+
cases.each_with_index do |case_data, case_id|
|
90
|
+
File.write(File.join(data_dir, "#{case_id.to_s}.in"), case_data.input)
|
91
|
+
File.write(File.join(data_dir, "#{case_id.to_s}.out"), case_data.output)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def generate_template(source, target)
|
96
|
+
before = File.read(source)
|
97
|
+
|
98
|
+
begin
|
99
|
+
after = ERB.new(before).result(@context)
|
100
|
+
rescue SyntaxError, NameError => err
|
101
|
+
raise TemplateError.new(err, source)
|
102
|
+
end
|
103
|
+
|
104
|
+
begin
|
105
|
+
File.write(target, after)
|
106
|
+
rescue StandardError
|
107
|
+
raise target
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def filter(target_dir, name)
|
112
|
+
case name
|
113
|
+
when "{images_d}"
|
114
|
+
generate_images(@prob.images, target_dir)
|
115
|
+
when "{examples_d}"
|
116
|
+
generate_test_case(@prob.examples, target_dir)
|
117
|
+
when "{systests_d}"
|
118
|
+
generate_test_case(@prob.systests, target_dir)
|
119
|
+
else
|
120
|
+
return name.gsub(/\{(\w*)\}/) { |match| @prob.name if $1 == "name" }
|
121
|
+
end
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def load_engines(include_dir)
|
126
|
+
return unless File.exists?(include_dir)
|
127
|
+
Dir.foreach(include_dir) do |name|
|
128
|
+
if File.directory?(child = File.join(include_dir, name))
|
129
|
+
if File.exists?(engine = File.join(child, "engine.rb"))
|
130
|
+
engine = "./#{engine}" unless Pathname.new(engine).absolute?
|
131
|
+
require engine
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def walk(source_parent, target_parent)
|
138
|
+
Dir.foreach(source_parent) do |name|
|
139
|
+
next if [".", ".."].include?(name)
|
140
|
+
|
141
|
+
target_name = filter(target_parent, name)
|
142
|
+
next if target_name.nil?
|
143
|
+
|
144
|
+
source_child = File.join(source_parent, name)
|
145
|
+
target_child = File.join(target_parent, target_name)
|
146
|
+
|
147
|
+
if File.directory?(source_child)
|
148
|
+
FileUtils.mkdir(target_child) unless File.exists?(target_child)
|
149
|
+
walk(source_child, target_child)
|
150
|
+
elsif File.file?(source_child)
|
151
|
+
generate_template(source_child, target_child)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/lib/gettc/parse.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
require "gettc/problem"
|
2
|
+
require "gettc/download"
|
3
|
+
|
4
|
+
require "uri"
|
5
|
+
require "pathname"
|
6
|
+
require "hpricot"
|
7
|
+
require "logger"
|
8
|
+
|
9
|
+
module Gettc
|
10
|
+
class Parser
|
11
|
+
def initialize(downloader)
|
12
|
+
@downloader = downloader
|
13
|
+
@problem = Problem.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(problem_id)
|
17
|
+
doc = Hpricot(@downloader.download_statement(problem_id))
|
18
|
+
|
19
|
+
@problem = Problem.new
|
20
|
+
@problem.id = problem_id
|
21
|
+
@problem.name = parse_name(doc.search("tr/td.statTextBig").html)
|
22
|
+
|
23
|
+
html = doc.search("td.problemText/table").html
|
24
|
+
|
25
|
+
has_notes = true
|
26
|
+
has_constraints = true
|
27
|
+
has_examples = true
|
28
|
+
|
29
|
+
_, x = self.class.indexes(html, self.class.h3("Problem Statement"))
|
30
|
+
y, z = self.class.indexes(html, self.class.h3("Definition"))
|
31
|
+
parse_statement(html[x .. y])
|
32
|
+
|
33
|
+
x, y = self.class.indexes(html, self.class.h3("Notes"))
|
34
|
+
if x.nil?
|
35
|
+
has_notes = false
|
36
|
+
x, y = self.class.indexes(html, self.class.h3("Constraints"))
|
37
|
+
if x.nil?
|
38
|
+
has_constraints = false
|
39
|
+
x, y = self.class.indexes(html, self.class.h3("Examples"))
|
40
|
+
if x.nil?
|
41
|
+
has_examples = false
|
42
|
+
x = -2
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
parse_definitions(html[z .. x])
|
48
|
+
|
49
|
+
if has_notes
|
50
|
+
z, x = self.class.indexes(html, self.class.h3("Constraints"))
|
51
|
+
if z.nil?
|
52
|
+
has_constraints = false
|
53
|
+
z, x = self.class.indexes(html, self.class.h3("Examples"))
|
54
|
+
if z.nil?
|
55
|
+
has_examples = false
|
56
|
+
z = - 2
|
57
|
+
end
|
58
|
+
end
|
59
|
+
parse_notes(html[y .. z])
|
60
|
+
x, y = z, x
|
61
|
+
end
|
62
|
+
|
63
|
+
if has_constraints
|
64
|
+
z, x = self.class.indexes(html, self.class.h3("Examples"))
|
65
|
+
if z.nil?
|
66
|
+
has_examples = false
|
67
|
+
z = -2
|
68
|
+
end
|
69
|
+
parse_constraints(html[y .. z])
|
70
|
+
end
|
71
|
+
|
72
|
+
parse_examples(html[x .. -2]) if has_examples
|
73
|
+
parse_details(doc)
|
74
|
+
|
75
|
+
@problem
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def self.indexes(str, substr)
|
81
|
+
from = str.index(substr)
|
82
|
+
return nil if from.nil?
|
83
|
+
return from - 1, from + substr.size
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.h3(tag)
|
87
|
+
"<h3>#{tag}</h3>"
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.filter_inout(text)
|
91
|
+
text.gsub("{", "[").gsub("}", "]").strip
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.get_param_value(url, param_key)
|
95
|
+
if (match_data = url.match("[&|&]#{param_key}=(\\d+)"))
|
96
|
+
match_data[1] if match_data.size > 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def filter(html)
|
101
|
+
html = html.force_encoding("ISO-8859-1")
|
102
|
+
.encode("utf-8", {
|
103
|
+
invaid: :replace,
|
104
|
+
undef: :replace,
|
105
|
+
replace: ""
|
106
|
+
})
|
107
|
+
|
108
|
+
html.gsub!(/<b>(\w*)<\/b>/) { |match| "*#{$1}*" }
|
109
|
+
html.gsub!(/<sup>(\w*)<\/sup>/) { |match| "^(#{$1})" }
|
110
|
+
html.gsub!(" ", "")
|
111
|
+
html.gsub!(" ", " ")
|
112
|
+
|
113
|
+
text = Hpricot(html).to_plain_text
|
114
|
+
text.gsub!(/\[img:(http:\/\/[^\]]*)\]/) do |match|
|
115
|
+
url = $1
|
116
|
+
image = Image.new
|
117
|
+
image.name = Pathname.new(url).basename
|
118
|
+
begin
|
119
|
+
image.content = @downloader.download(url)
|
120
|
+
@problem.images << image
|
121
|
+
"![image](images/#{image.name})"
|
122
|
+
rescue HttpError
|
123
|
+
"![image](#{url})"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
text
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_list_table(html)
|
131
|
+
result = []
|
132
|
+
Hpricot(html).search("/tr").each do |tr|
|
133
|
+
tds = tr.search("/td.statText")
|
134
|
+
result << filter(tds[1].html) if tds.size == 2
|
135
|
+
end
|
136
|
+
result
|
137
|
+
end
|
138
|
+
|
139
|
+
def parse_name(html)
|
140
|
+
@problem.name = filter(html.sub("Problem Statement for", ""))
|
141
|
+
end
|
142
|
+
|
143
|
+
def parse_statement(html)
|
144
|
+
@problem.statement = filter(html)
|
145
|
+
end
|
146
|
+
|
147
|
+
def parse_definitions(html)
|
148
|
+
Hpricot(html).search("/tr/td.statText/table/tr").each do |tr|
|
149
|
+
tds = tr.search("/td.statText")
|
150
|
+
@problem.definitions[tds[0].to_plain_text[0 .. -2]] = tds[1].to_plain_text if tds.size == 2
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def parse_notes(html)
|
155
|
+
@problem.notes = parse_list_table(html)
|
156
|
+
end
|
157
|
+
|
158
|
+
def parse_constraints(html)
|
159
|
+
@problem.constraints = parse_list_table(html)
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_input(html)
|
163
|
+
text = ""
|
164
|
+
Hpricot(html).search("/table/tr/td.statText") do |td|
|
165
|
+
input = td.to_plain_text.strip
|
166
|
+
if text.empty?
|
167
|
+
text = input
|
168
|
+
else
|
169
|
+
text << ",\n" << input
|
170
|
+
end
|
171
|
+
end
|
172
|
+
self.class.filter_inout(text)
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_output(html)
|
176
|
+
self.class.filter_inout(Hpricot(html).to_plain_text.sub("Returns: ", ""))
|
177
|
+
end
|
178
|
+
|
179
|
+
def parse_reason(html)
|
180
|
+
filter(html)
|
181
|
+
end
|
182
|
+
|
183
|
+
def parse_examples(html)
|
184
|
+
tds = Hpricot(html).search("/tr/td.statText/table/tr/td.statText")
|
185
|
+
i = 0
|
186
|
+
while i < tds.size
|
187
|
+
example = Case.new
|
188
|
+
example.input = parse_input(tds[i].html)
|
189
|
+
example.output = parse_output(tds[i += 1].html)
|
190
|
+
example.reason = parse_reason(tds[i += 1].html)
|
191
|
+
@problem.examples << example
|
192
|
+
i += 1
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def parse_systests(html)
|
197
|
+
_, y = self.class.indexes(html, "<!-- System Testing -->")
|
198
|
+
z, _ = self.class.indexes(html, "<!-- End System Testing -->")
|
199
|
+
return [] unless y && z
|
200
|
+
|
201
|
+
Hpricot(html[y .. z]).search("/table/tr[@valign=top]").each_with_object([]) do |tr, memo|
|
202
|
+
tds = tr.search("/td.statText")
|
203
|
+
next unless tds.size == 3
|
204
|
+
|
205
|
+
test = Case.new
|
206
|
+
test.input = self.class.filter_inout(tds[0].to_plain_text)
|
207
|
+
test.output = self.class.filter_inout(tds[1].to_plain_text)
|
208
|
+
@problem.systests << test
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def download_systests(round_id)
|
213
|
+
detail_html = @downloader.download_detail(@problem.id, round_id)
|
214
|
+
|
215
|
+
Hpricot(detail_html).search("a[@href^=/stat?c=problem_solution]") do |elem|
|
216
|
+
url = elem.attributes["href"]
|
217
|
+
|
218
|
+
if solution_id = self.class.get_param_value(url, "cr")
|
219
|
+
parse_systests(@downloader.download_solution(@problem.id, round_id, solution_id))
|
220
|
+
return unless @problem.systests.empty?
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def parse_details(doc)
|
226
|
+
doc.search("a[@href^=/tc?module=ProblemDetail]") do |elem|
|
227
|
+
@problem.url = elem.attributes["href"]
|
228
|
+
@problem.source = filter(elem.html)
|
229
|
+
|
230
|
+
if round_id = self.class.get_param_value(@problem.url, "rd")
|
231
|
+
download_systests(round_id)
|
232
|
+
return unless @problem.systests.empty?
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
data/lib/gettc/print.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require "gettc/problem"
|
2
|
+
require "rdiscount"
|
3
|
+
|
4
|
+
module Gettc
|
5
|
+
class Problem
|
6
|
+
def to_md
|
7
|
+
out = "# [#{@name}](#{@url})\n"
|
8
|
+
out << "*#{@source}*\n\n"
|
9
|
+
|
10
|
+
out << "## Statement\n"
|
11
|
+
out << @statement << "\n\n"
|
12
|
+
|
13
|
+
unless @definitions.empty?
|
14
|
+
out << "## Definitions\n"
|
15
|
+
@definitions.each { |k, v| out << "- *#{k}*: `#{v}`\n" }
|
16
|
+
out << "\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
unless @notes.empty?
|
20
|
+
out << "## Notes\n"
|
21
|
+
@notes.each { |note| out << "- #{note}\n" }
|
22
|
+
out << "\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
unless @constraints.empty?
|
26
|
+
out << "## Constraints\n"
|
27
|
+
@constraints.each { |constraint| out << "- #{constraint}\n" }
|
28
|
+
out << "\n"
|
29
|
+
end
|
30
|
+
|
31
|
+
unless @examples.empty?
|
32
|
+
out << "## Examples\n"
|
33
|
+
|
34
|
+
@examples.each_index do |i|
|
35
|
+
example = @examples[i]
|
36
|
+
out << "### Example #{i + 1}\n"
|
37
|
+
out << "#### Input\n<c>"
|
38
|
+
out << example.input.gsub("\n", "<br />")
|
39
|
+
out << "</c>\n"
|
40
|
+
out << "#### Output\n<c>"
|
41
|
+
out << example.output.gsub("\n", "<br />")
|
42
|
+
out << "</c>\n"
|
43
|
+
out << "#### Reason\n#{example.reason}\n\n" unless example.reason.empty?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
out
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_html
|
51
|
+
RDiscount.new(to_md).to_html
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Gettc
|
2
|
+
class Case
|
3
|
+
attr_accessor :input, :output, :reason
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@input = ""
|
7
|
+
@output = ""
|
8
|
+
@reason = ""
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Image
|
13
|
+
attr_accessor :name, :content
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@name = ""
|
17
|
+
@content = ""
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Problem
|
22
|
+
attr_accessor :id, :name, :url, :source, :statement, :definitions, :notes,
|
23
|
+
:constraints, :examples, :systests, :images
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@id = 0
|
27
|
+
@name = ""
|
28
|
+
@url = ""
|
29
|
+
@source = ""
|
30
|
+
@statement = ""
|
31
|
+
@definitions = {}
|
32
|
+
@notes = []
|
33
|
+
@constraints = []
|
34
|
+
@examples = []
|
35
|
+
@systests = []
|
36
|
+
@images = []
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|