xlsxrb 0.1.13 → 0.1.15
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/CHANGELOG.md +104 -0
- data/README.md +17 -14
- data/lib/xlsxrb/colors.rb +118 -0
- data/lib/xlsxrb/elements/cell.rb +205 -16
- data/lib/xlsxrb/elements/column.rb +6 -4
- data/lib/xlsxrb/elements/coordinate_access.rb +32 -0
- data/lib/xlsxrb/elements/image.rb +128 -0
- data/lib/xlsxrb/elements/row.rb +206 -9
- data/lib/xlsxrb/elements/styles.rb +157 -0
- data/lib/xlsxrb/elements/types.rb +95 -1
- data/lib/xlsxrb/elements/workbook.rb +121 -4
- data/lib/xlsxrb/elements/worksheet.rb +519 -29
- data/lib/xlsxrb/elements.rb +2 -0
- data/lib/xlsxrb/number_formatter.rb +429 -0
- data/lib/xlsxrb/ooxml/cfb.rb +1 -1
- data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +7 -5
- data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +11 -4
- data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
- data/lib/xlsxrb/ooxml/reader.rb +14 -17
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
- data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
- data/lib/xlsxrb/ooxml/utils.rb +34 -18
- data/lib/xlsxrb/ooxml/workbook_parser.rb +74 -16
- data/lib/xlsxrb/ooxml/workbook_writer.rb +82 -23
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +263 -66
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +5 -3
- data/lib/xlsxrb/ooxml/writer.rb +14 -0
- data/lib/xlsxrb/ooxml/xml_builder.rb +14 -2
- data/lib/xlsxrb/ooxml/zip_reader.rb +144 -28
- data/lib/xlsxrb/stream_row.rb +243 -16
- data/lib/xlsxrb/stream_sheet.rb +647 -16
- data/lib/xlsxrb/stream_writer.rb +375 -34
- data/lib/xlsxrb/style_builder.rb +308 -22
- data/lib/xlsxrb/utils.rb +211 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +72 -11
- data/lib/xlsxrb/worksheet_builder.rb +738 -24
- data/lib/xlsxrb.rb +1073 -83
- data/sig/generated/xlsxrb/colors.rbs +40 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
- data/sig/generated/xlsxrb/elements/column.rbs +3 -2
- data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
- data/sig/generated/xlsxrb/elements/image.rbs +82 -0
- data/sig/generated/xlsxrb/elements/row.rbs +83 -4
- data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
- data/sig/generated/xlsxrb/elements/types.rbs +43 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +79 -2
- data/sig/generated/xlsxrb/elements/worksheet.rbs +282 -18
- data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
- data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +1 -1
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +13 -3
- data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +13 -0
- data/sig/generated/xlsxrb/stream_row.rbs +100 -6
- data/sig/generated/xlsxrb/stream_sheet.rbs +358 -14
- data/sig/generated/xlsxrb/stream_writer.rbs +220 -30
- data/sig/generated/xlsxrb/style_builder.rbs +55 -18
- data/sig/generated/xlsxrb/utils.rbs +151 -0
- data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
- data/sig/generated/xlsxrb/worksheet_builder.rbs +356 -18
- data/sig/generated/xlsxrb.rbs +223 -27
- metadata +11 -1
data/lib/xlsxrb/stream_sheet.rb
CHANGED
|
@@ -33,6 +33,16 @@ module Xlsxrb
|
|
|
33
33
|
#: String
|
|
34
34
|
attr_reader :name
|
|
35
35
|
|
|
36
|
+
# @return [Symbol] The sheet visibility state (:visible, :hidden, or :very_hidden).
|
|
37
|
+
# @api public
|
|
38
|
+
#: Symbol
|
|
39
|
+
attr_reader :state
|
|
40
|
+
|
|
41
|
+
# @return [Hash, nil] The parsed styles definition hash.
|
|
42
|
+
# @api public
|
|
43
|
+
#: Hash[untyped, untyped]?
|
|
44
|
+
attr_reader :styles
|
|
45
|
+
|
|
36
46
|
# Initializes a streaming worksheet context.
|
|
37
47
|
#
|
|
38
48
|
# @param name [String] The sheet name.
|
|
@@ -41,15 +51,136 @@ module Xlsxrb
|
|
|
41
51
|
# @param styles [Hash, nil] Optional parsed styles hash.
|
|
42
52
|
# @param zip_reader [Ooxml::ZipReader, nil] Optional ZipReader context.
|
|
43
53
|
# @param entry_name [String, nil] Archive entry name for this sheet.
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
# @param date1904 [Boolean] Whether the 1904 date system is active.
|
|
55
|
+
# @param dimension [String, nil] Sheet dimension reference string (e.g. "A1:Z50000").
|
|
56
|
+
# @param trim_empty_rows [Boolean] Whether to omit trailing empty rows during iteration.
|
|
57
|
+
# @param pad_empty_rows [Boolean] Whether to yield empty rows for skipped row numbers.
|
|
58
|
+
# @param pad_empty_cells [Boolean] Whether to pad missing cells within rows.
|
|
59
|
+
# @param images [Array<Elements::Image>, nil] Optional preloaded images array.
|
|
60
|
+
#: (String name, untyped sheet_source, Array[String] shared_strings, ?Hash[untyped, untyped]? styles, ?zip_reader: Ooxml::ZipReader?, ?entry_name: String?, ?state: Symbol, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?images: Array[Elements::Image]?) -> void
|
|
61
|
+
def initialize(name, sheet_source, shared_strings, styles = nil, zip_reader: nil, entry_name: nil, state: :visible, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, images: nil)
|
|
46
62
|
@name = name
|
|
47
63
|
@sheet_source = sheet_source
|
|
48
64
|
@shared_strings = shared_strings
|
|
49
65
|
@styles = styles
|
|
50
66
|
@zip_reader = zip_reader
|
|
51
67
|
@entry_name = entry_name
|
|
68
|
+
@state = state ? state.to_sym : :visible
|
|
69
|
+
@date1904 = date1904 ? true : false
|
|
70
|
+
@dimension = dimension
|
|
71
|
+
@dimension_checked = !dimension.nil?
|
|
52
72
|
@sheet_xml = sheet_source if sheet_source.is_a?(String)
|
|
73
|
+
@trim_empty_rows = trim_empty_rows ? true : false
|
|
74
|
+
@pad_empty_rows = pad_empty_rows ? true : false
|
|
75
|
+
@pad_empty_cells = pad_empty_cells ? true : false
|
|
76
|
+
@images = images
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns whether trailing empty rows are omitted during iteration.
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
# @api public
|
|
83
|
+
#: () -> bool
|
|
84
|
+
def trim_empty_rows?
|
|
85
|
+
@trim_empty_rows ? true : false
|
|
86
|
+
end
|
|
87
|
+
alias trim_empty_rows trim_empty_rows?
|
|
88
|
+
|
|
89
|
+
# Returns whether skipped rows are padded during iteration.
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
# @api public
|
|
93
|
+
#: () -> bool
|
|
94
|
+
def pad_empty_rows?
|
|
95
|
+
@pad_empty_rows ? true : false
|
|
96
|
+
end
|
|
97
|
+
alias pad_empty_rows pad_empty_rows?
|
|
98
|
+
|
|
99
|
+
# Returns whether missing cells within rows are padded during iteration.
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
# @api public
|
|
103
|
+
#: () -> bool
|
|
104
|
+
def pad_empty_cells?
|
|
105
|
+
@pad_empty_cells ? true : false
|
|
106
|
+
end
|
|
107
|
+
alias pad_empty_cells pad_empty_cells?
|
|
108
|
+
|
|
109
|
+
# Returns the dimension reference string (e.g. "A1:Z50000"), or nil if not present.
|
|
110
|
+
#
|
|
111
|
+
# @return [String, nil]
|
|
112
|
+
# @api public
|
|
113
|
+
#: () -> String?
|
|
114
|
+
def dimension
|
|
115
|
+
return @dimension if @dimension_checked
|
|
116
|
+
|
|
117
|
+
@dimension_checked = true
|
|
118
|
+
@dimension = extract_dimension
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns the 1-based index of the first row defined in the sheet dimension, or nil.
|
|
122
|
+
#
|
|
123
|
+
# @return [Integer, nil]
|
|
124
|
+
# @api public
|
|
125
|
+
#: () -> Integer?
|
|
126
|
+
def first_row
|
|
127
|
+
dimension_bounds&.[](0)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Returns the 1-based index of the first column defined in the sheet dimension, or nil.
|
|
131
|
+
#
|
|
132
|
+
# @return [Integer, nil]
|
|
133
|
+
# @api public
|
|
134
|
+
#: () -> Integer?
|
|
135
|
+
def first_column
|
|
136
|
+
dimension_bounds&.[](1)
|
|
137
|
+
end
|
|
138
|
+
alias first_col first_column
|
|
139
|
+
|
|
140
|
+
# Returns the 1-based index of the last row defined in the sheet dimension, or nil.
|
|
141
|
+
#
|
|
142
|
+
# @return [Integer, nil]
|
|
143
|
+
# @api public
|
|
144
|
+
#: () -> Integer?
|
|
145
|
+
def last_row
|
|
146
|
+
dimension_bounds&.[](2)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Returns the 1-based index of the last column defined in the sheet dimension, or nil.
|
|
150
|
+
#
|
|
151
|
+
# @return [Integer, nil]
|
|
152
|
+
# @api public
|
|
153
|
+
#: () -> Integer?
|
|
154
|
+
def last_column
|
|
155
|
+
dimension_bounds&.[](3)
|
|
156
|
+
end
|
|
157
|
+
alias last_col last_column
|
|
158
|
+
|
|
159
|
+
# Returns whether the sheet uses the 1904 date system.
|
|
160
|
+
#
|
|
161
|
+
# @return [Boolean]
|
|
162
|
+
# @api public
|
|
163
|
+
#: () -> bool
|
|
164
|
+
def date1904?
|
|
165
|
+
@date1904
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns whether the sheet is hidden (:hidden or :very_hidden).
|
|
169
|
+
#
|
|
170
|
+
# @return [Boolean]
|
|
171
|
+
# @api public
|
|
172
|
+
#: () -> bool
|
|
173
|
+
def hidden?
|
|
174
|
+
@state == :hidden || @state == :very_hidden
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns whether the sheet is visible.
|
|
178
|
+
#
|
|
179
|
+
# @return [Boolean]
|
|
180
|
+
# @api public
|
|
181
|
+
#: () -> bool
|
|
182
|
+
def visible?
|
|
183
|
+
@state == :visible
|
|
53
184
|
end
|
|
54
185
|
|
|
55
186
|
# Iterates over rows in this streaming worksheet with O(1) memory.
|
|
@@ -57,16 +188,41 @@ module Xlsxrb
|
|
|
57
188
|
# @overload each_row(&block)
|
|
58
189
|
# @yield [row]
|
|
59
190
|
# @yieldparam row [StreamRow, Elements::Row] The current row.
|
|
191
|
+
# @overload each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
|
|
192
|
+
# @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
|
|
193
|
+
# @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
|
|
194
|
+
# @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
|
|
195
|
+
# @yield [row]
|
|
196
|
+
# @yieldparam row [StreamRow, Elements::Row] The current row.
|
|
60
197
|
# @return [void]
|
|
61
198
|
#
|
|
62
|
-
# @overload each_row
|
|
199
|
+
# @overload each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
|
|
200
|
+
# @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
|
|
201
|
+
# @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
|
|
202
|
+
# @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
|
|
63
203
|
# @return [Enumerator<StreamRow | Elements::Row, void>]
|
|
64
204
|
#
|
|
65
205
|
# @api public
|
|
66
|
-
#: () { (StreamRow | Elements::Row) -> void } -> void
|
|
67
|
-
#: () -> Enumerator[StreamRow | Elements::Row, void]
|
|
68
|
-
def each_row(&)
|
|
69
|
-
return enum_for(:each_row) unless block_given?
|
|
206
|
+
#: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (StreamRow | Elements::Row) -> void } -> void
|
|
207
|
+
#: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[StreamRow | Elements::Row, void]
|
|
208
|
+
def each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &)
|
|
209
|
+
return enum_for(:each_row, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block_given?
|
|
210
|
+
|
|
211
|
+
trim = if trim_empty_rows.nil?
|
|
212
|
+
@trim_empty_rows
|
|
213
|
+
else
|
|
214
|
+
(trim_empty_rows ? true : false)
|
|
215
|
+
end
|
|
216
|
+
pad_rows = if pad_empty_rows.nil?
|
|
217
|
+
@pad_empty_rows
|
|
218
|
+
else
|
|
219
|
+
(pad_empty_rows ? true : false)
|
|
220
|
+
end
|
|
221
|
+
pad_cells = if pad_empty_cells.nil?
|
|
222
|
+
@pad_empty_cells
|
|
223
|
+
else
|
|
224
|
+
(pad_empty_cells ? true : false)
|
|
225
|
+
end
|
|
70
226
|
|
|
71
227
|
source = if @sheet_source
|
|
72
228
|
@sheet_source
|
|
@@ -74,7 +230,7 @@ module Xlsxrb
|
|
|
74
230
|
->(&blk) { @zip_reader.each_entry_chunk(@entry_name, &blk) }
|
|
75
231
|
end
|
|
76
232
|
|
|
77
|
-
Ooxml::WorksheetParser.each_row(source, shared_strings: @shared_strings, &)
|
|
233
|
+
Ooxml::WorksheetParser.each_row(source, shared_strings: @shared_strings, styles: @styles, date1904: @date1904, trim_empty_rows: trim, pad_empty_rows: pad_rows, pad_empty_cells: pad_cells, &)
|
|
78
234
|
end
|
|
79
235
|
|
|
80
236
|
# Iterates over all cells across all rows continuously with O(1) memory.
|
|
@@ -98,21 +254,56 @@ module Xlsxrb
|
|
|
98
254
|
end
|
|
99
255
|
end
|
|
100
256
|
|
|
257
|
+
# Iterates over row values directly as Arrays without wrapping cells in Elements::Cell objects.
|
|
258
|
+
#
|
|
259
|
+
# @overload each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
|
|
260
|
+
# @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
|
|
261
|
+
# @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
|
|
262
|
+
# @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
|
|
263
|
+
# @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
|
|
264
|
+
# @yield [values]
|
|
265
|
+
# @yieldparam values [Array<Object>] Row values array.
|
|
266
|
+
# @return [void]
|
|
267
|
+
#
|
|
268
|
+
# @overload each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
|
|
269
|
+
# @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
|
|
270
|
+
# @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
|
|
271
|
+
# @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
|
|
272
|
+
# @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
|
|
273
|
+
# @return [Enumerator<Array<Object>, void>]
|
|
274
|
+
#
|
|
275
|
+
# @api public
|
|
276
|
+
#: (?type_cast: bool, ?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (Array[untyped]) -> void } -> void
|
|
277
|
+
#: (?type_cast: bool, ?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[Array[untyped], void]
|
|
278
|
+
def each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
|
|
279
|
+
return enum_for(:each_row_values, type_cast: type_cast, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block
|
|
280
|
+
|
|
281
|
+
each_row(trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) do |row|
|
|
282
|
+
block.call(row.values(type_cast: type_cast))
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
101
286
|
# Default Enumerable iteration delegates to {#each_row}.
|
|
102
287
|
#
|
|
103
|
-
# @overload each(&block)
|
|
288
|
+
# @overload each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
|
|
289
|
+
# @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
|
|
290
|
+
# @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
|
|
291
|
+
# @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
|
|
104
292
|
# @yield [row]
|
|
105
293
|
# @yieldparam row [StreamRow, Elements::Row]
|
|
106
294
|
# @return [void]
|
|
107
295
|
#
|
|
108
|
-
# @overload each
|
|
296
|
+
# @overload each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
|
|
297
|
+
# @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
|
|
298
|
+
# @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
|
|
299
|
+
# @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
|
|
109
300
|
# @return [Enumerator<StreamRow | Elements::Row, void>]
|
|
110
301
|
#
|
|
111
302
|
# @api public
|
|
112
|
-
#: () { (StreamRow | Elements::Row) -> void } -> void
|
|
113
|
-
#: () -> Enumerator[StreamRow | Elements::Row, void]
|
|
114
|
-
def each(&)
|
|
115
|
-
each_row(&)
|
|
303
|
+
#: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (StreamRow | Elements::Row) -> void } -> void
|
|
304
|
+
#: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[StreamRow | Elements::Row, void]
|
|
305
|
+
def each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &)
|
|
306
|
+
each_row(trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells, &)
|
|
116
307
|
end
|
|
117
308
|
|
|
118
309
|
# Loads this sheet completely into an in-memory {Elements::Worksheet},
|
|
@@ -123,10 +314,185 @@ module Xlsxrb
|
|
|
123
314
|
# @api public
|
|
124
315
|
#: () -> Elements::Worksheet
|
|
125
316
|
def load
|
|
126
|
-
Xlsxrb.send(
|
|
317
|
+
Xlsxrb.send(
|
|
318
|
+
:build_worksheet,
|
|
319
|
+
@name,
|
|
320
|
+
raw_sheet_xml,
|
|
321
|
+
@shared_strings,
|
|
322
|
+
@styles,
|
|
323
|
+
state: @state,
|
|
324
|
+
zip_reader: @zip_reader,
|
|
325
|
+
entry_name: @entry_name,
|
|
326
|
+
hyperlinks: hyperlinks,
|
|
327
|
+
comments: comments,
|
|
328
|
+
images: images,
|
|
329
|
+
date1904: @date1904,
|
|
330
|
+
dimension: dimension,
|
|
331
|
+
trim_empty_rows: @trim_empty_rows,
|
|
332
|
+
pad_empty_rows: @pad_empty_rows,
|
|
333
|
+
pad_empty_cells: @pad_empty_cells,
|
|
334
|
+
merged_cells: merged_cells,
|
|
335
|
+
auto_filter: auto_filter,
|
|
336
|
+
tables: tables,
|
|
337
|
+
shapes: shapes,
|
|
338
|
+
freeze_pane: freeze_pane,
|
|
339
|
+
page_setup: page_setup,
|
|
340
|
+
page_margins: page_margins,
|
|
341
|
+
header_footer: header_footer,
|
|
342
|
+
print_options: print_options,
|
|
343
|
+
sheet_protection: sheet_protection,
|
|
344
|
+
sheet_view: sheet_view,
|
|
345
|
+
sheet_properties: sheet_properties,
|
|
346
|
+
sheet_format: sheet_format,
|
|
347
|
+
row_breaks: row_breaks,
|
|
348
|
+
col_breaks: col_breaks,
|
|
349
|
+
protected_ranges: protected_ranges,
|
|
350
|
+
cell_watches: cell_watches
|
|
351
|
+
)
|
|
127
352
|
end
|
|
128
353
|
alias to_worksheet load
|
|
129
354
|
|
|
355
|
+
# Returns hyperlinks configured for this worksheet as a Hash of cell references to hyperlink hashes.
|
|
356
|
+
#
|
|
357
|
+
# @return [Hash<String, Hash[Symbol, untyped]>]
|
|
358
|
+
# @api public
|
|
359
|
+
#: () -> Hash[String, Hash[Symbol, untyped]]
|
|
360
|
+
def hyperlinks
|
|
361
|
+
@hyperlinks ||= Xlsxrb.send(:resolve_hyperlinks, raw_sheet_xml, zip_reader: @zip_reader, entry_name: @entry_name)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Returns hyperlink metadata for a specific cell reference, or nil.
|
|
365
|
+
#
|
|
366
|
+
# @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
|
|
367
|
+
# @param col [Integer, nil] Optional 0-based column index.
|
|
368
|
+
# @return [Hash, nil]
|
|
369
|
+
# @api public
|
|
370
|
+
#: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Hash[Symbol, untyped]?
|
|
371
|
+
def hyperlink(ref_or_row, col = nil)
|
|
372
|
+
ref = if col
|
|
373
|
+
"#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
|
|
374
|
+
else
|
|
375
|
+
ref_or_row.to_s.upcase
|
|
376
|
+
end
|
|
377
|
+
hyperlinks[ref]
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Returns comments configured for this worksheet as an Array of comment hashes.
|
|
381
|
+
#
|
|
382
|
+
# @return [Array<Hash[Symbol, untyped]>]
|
|
383
|
+
# @api public
|
|
384
|
+
#: () -> Array[Hash[Symbol, untyped]]
|
|
385
|
+
def comments
|
|
386
|
+
@comments ||= Xlsxrb.send(:resolve_comments, zip_reader: @zip_reader, entry_name: @entry_name)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Returns comment metadata for a specific cell reference, or nil.
|
|
390
|
+
#
|
|
391
|
+
# @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
|
|
392
|
+
# @param col [Integer, nil] Optional 0-based column index.
|
|
393
|
+
# @return [Hash, nil]
|
|
394
|
+
# @api public
|
|
395
|
+
#: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Hash[Symbol, untyped]?
|
|
396
|
+
def comment(ref_or_row, col = nil)
|
|
397
|
+
ref = if col
|
|
398
|
+
"#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
|
|
399
|
+
else
|
|
400
|
+
ref_or_row.to_s.upcase
|
|
401
|
+
end
|
|
402
|
+
comments_by_ref[ref]
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Returns comments indexed by cell reference.
|
|
406
|
+
#
|
|
407
|
+
# @return [Hash<String, Hash[Symbol, untyped]>]
|
|
408
|
+
# @api public
|
|
409
|
+
#: () -> Hash[String, Hash[Symbol, untyped]]
|
|
410
|
+
def comments_by_ref
|
|
411
|
+
@comments_by_ref ||= comments.each_with_object({}) do |c, acc|
|
|
412
|
+
r = c[:ref] || c[:cell]
|
|
413
|
+
acc[r.to_s.upcase] = c if r
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Returns all embedded images in this worksheet.
|
|
418
|
+
#
|
|
419
|
+
# @return [Array<Elements::Image>]
|
|
420
|
+
# @api public
|
|
421
|
+
#: () -> Array[Elements::Image]
|
|
422
|
+
def images
|
|
423
|
+
@images ||= Xlsxrb.send(:resolve_images, zip_reader: @zip_reader, entry_name: @entry_name)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Returns all images anchored at a specific cell reference or 0-based coordinate.
|
|
427
|
+
#
|
|
428
|
+
# @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
|
|
429
|
+
# @param col [Integer, nil] Optional 0-based column index.
|
|
430
|
+
# @return [Array<Elements::Image>]
|
|
431
|
+
# @api public
|
|
432
|
+
#: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Array[Elements::Image]
|
|
433
|
+
def images_at(ref_or_row, col = nil)
|
|
434
|
+
ref = if col
|
|
435
|
+
"#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
|
|
436
|
+
else
|
|
437
|
+
ref_or_row.to_s.upcase
|
|
438
|
+
end
|
|
439
|
+
images.select { |img| img.cell_ref == ref }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Loads and returns images for this worksheet, optionally yielding self for block syntax.
|
|
443
|
+
#
|
|
444
|
+
# @yield [sheet]
|
|
445
|
+
# @yieldparam sheet [StreamSheet]
|
|
446
|
+
# @return [StreamSheet, void]
|
|
447
|
+
# @api public
|
|
448
|
+
#: () { (StreamSheet) -> void } -> void
|
|
449
|
+
#: () -> StreamSheet
|
|
450
|
+
def with_images
|
|
451
|
+
images
|
|
452
|
+
if block_given?
|
|
453
|
+
yield self
|
|
454
|
+
nil
|
|
455
|
+
else
|
|
456
|
+
self
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Access a cell by reference or 0-based coordinates.
|
|
461
|
+
#
|
|
462
|
+
# @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
|
|
463
|
+
# @param col [Integer, nil] Optional 0-based column index.
|
|
464
|
+
# @return [Elements::Cell, nil]
|
|
465
|
+
# @api public
|
|
466
|
+
#: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Elements::Cell?
|
|
467
|
+
def cell(ref_or_row, col = nil)
|
|
468
|
+
target_row, target_col = if col
|
|
469
|
+
[ref_or_row.to_i, col.to_i]
|
|
470
|
+
else
|
|
471
|
+
parsed = Elements::Cell.parse_ref(ref_or_row.to_s)
|
|
472
|
+
return nil unless parsed
|
|
473
|
+
|
|
474
|
+
parsed
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
each_row do |row|
|
|
478
|
+
next if row.index < target_row
|
|
479
|
+
return row.cell_at(target_col) if row.index == target_row
|
|
480
|
+
break if row.index > target_row
|
|
481
|
+
end
|
|
482
|
+
nil
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Returns the formatted string representation of a cell's value.
|
|
486
|
+
#
|
|
487
|
+
# @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
|
|
488
|
+
# @param col [Integer, nil] Optional 0-based column index.
|
|
489
|
+
# @return [String, nil]
|
|
490
|
+
# @api public
|
|
491
|
+
#: (String | Symbol | Integer ref_or_row, ?Integer? col) -> String?
|
|
492
|
+
def formatted_value(ref_or_row, col = nil)
|
|
493
|
+
cell(ref_or_row, col)&.formatted_value
|
|
494
|
+
end
|
|
495
|
+
|
|
130
496
|
# Returns merged cell ranges (e.g. ["A1:B2"]) for this worksheet.
|
|
131
497
|
#
|
|
132
498
|
# @return [Array<String>]
|
|
@@ -179,6 +545,169 @@ module Xlsxrb
|
|
|
179
545
|
end
|
|
180
546
|
end
|
|
181
547
|
|
|
548
|
+
# Returns tables defined in this worksheet.
|
|
549
|
+
#
|
|
550
|
+
# @return [Array<Hash[Symbol, untyped]>]
|
|
551
|
+
# @api public
|
|
552
|
+
#: () -> Array[Hash[Symbol, untyped]]
|
|
553
|
+
def tables
|
|
554
|
+
@tables ||= Xlsxrb.send(:resolve_tables, zip_reader: @zip_reader, entry_name: @entry_name)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Returns shapes defined in this worksheet.
|
|
558
|
+
#
|
|
559
|
+
# @return [Array<Hash[Symbol, untyped]>]
|
|
560
|
+
# @api public
|
|
561
|
+
#: () -> Array[Hash[Symbol, untyped]]
|
|
562
|
+
def shapes
|
|
563
|
+
@shapes ||= Xlsxrb.send(:resolve_shapes, zip_reader: @zip_reader, entry_name: @entry_name)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# Returns freeze pane settings for this worksheet, or nil if none.
|
|
567
|
+
#
|
|
568
|
+
# @return [Hash[Symbol, untyped], nil]
|
|
569
|
+
# @api public
|
|
570
|
+
#: () -> Hash[Symbol, untyped]?
|
|
571
|
+
def freeze_pane
|
|
572
|
+
sheet_view_data[:pane]
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Returns sheet view properties for this worksheet.
|
|
576
|
+
#
|
|
577
|
+
# @return [Hash[Symbol, untyped]]
|
|
578
|
+
# @api public
|
|
579
|
+
#: () -> Hash[Symbol, untyped]
|
|
580
|
+
def sheet_view
|
|
581
|
+
sheet_view_data[:view] || {}
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Returns sheet protection settings as a hash, or nil if unprotected.
|
|
585
|
+
#
|
|
586
|
+
# @return [Hash[Symbol, untyped], nil]
|
|
587
|
+
# @api public
|
|
588
|
+
#: () -> Hash[Symbol, untyped]?
|
|
589
|
+
def sheet_protection
|
|
590
|
+
@sheet_protection ||= if raw_sheet_xml.include?("<sheetProtection")
|
|
591
|
+
listener = Ooxml::Reader::SheetProtectionListener.new
|
|
592
|
+
Ooxml::XmlParser.parse(raw_sheet_xml, listener)
|
|
593
|
+
listener.protection
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Returns sheet-level properties (tabColor, outlinePr) for this worksheet.
|
|
598
|
+
#
|
|
599
|
+
# @return [Hash[Symbol, untyped]]
|
|
600
|
+
# @api public
|
|
601
|
+
#: () -> Hash[Symbol, untyped]
|
|
602
|
+
def sheet_properties
|
|
603
|
+
@sheet_properties ||= if raw_sheet_xml.include?("<sheetPr")
|
|
604
|
+
listener = Ooxml::Reader::SheetPropertiesListener.new
|
|
605
|
+
Ooxml::XmlParser.parse(raw_sheet_xml, listener)
|
|
606
|
+
listener.properties
|
|
607
|
+
else
|
|
608
|
+
{}
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Returns sheet format properties (defaultRowHeight, defaultColWidth, baseColWidth) for this worksheet.
|
|
613
|
+
#
|
|
614
|
+
# @return [Hash[Symbol, untyped]]
|
|
615
|
+
# @api public
|
|
616
|
+
#: () -> Hash[Symbol, untyped]
|
|
617
|
+
def sheet_format
|
|
618
|
+
@sheet_format ||= if raw_sheet_xml.include?("<sheetFormatPr")
|
|
619
|
+
listener = Ooxml::Reader::SheetFormatListener.new
|
|
620
|
+
Ooxml::XmlParser.parse(raw_sheet_xml, listener)
|
|
621
|
+
listener.properties
|
|
622
|
+
else
|
|
623
|
+
{}
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Returns page setup configurations for this worksheet.
|
|
628
|
+
#
|
|
629
|
+
# @return [Hash[Symbol, untyped]]
|
|
630
|
+
# @api public
|
|
631
|
+
#: () -> Hash[Symbol, untyped]
|
|
632
|
+
def page_setup
|
|
633
|
+
print_page_data[:page_setup] || {}
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Returns page margins for this worksheet, or nil if none.
|
|
637
|
+
#
|
|
638
|
+
# @return [Hash[Symbol, untyped], nil]
|
|
639
|
+
# @api public
|
|
640
|
+
#: () -> Hash[Symbol, untyped]?
|
|
641
|
+
def page_margins
|
|
642
|
+
print_page_data[:page_margins]
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Returns header/footer configurations for this worksheet.
|
|
646
|
+
#
|
|
647
|
+
# @return [Hash[Symbol, untyped]]
|
|
648
|
+
# @api public
|
|
649
|
+
#: () -> Hash[Symbol, untyped]
|
|
650
|
+
def header_footer
|
|
651
|
+
print_page_data[:header_footer] || {}
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Returns print options for this worksheet.
|
|
655
|
+
#
|
|
656
|
+
# @return [Hash[Symbol, untyped]]
|
|
657
|
+
# @api public
|
|
658
|
+
#: () -> Hash[Symbol, untyped]
|
|
659
|
+
def print_options
|
|
660
|
+
print_page_data[:print_options] || {}
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Returns explicit row page breaks for this worksheet.
|
|
664
|
+
#
|
|
665
|
+
# @return [Array<untyped>]
|
|
666
|
+
# @api public
|
|
667
|
+
#: () -> Array[untyped]
|
|
668
|
+
def row_breaks
|
|
669
|
+
print_page_data[:row_breaks] || []
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# Returns explicit column page breaks for this worksheet.
|
|
673
|
+
#
|
|
674
|
+
# @return [Array<untyped>]
|
|
675
|
+
# @api public
|
|
676
|
+
#: () -> Array[untyped]
|
|
677
|
+
def col_breaks
|
|
678
|
+
print_page_data[:col_breaks] || []
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Returns protected ranges defined for this worksheet.
|
|
682
|
+
#
|
|
683
|
+
# @return [Array<Hash[Symbol, untyped]>]
|
|
684
|
+
# @api public
|
|
685
|
+
#: () -> Array[Hash[Symbol, untyped]]
|
|
686
|
+
def protected_ranges
|
|
687
|
+
@protected_ranges ||= if raw_sheet_xml.include?("<protectedRanges")
|
|
688
|
+
listener = Ooxml::Reader::ProtectedRangesListener.new
|
|
689
|
+
Ooxml::XmlParser.parse(raw_sheet_xml, listener)
|
|
690
|
+
listener.ranges
|
|
691
|
+
else
|
|
692
|
+
[]
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Returns cell watch cell references defined for this worksheet.
|
|
697
|
+
#
|
|
698
|
+
# @return [Array<String>]
|
|
699
|
+
# @api public
|
|
700
|
+
#: () -> Array[String]
|
|
701
|
+
def cell_watches
|
|
702
|
+
@cell_watches ||= if raw_sheet_xml.include?("<cellWatches")
|
|
703
|
+
listener = Ooxml::Reader::CellWatchesListener.new
|
|
704
|
+
Ooxml::XmlParser.parse(raw_sheet_xml, listener)
|
|
705
|
+
listener.watches
|
|
706
|
+
else
|
|
707
|
+
[]
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
|
|
182
711
|
# Closes any underlying reader resources.
|
|
183
712
|
#: () -> void
|
|
184
713
|
def close
|
|
@@ -188,7 +717,42 @@ module Xlsxrb
|
|
|
188
717
|
|
|
189
718
|
private
|
|
190
719
|
|
|
191
|
-
|
|
720
|
+
#: () -> Hash[Symbol, untyped]
|
|
721
|
+
def sheet_view_data
|
|
722
|
+
@sheet_view_data ||= if raw_sheet_xml.include?("<sheetView")
|
|
723
|
+
listener = Ooxml::Reader::SheetViewListener.new
|
|
724
|
+
Ooxml::XmlParser.parse(raw_sheet_xml, listener)
|
|
725
|
+
{ view: listener.view, pane: listener.pane, selection: listener.selection }
|
|
726
|
+
else
|
|
727
|
+
{ view: {}, pane: nil, selection: nil }
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
#: () -> Hash[Symbol, untyped]
|
|
732
|
+
def print_page_data
|
|
733
|
+
@print_page_data ||= if raw_sheet_xml.match?(/<(?:pageSetup|pageMargins|headerFooter|printOptions|rowBreaks|colBreaks)\b/)
|
|
734
|
+
listener = Ooxml::Reader::PrintPageListener.new
|
|
735
|
+
Ooxml::XmlParser.parse(raw_sheet_xml, listener)
|
|
736
|
+
{
|
|
737
|
+
page_margins: listener.page_margins,
|
|
738
|
+
page_setup: listener.page_setup,
|
|
739
|
+
header_footer: listener.header_footer,
|
|
740
|
+
print_options: listener.print_options,
|
|
741
|
+
row_breaks: listener.row_breaks,
|
|
742
|
+
col_breaks: listener.col_breaks
|
|
743
|
+
}
|
|
744
|
+
else
|
|
745
|
+
{
|
|
746
|
+
page_margins: nil,
|
|
747
|
+
page_setup: {},
|
|
748
|
+
header_footer: {},
|
|
749
|
+
print_options: {},
|
|
750
|
+
row_breaks: [],
|
|
751
|
+
col_breaks: []
|
|
752
|
+
}
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
|
|
192
756
|
#: () -> String
|
|
193
757
|
def raw_sheet_xml
|
|
194
758
|
@raw_sheet_xml ||= if @sheet_source.is_a?(String)
|
|
@@ -203,5 +767,72 @@ module Xlsxrb
|
|
|
203
767
|
""
|
|
204
768
|
end
|
|
205
769
|
end
|
|
770
|
+
|
|
771
|
+
# Returns 1-based bounds [first_row, first_col, last_row, last_col] from dimension ref.
|
|
772
|
+
#: () -> Array[Integer]?
|
|
773
|
+
def dimension_bounds
|
|
774
|
+
return @dimension_bounds if defined?(@dimension_bounds)
|
|
775
|
+
|
|
776
|
+
ref = dimension
|
|
777
|
+
return @dimension_bounds = nil unless ref
|
|
778
|
+
|
|
779
|
+
parts = ref.split(":", 2)
|
|
780
|
+
first_coords = Elements::Cell.parse_ref(parts[0])
|
|
781
|
+
return @dimension_bounds = nil unless first_coords
|
|
782
|
+
|
|
783
|
+
last_coords = parts[1] ? Elements::Cell.parse_ref(parts[1]) : first_coords
|
|
784
|
+
return @dimension_bounds = nil unless last_coords
|
|
785
|
+
|
|
786
|
+
@dimension_bounds = [
|
|
787
|
+
first_coords[0] + 1,
|
|
788
|
+
first_coords[1] + 1,
|
|
789
|
+
last_coords[0] + 1,
|
|
790
|
+
last_coords[1] + 1
|
|
791
|
+
].freeze
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Extracts dimension reference from XML source lazily without parsing the entire file.
|
|
795
|
+
#: () -> String?
|
|
796
|
+
def extract_dimension
|
|
797
|
+
if @sheet_source.is_a?(String)
|
|
798
|
+
@sheet_source.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
|
|
799
|
+
elsif @zip_reader && @entry_name
|
|
800
|
+
stream = @zip_reader.open_entry_io(@entry_name)
|
|
801
|
+
buf = +""
|
|
802
|
+
dim = nil
|
|
803
|
+
begin
|
|
804
|
+
while (chunk = stream.read(65_536))
|
|
805
|
+
buf << chunk
|
|
806
|
+
if (m = buf.match(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/))
|
|
807
|
+
dim = m[1]
|
|
808
|
+
break
|
|
809
|
+
end
|
|
810
|
+
break if buf.include?("<sheetData") || buf.include?(":sheetData")
|
|
811
|
+
end
|
|
812
|
+
ensure
|
|
813
|
+
stream.close
|
|
814
|
+
end
|
|
815
|
+
dim
|
|
816
|
+
elsif @sheet_source.respond_to?(:call)
|
|
817
|
+
buf = +""
|
|
818
|
+
dim = nil
|
|
819
|
+
@sheet_source.call do |chunk|
|
|
820
|
+
buf << chunk
|
|
821
|
+
if (m = buf.match(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/))
|
|
822
|
+
dim = m[1]
|
|
823
|
+
break
|
|
824
|
+
end
|
|
825
|
+
break if buf.include?("<sheetData") || buf.include?(":sheetData")
|
|
826
|
+
end
|
|
827
|
+
dim
|
|
828
|
+
elsif @sheet_source.respond_to?(:seek) && @sheet_source.respond_to?(:pos)
|
|
829
|
+
pos = @sheet_source.pos
|
|
830
|
+
buf = @sheet_source.read(65_536) || ""
|
|
831
|
+
@sheet_source.seek(pos, IO::SEEK_SET)
|
|
832
|
+
buf.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
|
|
833
|
+
else
|
|
834
|
+
raw_sheet_xml.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
|
|
835
|
+
end
|
|
836
|
+
end
|
|
206
837
|
end
|
|
207
838
|
end
|