gettc 1.10 → 2.0
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 +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
|
+
""
|
122
|
+
rescue HttpError
|
123
|
+
""
|
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
|