workbook 0.8.1 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +21 -0
- data/.gitignore +4 -1
- data/.ruby-version +1 -1
- data/.travis.yml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -2
- data/README.md +9 -7
- data/Rakefile +6 -6
- data/json_test.json +1 -0
- data/lib/workbook/book.rb +73 -62
- data/lib/workbook/cell.rb +58 -13
- data/lib/workbook/column.rb +31 -28
- data/lib/workbook/format.rb +23 -24
- data/lib/workbook/generatetypes.rb +4 -4
- data/lib/workbook/modules/cache.rb +6 -7
- data/lib/workbook/modules/cell.rb +77 -100
- data/lib/workbook/modules/diff_sort.rb +92 -83
- data/lib/workbook/modules/raw_objects_storage.rb +6 -8
- data/lib/workbook/modules/type_parser.rb +30 -22
- data/lib/workbook/nil_value.rb +4 -9
- data/lib/workbook/readers/csv_reader.rb +7 -10
- data/lib/workbook/readers/ods_reader.rb +51 -50
- data/lib/workbook/readers/txt_reader.rb +6 -8
- data/lib/workbook/readers/xls_reader.rb +21 -33
- data/lib/workbook/readers/xls_shared.rb +106 -117
- data/lib/workbook/readers/xlsx_reader.rb +45 -46
- data/lib/workbook/row.rb +99 -84
- data/lib/workbook/sheet.rb +47 -38
- data/lib/workbook/table.rb +96 -72
- data/lib/workbook/template.rb +12 -15
- data/lib/workbook/types/false.rb +0 -1
- data/lib/workbook/types/nil.rb +0 -1
- data/lib/workbook/types/nil_class.rb +1 -1
- data/lib/workbook/types/numeric.rb +1 -1
- data/lib/workbook/types/string.rb +1 -1
- data/lib/workbook/types/time.rb +1 -1
- data/lib/workbook/types/true.rb +0 -1
- data/lib/workbook/types/true_class.rb +1 -1
- data/lib/workbook/version.rb +2 -3
- data/lib/workbook/writers/csv_table_writer.rb +10 -13
- data/lib/workbook/writers/html_writer.rb +34 -38
- data/lib/workbook/writers/json_table_writer.rb +8 -11
- data/lib/workbook/writers/xls_writer.rb +30 -36
- data/lib/workbook/writers/xlsx_writer.rb +45 -29
- data/lib/workbook.rb +16 -15
- data/test/artifacts/currency_test.ods +0 -0
- data/test/helper.rb +6 -5
- data/test/test_book.rb +41 -38
- data/test/test_column.rb +26 -24
- data/test/test_format.rb +51 -55
- data/test/test_functional.rb +7 -8
- data/test/test_modules_cache.rb +18 -17
- data/test/test_modules_cell.rb +55 -46
- data/test/test_modules_table_diff_sort.rb +55 -64
- data/test/test_modules_type_parser.rb +61 -31
- data/test/test_readers_csv_reader.rb +48 -42
- data/test/test_readers_ods_reader.rb +36 -31
- data/test/test_readers_txt_reader.rb +21 -23
- data/test/test_readers_xls_reader.rb +20 -23
- data/test/test_readers_xls_shared.rb +2 -3
- data/test/test_readers_xlsx_reader.rb +44 -37
- data/test/test_row.rb +105 -109
- data/test/test_sheet.rb +35 -41
- data/test/test_table.rb +82 -60
- data/test/test_template.rb +16 -15
- data/test/test_types_date.rb +4 -6
- data/test/test_writers_csv_writer.rb +24 -0
- data/test/test_writers_html_writer.rb +42 -41
- data/test/test_writers_json_writer.rb +16 -9
- data/test/test_writers_xls_writer.rb +50 -35
- data/test/test_writers_xlsx_writer.rb +62 -34
- data/workbook.gemspec +25 -27
- metadata +96 -42
data/lib/workbook/format.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# -*- encoding : utf-8 -*-
|
4
2
|
# frozen_string_literal: true
|
5
|
-
|
3
|
+
|
4
|
+
require "workbook/modules/raw_objects_storage"
|
6
5
|
|
7
6
|
module Workbook
|
8
7
|
# Format is an object used for maintinaing a cell's formatting. It can belong to many cells. It maintains a relation to the raw template's equivalent, to preserve attributes Workbook cannot modify/access.
|
@@ -26,25 +25,25 @@ module Workbook
|
|
26
25
|
#
|
27
26
|
# @param [Workbook::Format, Hash] options (e.g. :background, :color, :background_color, :font_weight (integer or css-type labels)
|
28
27
|
# @return [String] the name of the format, default: nil
|
29
|
-
def initialize options={}, name=nil
|
28
|
+
def initialize options = {}, name = nil
|
30
29
|
if options.is_a? String
|
31
30
|
name = options
|
32
31
|
else
|
33
|
-
options.each {|k,v| self[k]=v}
|
32
|
+
options.each { |k, v| self[k] = v }
|
34
33
|
end
|
35
34
|
self.name = name
|
36
35
|
end
|
37
36
|
|
38
37
|
# Does the current format feature a background *color*? (not black or white or transparant).
|
39
|
-
def has_background_color? color
|
38
|
+
def has_background_color? color = :any
|
40
39
|
bg_color = flattened[:background_color] ? flattened[:background_color].to_s.downcase : nil
|
41
40
|
|
42
|
-
if color != :any
|
43
|
-
|
41
|
+
if (color != :any) && bg_color
|
42
|
+
bg_color == color.to_s.downcase
|
44
43
|
elsif bg_color
|
45
|
-
|
44
|
+
!((flattened[:background_color].downcase == "#ffffff") || (flattened[:background_color] == "#000000"))
|
46
45
|
else
|
47
|
-
|
46
|
+
false
|
48
47
|
end
|
49
48
|
end
|
50
49
|
|
@@ -52,9 +51,9 @@ module Workbook
|
|
52
51
|
# @return String very basic CSS styling string
|
53
52
|
def to_css
|
54
53
|
css_parts = []
|
55
|
-
background = [flattened[:background_color].to_s,flattened[:background].to_s].join(" ").strip
|
56
|
-
css_parts.push("background: #{background}") if background
|
57
|
-
css_parts.push("color: #{flattened[:color]
|
54
|
+
background = [flattened[:background_color].to_s, flattened[:background].to_s].join(" ").strip
|
55
|
+
css_parts.push("background: #{background}") if background && (background != "")
|
56
|
+
css_parts.push("color: #{flattened[:color]}") if flattened[:color]
|
58
57
|
css_parts.join("; ")
|
59
58
|
end
|
60
59
|
|
@@ -62,22 +61,22 @@ module Workbook
|
|
62
61
|
# @param [Workbook::Format] other_format
|
63
62
|
# @return [Workbook::Format] a new resulting Workbook::Format
|
64
63
|
def merge(other_format)
|
65
|
-
|
66
|
-
|
64
|
+
remove_all_raws!
|
65
|
+
merge_hash(other_format)
|
67
66
|
end
|
68
67
|
|
69
68
|
# Applies the formatting options of self with another, removes as a consequence the reference to the raw object's equivalent.
|
70
69
|
# @param [Workbook::Format] other_format
|
71
70
|
# @return [Workbook::Format] self
|
72
71
|
def merge!(other_format)
|
73
|
-
|
74
|
-
|
72
|
+
remove_all_raws!
|
73
|
+
merge_hash!(other_format)
|
75
74
|
end
|
76
75
|
|
77
76
|
# returns an array of all formats this style is inheriting from (including itself)
|
78
77
|
# @return [Array<Workbook::Format>] an array of Workbook::Formats
|
79
78
|
def formats
|
80
|
-
formats=[]
|
79
|
+
formats = []
|
81
80
|
f = self
|
82
81
|
formats << f
|
83
82
|
while f.parent
|
@@ -90,23 +89,23 @@ module Workbook
|
|
90
89
|
# returns an array of all format-names this style is inheriting from (and this style)
|
91
90
|
# @return [Array<String>] an array of Workbook::Formats
|
92
91
|
def all_names
|
93
|
-
formats.collect{|a| a.name}
|
92
|
+
formats.collect { |a| a.name }
|
94
93
|
end
|
95
94
|
|
96
95
|
# Applies the formatting options of self with its parents until no parent can be found
|
97
96
|
# @return [Workbook::Format] new Workbook::Format that is the result of merging current style with all its parent's styles.
|
98
97
|
def flattened
|
99
|
-
ff=Workbook::Format.new
|
100
|
-
formats.each{|a| ff.merge!(a) }
|
101
|
-
|
98
|
+
ff = Workbook::Format.new
|
99
|
+
formats.each { |a| ff.merge!(a) }
|
100
|
+
ff
|
102
101
|
end
|
103
102
|
|
104
103
|
# Formatting is sometimes the only way to detect the cells' type.
|
105
104
|
def derived_type
|
106
105
|
if self[:numberformat]
|
107
|
-
if self[:numberformat].to_s.match("h")
|
106
|
+
if self[:numberformat].to_s.match?("h")
|
108
107
|
:time
|
109
|
-
elsif self[:numberformat].to_s
|
108
|
+
elsif /y/i.match?(self[:numberformat].to_s)
|
110
109
|
:date
|
111
110
|
end
|
112
111
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
["Numeric","String","Time","Date","TrueClass","FalseClass","NilClass"].each do |type|
|
4
|
-
f = File.open(File.join(File.dirname(__FILE__),"types","#{type}.rb"),
|
3
|
+
["Numeric", "String", "Time", "Date", "TrueClass", "FalseClass", "NilClass"].each do |type|
|
4
|
+
f = File.open(File.join(File.dirname(__FILE__), "types", "#{type}.rb"), "w+")
|
5
5
|
puts f.inspect
|
6
|
-
doc="require 'workbook/cell'
|
6
|
+
doc = "require 'workbook/cell'
|
7
7
|
|
8
8
|
module Workbook
|
9
9
|
module Types
|
@@ -12,5 +12,5 @@ module Workbook
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end"
|
15
|
-
f.write(doc)
|
15
|
+
f.write(doc)
|
16
16
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# -*- encoding : utf-8 -*-
|
4
2
|
# frozen_string_literal: true
|
3
|
+
|
5
4
|
module Workbook
|
6
5
|
module Modules
|
7
6
|
# Adds simple caching
|
@@ -33,12 +32,12 @@ module Workbook
|
|
33
32
|
|
34
33
|
# Check if currently stored key is available and still valid
|
35
34
|
# @return [Boolean]
|
36
|
-
def valid_cache_key?(key, expires=nil)
|
35
|
+
def valid_cache_key?(key, expires = nil)
|
37
36
|
cache_valid_from
|
38
|
-
|
39
|
-
rv
|
37
|
+
@cache[key] && (@cache[key][:inserted_at] > cache_valid_from) && (expires.nil? || (@cache[key][:inserted_at] < expires))
|
40
38
|
end
|
41
|
-
|
39
|
+
|
40
|
+
def fetch_cache(key, expires = nil)
|
42
41
|
@cache ||= {}
|
43
42
|
if valid_cache_key?(key, expires)
|
44
43
|
return @cache[key][:value]
|
@@ -48,7 +47,7 @@ module Workbook
|
|
48
47
|
inserted_at: Time.now
|
49
48
|
}
|
50
49
|
end
|
51
|
-
|
50
|
+
@cache[key][:value]
|
52
51
|
end
|
53
52
|
end
|
54
53
|
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# -*- encoding : utf-8 -*-
|
4
2
|
# frozen_string_literal: true
|
5
|
-
|
6
|
-
require
|
7
|
-
require
|
3
|
+
|
4
|
+
require "workbook/modules/type_parser"
|
5
|
+
require "workbook/nil_value"
|
6
|
+
require "date"
|
8
7
|
|
9
8
|
module Workbook
|
10
9
|
module Modules
|
@@ -12,50 +11,51 @@ module Workbook
|
|
12
11
|
include Workbook::Modules::TypeParser
|
13
12
|
|
14
13
|
CHARACTER_REPACEMENTS = {
|
15
|
-
[/[
|
16
|
-
[
|
17
|
-
[/\+/] =>
|
18
|
-
[/\s/,
|
19
|
-
[
|
20
|
-
[
|
21
|
-
[
|
22
|
-
[
|
23
|
-
[
|
24
|
-
[
|
25
|
-
[
|
26
|
-
[
|
27
|
-
[
|
28
|
-
[
|
29
|
-
[
|
30
|
-
[
|
31
|
-
[
|
32
|
-
[
|
33
|
-
[
|
34
|
-
[
|
35
|
-
[
|
36
|
-
[
|
37
|
-
[
|
38
|
-
[
|
39
|
-
[
|
40
|
-
[
|
41
|
-
[
|
42
|
-
[
|
14
|
+
[/[().?,!=$:]/] => "",
|
15
|
+
[/&/] => "amp",
|
16
|
+
[/\+/] => "_plus_",
|
17
|
+
[/\s/, "/_", "/", "\\"] => "_",
|
18
|
+
["–_", "-_", "+_", "-"] => "",
|
19
|
+
["__"] => "_",
|
20
|
+
[">"] => "gt",
|
21
|
+
["<"] => "lt",
|
22
|
+
["á", "à", "â", "ä", "ã", "å"] => "a",
|
23
|
+
["Ã", "Ä", "Â", "À", "�?", "Å"] => "A",
|
24
|
+
["é", "è", "ê", "ë"] => "e",
|
25
|
+
["Ë", "É", "È", "Ê"] => "E",
|
26
|
+
["í", "ì", "î", "ï"] => "i",
|
27
|
+
["�?", "Î", "Ì", "�?"] => "I",
|
28
|
+
["ó", "ò", "ô", "ö", "õ"] => "o",
|
29
|
+
["Õ", "Ö", "Ô", "Ò", "Ó"] => "O",
|
30
|
+
["ú", "ù", "û", "ü"] => "u",
|
31
|
+
["Ú", "Û", "Ù", "Ü"] => "U",
|
32
|
+
["ç"] => "c",
|
33
|
+
["Ç"] => "C",
|
34
|
+
["š", "ś"] => "s",
|
35
|
+
["Š", "Ś"] => "S",
|
36
|
+
["ž", "ź"] => "z",
|
37
|
+
["Ž", "Ź"] => "Z",
|
38
|
+
["ñ"] => "n",
|
39
|
+
["Ñ"] => "N",
|
40
|
+
["#"] => "hash",
|
41
|
+
["*"] => "asterisk"
|
43
42
|
}
|
44
43
|
CLASS_CELLTYPE_MAPPING = {
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
44
|
+
"BigDecimal" => :decimal,
|
45
|
+
"Numeric" => :integer,
|
46
|
+
"Integer" => :integer,
|
47
|
+
"Fixnum" => :integer,
|
48
|
+
"Float" => :float,
|
49
|
+
"String" => :string,
|
50
|
+
"Symbol" => :string,
|
51
|
+
"Time" => :time,
|
52
|
+
"Date" => :date,
|
53
|
+
"DateTime" => :datetime,
|
54
|
+
"ActiveSupport::TimeWithZone" => :datetime,
|
55
|
+
"TrueClass" => :boolean,
|
56
|
+
"FalseClass" => :boolean,
|
57
|
+
"NilClass" => :nil,
|
58
|
+
"Workbook::NilValue" => :nil
|
59
59
|
}
|
60
60
|
# Note that these types are sorted by 'importance'
|
61
61
|
|
@@ -80,7 +80,7 @@ module Workbook
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def row= r
|
83
|
-
@row= r
|
83
|
+
@row = r
|
84
84
|
end
|
85
85
|
|
86
86
|
# Change the current value
|
@@ -109,18 +109,25 @@ module Workbook
|
|
109
109
|
@value
|
110
110
|
end
|
111
111
|
|
112
|
+
# Returns the column object for the cell
|
113
|
+
#
|
114
|
+
# @return [Workbook::Column] the column the cell belongs to
|
115
|
+
def column
|
116
|
+
table.columns[index]
|
117
|
+
end
|
118
|
+
|
112
119
|
# Returns the sheet its at.
|
113
120
|
#
|
114
121
|
# @return [Workbook::Table]
|
115
122
|
def table
|
116
|
-
row
|
123
|
+
row&.table
|
117
124
|
end
|
118
125
|
|
119
126
|
# Quick assessor to the book's template, if it exists
|
120
127
|
#
|
121
128
|
# @return [Workbook::Template]
|
122
129
|
def template
|
123
|
-
row
|
130
|
+
row&.template
|
124
131
|
end
|
125
132
|
|
126
133
|
# Change the current format
|
@@ -131,7 +138,7 @@ module Workbook
|
|
131
138
|
@workbook_format = f
|
132
139
|
elsif f.is_a? Hash
|
133
140
|
@workbook_format = Workbook::Format.new(f)
|
134
|
-
elsif f.
|
141
|
+
elsif f.instance_of?(NilClass)
|
135
142
|
@workbook_format = Workbook::Format.new
|
136
143
|
end
|
137
144
|
end
|
@@ -141,7 +148,7 @@ module Workbook
|
|
141
148
|
# @return [Workbook::Format] the current format
|
142
149
|
def format
|
143
150
|
# return @workbook_format if @workbook_format
|
144
|
-
if row
|
151
|
+
if row && template && row.header? && !defined?(@workbook_format)
|
145
152
|
@workbook_format = template.create_or_find_format_by(:header)
|
146
153
|
else
|
147
154
|
@workbook_format ||= Workbook::Format.new
|
@@ -155,9 +162,9 @@ module Workbook
|
|
155
162
|
# @return [Boolean]
|
156
163
|
def ==(other)
|
157
164
|
if other.is_a? Cell
|
158
|
-
other.value ==
|
165
|
+
other.value == value
|
159
166
|
else
|
160
|
-
other ==
|
167
|
+
other == value
|
161
168
|
end
|
162
169
|
end
|
163
170
|
|
@@ -168,7 +175,7 @@ module Workbook
|
|
168
175
|
end
|
169
176
|
|
170
177
|
def nil_or_empty?
|
171
|
-
value.nil? || value.to_s == ""
|
178
|
+
value.nil? || value.strip.to_s == ""
|
172
179
|
end
|
173
180
|
|
174
181
|
def value_to_s
|
@@ -181,46 +188,24 @@ module Workbook
|
|
181
188
|
#
|
182
189
|
# <Workbook::Cell value="yet another value">.to_sym # returns :yet_another_value
|
183
190
|
def to_sym
|
184
|
-
|
185
|
-
v = nil
|
186
|
-
unless nil_or_empty?
|
187
|
-
if cell_type == :integer
|
188
|
-
v = "num#{value}".to_sym
|
189
|
-
elsif cell_type == :float
|
190
|
-
v = "num#{value}".sub(".","_").to_sym
|
191
|
-
else
|
192
|
-
v = value_to_s.strip
|
193
|
-
ends_with_exclamationmark = (v[-1] == '!')
|
194
|
-
ends_with_questionmark = (v[-1] == '?')
|
195
|
-
|
196
|
-
v = _replace_possibly_problematic_characters_from_string(v)
|
197
|
-
|
198
|
-
v = v.encode(Encoding.find('ASCII'), {:invalid => :replace, :undef => :replace, :replace => ''})
|
199
|
-
|
200
|
-
v = "#{v}!" if ends_with_exclamationmark
|
201
|
-
v = "#{v}?" if ends_with_questionmark
|
202
|
-
v = v.downcase.to_sym
|
203
|
-
end
|
204
|
-
end
|
205
|
-
@to_sym = v
|
206
|
-
return @to_sym
|
191
|
+
@to_sym ||= ::Workbook::Cell.value_to_sym(value)
|
207
192
|
end
|
208
193
|
|
209
194
|
# Compare
|
210
195
|
#
|
211
196
|
# @param [Workbook::Cell] other cell to compare against (based on value), can compare different value-types using #compare_on_class
|
212
|
-
# @return [
|
197
|
+
# @return [Integer] -1, 0, 1
|
213
198
|
def <=> other
|
214
199
|
rv = nil
|
215
200
|
begin
|
216
|
-
rv =
|
201
|
+
rv = value <=> other.value
|
217
202
|
rescue NoMethodError
|
218
203
|
rv = compare_on_class other
|
219
204
|
end
|
220
|
-
if rv
|
205
|
+
if rv.nil?
|
221
206
|
rv = compare_on_class other
|
222
207
|
end
|
223
|
-
|
208
|
+
rv
|
224
209
|
end
|
225
210
|
|
226
211
|
# Compare on class level
|
@@ -229,7 +214,7 @@ module Workbook
|
|
229
214
|
def compare_on_class other
|
230
215
|
other_value = nil
|
231
216
|
other_value = other.value if other
|
232
|
-
self_value = importance_of_class
|
217
|
+
self_value = importance_of_class value
|
233
218
|
other_value = importance_of_class other_value
|
234
219
|
self_value <=> other_value
|
235
220
|
end
|
@@ -245,14 +230,14 @@ module Workbook
|
|
245
230
|
#
|
246
231
|
# @return [Boolean] index of the cell
|
247
232
|
def format?
|
248
|
-
format
|
233
|
+
format && (format.keys.count > 0)
|
249
234
|
end
|
250
235
|
|
251
236
|
# Returns the index of the cell within the row, returns nil if no row is present
|
252
237
|
#
|
253
238
|
# @return [Integer, NilClass] index of the cell
|
254
239
|
def index
|
255
|
-
row
|
240
|
+
row&.index self
|
256
241
|
end
|
257
242
|
|
258
243
|
# Returns the key (a Symbol) of the cell, based on its table's header
|
@@ -265,6 +250,7 @@ module Workbook
|
|
265
250
|
def inspect
|
266
251
|
txt = "<Workbook::Cell @value=#{value}"
|
267
252
|
txt += " @format=#{format}" if format?
|
253
|
+
txt += " @cell_type=#{cell_type}"
|
268
254
|
txt += ">"
|
269
255
|
txt
|
270
256
|
end
|
@@ -272,9 +258,9 @@ module Workbook
|
|
272
258
|
# convert value to string, and in case of a Date or Time value, apply formatting
|
273
259
|
# @return [String]
|
274
260
|
def to_s
|
275
|
-
if (
|
261
|
+
if (is_a?(Date) || is_a?(Time)) && format[:number_format]
|
276
262
|
value.strftime(format[:number_format])
|
277
|
-
elsif (
|
263
|
+
elsif instance_of?(Workbook::Cell)
|
278
264
|
value.to_s
|
279
265
|
else
|
280
266
|
super
|
@@ -284,26 +270,17 @@ module Workbook
|
|
284
270
|
def colspan= c
|
285
271
|
@colspan = c
|
286
272
|
end
|
273
|
+
|
287
274
|
def rowspan= r
|
288
275
|
@rowspan = r
|
289
276
|
end
|
290
277
|
|
291
278
|
def colspan
|
292
|
-
@colspan.to_i if defined?(@colspan)
|
293
|
-
end
|
294
|
-
def rowspan
|
295
|
-
@rowspan.to_i if defined?(@rowspan) and @rowspan.to_i > 1
|
279
|
+
@colspan.to_i if defined?(@colspan) && (@colspan.to_i > 1)
|
296
280
|
end
|
297
281
|
|
298
|
-
|
299
|
-
|
300
|
-
def _replace_possibly_problematic_characters_from_string(string)
|
301
|
-
Workbook::Modules::Cell::CHARACTER_REPACEMENTS.each do |ac,rep|
|
302
|
-
ac.each do |s|
|
303
|
-
string = string.gsub(s, rep)
|
304
|
-
end
|
305
|
-
end
|
306
|
-
string
|
282
|
+
def rowspan
|
283
|
+
@rowspan.to_i if defined?(@rowspan) && (@rowspan.to_i > 1)
|
307
284
|
end
|
308
285
|
end
|
309
286
|
end
|