groonga-client 0.5.9 → 0.6.4
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/doc/text/news.md +54 -0
- data/groonga-client.gemspec +3 -2
- data/lib/groonga/client/command-line/groonga-client.rb +160 -2
- data/lib/groonga/client/protocol/http/synchronous.rb +45 -14
- data/lib/groonga/client/response.rb +3 -1
- data/lib/groonga/client/response/base.rb +49 -4
- data/lib/groonga/client/response/drilldownable.rb +85 -0
- data/lib/groonga/client/response/logical-range-filter.rb +52 -0
- data/lib/groonga/client/response/logical-select.rb +28 -0
- data/lib/groonga/client/response/searchable.rb +97 -0
- data/lib/groonga/client/response/select.rb +64 -125
- data/lib/groonga/client/version.rb +2 -2
- data/test/request/test-select.rb +2 -2
- data/test/response/test-base.rb +14 -1
- data/test/response/test-select-command-version1.rb +58 -12
- data/test/response/test-select-command-version3.rb +54 -8
- data/test/response/test-select-tsv.rb +26 -4
- data/test/response/test-select-xml.rb +26 -4
- metadata +41 -24
@@ -0,0 +1,85 @@
|
|
1
|
+
# Copyright (C) 2019 Sutou Kouhei <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License as published by the Free Software Foundation; either
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this library; if not, write to the Free Software
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
16
|
+
|
17
|
+
module Groonga
|
18
|
+
class Client
|
19
|
+
module Response
|
20
|
+
module Drilldownable
|
21
|
+
# @return [::Array<Groonga::Client::Response::Select::Drilldown>,
|
22
|
+
# ::Hash<String, Groonga::Client::Response::Select::Drilldown>]
|
23
|
+
# If labeled drilldowns are used or command version 3 or
|
24
|
+
# later is used, `{"label1" => drilldown1, "label2" => drilldown2}`
|
25
|
+
# is returned since 0.3.1.
|
26
|
+
#
|
27
|
+
# Otherwise, `[drilldown1, drilldown2]` is returned.
|
28
|
+
attr_accessor :drilldowns
|
29
|
+
|
30
|
+
private
|
31
|
+
def parse_drilldown(label, keys, raw_drilldown)
|
32
|
+
if raw_drilldown.is_a?(::Array)
|
33
|
+
n_hits = raw_drilldown[0][0]
|
34
|
+
raw_columns = raw_drilldown[1]
|
35
|
+
raw_records = raw_drilldown[2..-1]
|
36
|
+
else
|
37
|
+
n_hits = raw_drilldown["n_hits"]
|
38
|
+
raw_columns = raw_drilldown["columns"]
|
39
|
+
raw_records = raw_drilldown["records"]
|
40
|
+
end
|
41
|
+
records = parse_records(raw_columns, raw_records)
|
42
|
+
Drilldown.new(label,
|
43
|
+
keys,
|
44
|
+
n_hits,
|
45
|
+
records,
|
46
|
+
raw_columns,
|
47
|
+
raw_records)
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_drilldowns(keys, raw_drilldowns)
|
51
|
+
(raw_drilldowns || []).collect.with_index do |raw_drilldown, i|
|
52
|
+
key = keys[i]
|
53
|
+
parse_drilldown(key, [key], raw_drilldown)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_labeled_drilldowns(labeled_drilldown_requests,
|
58
|
+
raw_drilldowns)
|
59
|
+
drilldowns = {}
|
60
|
+
(raw_drilldowns || {}).each do |label, raw_drilldown|
|
61
|
+
labeled_drilldown_request = labeled_drilldown_requests[label]
|
62
|
+
drilldowns[label] = parse_drilldown(label,
|
63
|
+
labeled_drilldown_request.keys,
|
64
|
+
raw_drilldown)
|
65
|
+
end
|
66
|
+
drilldowns
|
67
|
+
end
|
68
|
+
|
69
|
+
class Drilldown < Struct.new(:label,
|
70
|
+
:keys,
|
71
|
+
:n_hits,
|
72
|
+
:records,
|
73
|
+
:raw_columns,
|
74
|
+
:raw_records)
|
75
|
+
# @deprecated since 0.2.6. Use {#records} instead.
|
76
|
+
alias_method :items, :records
|
77
|
+
|
78
|
+
def key
|
79
|
+
keys.join(", ")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Copyright (C) 2019 Sutou Kouhei <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License as published by the Free Software Foundation; either
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this library; if not, write to the Free Software
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
16
|
+
|
17
|
+
require "groonga/client/response/base"
|
18
|
+
require "groonga/client/response/searchable"
|
19
|
+
|
20
|
+
module Groonga
|
21
|
+
class Client
|
22
|
+
module Response
|
23
|
+
class LogicalRangeFilter < Base
|
24
|
+
Response.register("logical_range_filter", self)
|
25
|
+
|
26
|
+
include Searchable
|
27
|
+
|
28
|
+
attr_accessor :records
|
29
|
+
|
30
|
+
def body=(body)
|
31
|
+
super(body)
|
32
|
+
parse_body(body)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def parse_body(body)
|
37
|
+
if body.is_a?(::Array)
|
38
|
+
@raw_columns, *@raw_records = body.first
|
39
|
+
@raw_records ||= []
|
40
|
+
@records = parse_records(raw_columns, raw_records)
|
41
|
+
else
|
42
|
+
@raw_columns = body["columns"]
|
43
|
+
@raw_records = body["records"] || []
|
44
|
+
end
|
45
|
+
@records = parse_records(@raw_columns, @raw_records)
|
46
|
+
body
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Copyright (C) 2019 Sutou Kouhei <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License as published by the Free Software Foundation; either
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this library; if not, write to the Free Software
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
16
|
+
|
17
|
+
require "groonga/client/response/select"
|
18
|
+
|
19
|
+
module Groonga
|
20
|
+
class Client
|
21
|
+
module Response
|
22
|
+
class LogicalSelect < Select
|
23
|
+
Response.register("logical_select", self)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Copyright (C) 2019 Sutou Kouhei <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License as published by the Free Software Foundation; either
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this library; if not, write to the Free Software
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
16
|
+
|
17
|
+
module Groonga
|
18
|
+
class Client
|
19
|
+
module Response
|
20
|
+
module Searchable
|
21
|
+
include Enumerable
|
22
|
+
|
23
|
+
attr_accessor :records
|
24
|
+
attr_accessor :raw_columns
|
25
|
+
attr_accessor :raw_records
|
26
|
+
|
27
|
+
# For Kaminari
|
28
|
+
def limit_value
|
29
|
+
(@command[:limit] || 10).to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
# For Kaminari
|
33
|
+
def offset_value
|
34
|
+
(@command[:offset] || 0).to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
# For Kaminari
|
38
|
+
def size
|
39
|
+
records.size
|
40
|
+
end
|
41
|
+
|
42
|
+
def each(&block)
|
43
|
+
records.each(&block)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def parse_records(raw_columns, raw_records)
|
48
|
+
column_names = {}
|
49
|
+
columns = raw_columns.collect do |column|
|
50
|
+
if column.is_a?(::Array)
|
51
|
+
name, type = column
|
52
|
+
else
|
53
|
+
name = column["name"]
|
54
|
+
type = column["type"]
|
55
|
+
end
|
56
|
+
base_column_name = name
|
57
|
+
suffix = 2
|
58
|
+
while column_names.key?(name)
|
59
|
+
name = "#{base_column_name}#{suffix}"
|
60
|
+
suffix += 1
|
61
|
+
end
|
62
|
+
column_names[name] = true
|
63
|
+
[name, type]
|
64
|
+
end
|
65
|
+
|
66
|
+
(raw_records || []).collect do |raw_record|
|
67
|
+
record = Record.new
|
68
|
+
columns.each_with_index do |(name, type), i|
|
69
|
+
record[name] = convert_value(raw_record[i], type)
|
70
|
+
end
|
71
|
+
record
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def convert_value(value, type)
|
76
|
+
case value
|
77
|
+
when ::Array
|
78
|
+
value.collect do |element|
|
79
|
+
convert_value(element, type)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
case type
|
83
|
+
when "Time"
|
84
|
+
Time.at(value)
|
85
|
+
else
|
86
|
+
value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Record < ::Hash
|
92
|
+
include Hashie::Extensions::MethodAccess
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2013-
|
1
|
+
# Copyright (C) 2013-2019 Sutou Kouhei <kou@clear-code.com>
|
2
2
|
# Copyright (C) 2013 Kosuke Asami
|
3
3
|
# Copyright (C) 2016 Masafumi Yokoyama <yokoyama@clear-code.com>
|
4
4
|
#
|
@@ -17,6 +17,8 @@
|
|
17
17
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
18
|
|
19
19
|
require "groonga/client/response/base"
|
20
|
+
require "groonga/client/response/drilldownable"
|
21
|
+
require "groonga/client/response/searchable"
|
20
22
|
|
21
23
|
module Groonga
|
22
24
|
class Client
|
@@ -174,23 +176,14 @@ module Groonga
|
|
174
176
|
end
|
175
177
|
end
|
176
178
|
|
177
|
-
include
|
179
|
+
include Drilldownable
|
180
|
+
include Searchable
|
178
181
|
|
179
182
|
# @return [Integer] The number of records that match againt
|
180
183
|
# a search condition.
|
181
184
|
attr_accessor :n_hits
|
182
185
|
# For Kaminari
|
183
186
|
alias_method :total_count, :n_hits
|
184
|
-
attr_accessor :records
|
185
|
-
|
186
|
-
# @return [::Array<Groonga::Client::Response::Select::Drilldown>,
|
187
|
-
# ::Hash<String, Groonga::Client::Response::Select::Drilldown>]
|
188
|
-
# If labeled drilldowns are used or command version 3 or
|
189
|
-
# later is used, `{"label1" => drilldown1, "label2" => drilldown2}`
|
190
|
-
# is returned since 0.3.1.
|
191
|
-
#
|
192
|
-
# Otherwise, `[drilldown1, drilldown2]` is returned.
|
193
|
-
attr_accessor :drilldowns
|
194
187
|
|
195
188
|
# @return [::Hash<String, Groonga::Client::Response::Select::Slice>]
|
196
189
|
#
|
@@ -202,29 +195,11 @@ module Groonga
|
|
202
195
|
parse_body(body)
|
203
196
|
end
|
204
197
|
|
205
|
-
# For Kaminari
|
206
|
-
def limit_value
|
207
|
-
(@command[:limit] || 10).to_i
|
208
|
-
end
|
209
|
-
|
210
|
-
# For Kaminari
|
211
|
-
def offset_value
|
212
|
-
(@command[:offset] || 0).to_i
|
213
|
-
end
|
214
|
-
|
215
|
-
# For Kaminari
|
216
|
-
def size
|
217
|
-
records.size
|
218
|
-
end
|
219
|
-
|
220
|
-
def each(&block)
|
221
|
-
records.each(&block)
|
222
|
-
end
|
223
|
-
|
224
198
|
private
|
225
199
|
def parse_body(body)
|
226
200
|
if body.is_a?(::Array)
|
227
|
-
@n_hits, @records =
|
201
|
+
@n_hits, @raw_columns, @raw_records, @records =
|
202
|
+
parse_record_set_v1(body.first)
|
228
203
|
if @command.slices.empty?
|
229
204
|
raw_slices = nil
|
230
205
|
raw_drilldowns = body[1..-1]
|
@@ -232,105 +207,74 @@ module Groonga
|
|
232
207
|
raw_slices, *raw_drilldowns = body[1..-1]
|
233
208
|
end
|
234
209
|
@slices = parse_slices_v1(raw_slices)
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
end
|
241
|
-
body
|
242
|
-
end
|
243
|
-
|
244
|
-
def parse_records(raw_columns, raw_records)
|
245
|
-
column_names = {}
|
246
|
-
columns = raw_columns.collect do |column|
|
247
|
-
if column.is_a?(::Array)
|
248
|
-
name, type = column
|
210
|
+
drilldown_keys = @command.drilldowns
|
211
|
+
labeled_drilldowns = @command.labeled_drilldowns
|
212
|
+
if drilldown_keys.empty? and !labeled_drilldowns.empty?
|
213
|
+
@drilldowns = parse_labeled_drilldowns(labeled_drilldowns,
|
214
|
+
raw_drilldowns[0])
|
249
215
|
else
|
250
|
-
|
251
|
-
type = column["type"]
|
252
|
-
end
|
253
|
-
base_column_name = name
|
254
|
-
suffix = 2
|
255
|
-
while column_names.key?(name)
|
256
|
-
name = "#{base_column_name}#{suffix}"
|
257
|
-
suffix += 1
|
258
|
-
end
|
259
|
-
column_names[name] = true
|
260
|
-
[name, type]
|
261
|
-
end
|
262
|
-
|
263
|
-
(raw_records || []).collect do |raw_record|
|
264
|
-
record = Record.new
|
265
|
-
columns.each_with_index do |(name, type), i|
|
266
|
-
record[name] = convert_value(raw_record[i], type)
|
267
|
-
end
|
268
|
-
record
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def convert_value(value, type)
|
273
|
-
case value
|
274
|
-
when ::Array
|
275
|
-
value.collect do |element|
|
276
|
-
convert_value(element, type)
|
216
|
+
@drilldowns = parse_drilldowns(drilldown_keys, raw_drilldowns)
|
277
217
|
end
|
278
218
|
else
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
219
|
+
@n_hits, @raw_columns, @raw_records, @records =
|
220
|
+
parse_record_set_v3(body)
|
221
|
+
drilldown_keys = @command.drilldowns
|
222
|
+
labeled_drilldowns = @command.labeled_drilldowns
|
223
|
+
if labeled_drilldowns.empty?
|
224
|
+
drilldown_keys.each do |key|
|
225
|
+
labeled_drilldown =
|
226
|
+
Groonga::Command::Drilldownable::Drilldown.new
|
227
|
+
labeled_drilldown.label = key
|
228
|
+
labeled_drilldown.keys = [key]
|
229
|
+
labeled_drilldowns[key] = labeled_drilldown
|
230
|
+
end
|
284
231
|
end
|
232
|
+
@drilldowns = parse_labeled_drilldowns(labeled_drilldowns,
|
233
|
+
body["drilldowns"])
|
234
|
+
@slices = parse_slices_v3(body["slices"])
|
285
235
|
end
|
236
|
+
body
|
286
237
|
end
|
287
238
|
|
288
|
-
def
|
239
|
+
def parse_record_set_v1(raw_record_set)
|
240
|
+
n_hits = raw_record_set.first.first
|
241
|
+
raw_columns = raw_record_set[1]
|
242
|
+
raw_records = raw_record_set[2..-1] || []
|
289
243
|
[
|
290
|
-
|
291
|
-
|
244
|
+
n_hits,
|
245
|
+
raw_columns,
|
246
|
+
raw_records,
|
247
|
+
parse_records(raw_columns, raw_records),
|
292
248
|
]
|
293
249
|
end
|
294
250
|
|
295
|
-
def
|
251
|
+
def parse_record_set_v3(raw_record_set)
|
252
|
+
n_hits = raw_record_set["n_hits"]
|
253
|
+
raw_columns = raw_record_set["columns"]
|
254
|
+
raw_records = raw_record_set["records"] || []
|
296
255
|
[
|
297
|
-
|
298
|
-
|
256
|
+
n_hits,
|
257
|
+
raw_columns,
|
258
|
+
raw_records,
|
259
|
+
parse_records(raw_columns, raw_records),
|
299
260
|
]
|
300
261
|
end
|
301
262
|
|
302
|
-
def parse_drilldowns_v1(raw_drilldowns)
|
303
|
-
request_drilldowns = @command.drilldowns
|
304
|
-
if request_drilldowns.empty? and !@command.labeled_drilldowns.empty?
|
305
|
-
drilldowns = {}
|
306
|
-
(raw_drilldowns[0] || {}).each do |label, raw_drilldown|
|
307
|
-
n_hits, records = parse_match_records_v1(raw_drilldown)
|
308
|
-
drilldowns[label] = Drilldown.new(label, n_hits, records)
|
309
|
-
end
|
310
|
-
drilldowns
|
311
|
-
else
|
312
|
-
(raw_drilldowns || []).collect.with_index do |raw_drilldown, i|
|
313
|
-
key = request_drilldowns[i]
|
314
|
-
n_hits, records = parse_match_records_v1(raw_drilldown)
|
315
|
-
Drilldown.new(key, n_hits, records)
|
316
|
-
end
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
def parse_drilldowns_v3(raw_drilldowns)
|
321
|
-
drilldowns = {}
|
322
|
-
(raw_drilldowns || {}).each do |key, raw_drilldown|
|
323
|
-
n_hits, records = parse_match_records_v3(raw_drilldown)
|
324
|
-
drilldowns[key] = Drilldown.new(key, n_hits, records)
|
325
|
-
end
|
326
|
-
drilldowns
|
327
|
-
end
|
328
|
-
|
329
263
|
def parse_slices_v1(raw_slices)
|
330
264
|
slices = {}
|
331
265
|
(raw_slices || {}).each do |key, raw_slice|
|
332
|
-
|
333
|
-
|
266
|
+
requested_slice = @command.slices[key]
|
267
|
+
if raw_slice.last.is_a?(::Hash)
|
268
|
+
raw_drilldowns = raw_slice.last
|
269
|
+
raw_slice = raw_slice[0..-2]
|
270
|
+
drilldowns =
|
271
|
+
parse_labeled_drilldowns(requested_slice.labeled_drilldowns,
|
272
|
+
raw_drilldowns)
|
273
|
+
else
|
274
|
+
drilldowns = {}
|
275
|
+
end
|
276
|
+
n_hits, _, _, records = parse_record_set_v1(raw_slice)
|
277
|
+
slices[key] = Slice.new(key, n_hits, records, drilldowns)
|
334
278
|
end
|
335
279
|
slices
|
336
280
|
end
|
@@ -338,22 +282,17 @@ module Groonga
|
|
338
282
|
def parse_slices_v3(raw_slices)
|
339
283
|
slices = {}
|
340
284
|
(raw_slices || {}).each do |key, raw_slice|
|
341
|
-
|
342
|
-
|
285
|
+
requested_slice = @command.slices[key]
|
286
|
+
n_hits, _, _, records = parse_record_set_v3(raw_slice)
|
287
|
+
drilldowns =
|
288
|
+
parse_labeled_drilldowns(requested_slice.labeled_drilldowns,
|
289
|
+
raw_slice["drilldowns"])
|
290
|
+
slices[key] = Slice.new(key, n_hits, records, drilldowns)
|
343
291
|
end
|
344
292
|
slices
|
345
293
|
end
|
346
294
|
|
347
|
-
class
|
348
|
-
include Hashie::Extensions::MethodAccess
|
349
|
-
end
|
350
|
-
|
351
|
-
class Drilldown < Struct.new(:key, :n_hits, :records)
|
352
|
-
# @deprecated since 0.2.6. Use {#records} instead.
|
353
|
-
alias_method :items, :records
|
354
|
-
end
|
355
|
-
|
356
|
-
class Slice < Struct.new(:key, :n_hits, :records)
|
295
|
+
class Slice < Struct.new(:key, :n_hits, :records, :drilldowns)
|
357
296
|
end
|
358
297
|
end
|
359
298
|
end
|