xlsxrb 0.1.13 → 0.1.14
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 +57 -0
- data/README.md +2 -0
- 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 +85 -4
- data/lib/xlsxrb/elements/worksheet.rb +424 -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.rb +12 -14
- 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 +53 -15
- data/lib/xlsxrb/ooxml/workbook_writer.rb +65 -14
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +206 -61
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +4 -2
- data/lib/xlsxrb/ooxml/writer.rb +14 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +109 -25
- data/lib/xlsxrb/stream_row.rb +243 -16
- data/lib/xlsxrb/stream_sheet.rb +414 -15
- data/lib/xlsxrb/stream_writer.rb +213 -17
- data/lib/xlsxrb/style_builder.rb +308 -22
- data/lib/xlsxrb/utils.rb +158 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +73 -3
- data/lib/xlsxrb/worksheet_builder.rb +227 -16
- data/lib/xlsxrb.rb +578 -79
- 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 +51 -2
- data/sig/generated/xlsxrb/elements/worksheet.rbs +231 -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/worksheet_parser.rbs +28 -6
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +10 -0
- data/sig/generated/xlsxrb/stream_row.rbs +100 -6
- data/sig/generated/xlsxrb/stream_sheet.rbs +247 -13
- data/sig/generated/xlsxrb/stream_writer.rbs +120 -14
- data/sig/generated/xlsxrb/style_builder.rbs +55 -18
- data/sig/generated/xlsxrb/utils.rbs +119 -0
- data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
- data/sig/generated/xlsxrb/worksheet_builder.rbs +107 -11
- data/sig/generated/xlsxrb.rbs +186 -27
- metadata +11 -1
|
@@ -49,6 +49,7 @@ module Xlsxrb
|
|
|
49
49
|
@total_uncompressed = 0
|
|
50
50
|
@eof = false
|
|
51
51
|
@closed = false
|
|
52
|
+
@bom_checked = false
|
|
52
53
|
@inflater = @compression_method == 8 ? Zlib::Inflate.new(-Zlib::MAX_WBITS) : nil
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -56,6 +57,8 @@ module Xlsxrb
|
|
|
56
57
|
def read(length = nil)
|
|
57
58
|
raise IOError, "closed stream" if @closed
|
|
58
59
|
|
|
60
|
+
check_bom_encoding!
|
|
61
|
+
|
|
59
62
|
if length.nil?
|
|
60
63
|
result = @buffer.dup
|
|
61
64
|
@buffer.clear
|
|
@@ -96,6 +99,11 @@ module Xlsxrb
|
|
|
96
99
|
return if @closed
|
|
97
100
|
|
|
98
101
|
@closed = true
|
|
102
|
+
begin
|
|
103
|
+
@inflater&.reset
|
|
104
|
+
rescue StandardError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
99
107
|
@inflater&.close
|
|
100
108
|
@inflater = nil
|
|
101
109
|
@buffer.clear
|
|
@@ -176,18 +184,48 @@ module Xlsxrb
|
|
|
176
184
|
nil
|
|
177
185
|
end
|
|
178
186
|
end
|
|
187
|
+
|
|
188
|
+
def check_bom_encoding!
|
|
189
|
+
return if @bom_checked
|
|
190
|
+
|
|
191
|
+
@bom_checked = true
|
|
192
|
+
peek = fetch_chunk(DEFAULT_CHUNK_SIZE)
|
|
193
|
+
return unless peek
|
|
194
|
+
|
|
195
|
+
raw_b = peek.b
|
|
196
|
+
if raw_b.start_with?("\xFF\xFE".b)
|
|
197
|
+
rest = +""
|
|
198
|
+
while (chunk = fetch_chunk(DEFAULT_CHUNK_SIZE))
|
|
199
|
+
rest << chunk
|
|
200
|
+
end
|
|
201
|
+
full = (peek + rest).force_encoding(Encoding::UTF_16LE).encode(Encoding::UTF_8).delete_prefix("\uFEFF")
|
|
202
|
+
@buffer = full
|
|
203
|
+
@eof = true
|
|
204
|
+
elsif raw_b.start_with?("\xFE\xFF".b)
|
|
205
|
+
rest = +""
|
|
206
|
+
while (chunk = fetch_chunk(DEFAULT_CHUNK_SIZE))
|
|
207
|
+
rest << chunk
|
|
208
|
+
end
|
|
209
|
+
full = (peek + rest).force_encoding(Encoding::UTF_16BE).encode(Encoding::UTF_8).delete_prefix("\uFEFF")
|
|
210
|
+
@buffer = full
|
|
211
|
+
@eof = true
|
|
212
|
+
elsif raw_b.start_with?("\xEF\xBB\xBF".b)
|
|
213
|
+
@buffer << peek.b.delete_prefix("\xEF\xBB\xBF".b).force_encoding(Encoding::UTF_8)
|
|
214
|
+
else
|
|
215
|
+
@buffer << peek
|
|
216
|
+
end
|
|
217
|
+
end
|
|
179
218
|
end
|
|
180
219
|
|
|
181
220
|
# Opens a ZIP from a file path or IO and yields the reader.
|
|
182
221
|
#: (untyped source, ?max_uncompressed_size: Integer) ?{ (ZipReader) -> untyped } -> (ZipReader | untyped)
|
|
183
222
|
def self.open(source, max_uncompressed_size: MAX_UNCOMPRESSED_SIZE)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
end
|
|
223
|
+
is_binary = source.is_a?(String) && (source.start_with?(LOCAL_HEADER_SIG) || source.include?("\x00"))
|
|
224
|
+
should_close = source.is_a?(String) && !is_binary && File.file?(source)
|
|
225
|
+
io = if should_close
|
|
226
|
+
File.open(source, "rb")
|
|
227
|
+
elsif source.is_a?(String)
|
|
228
|
+
StringIO.new(source.b)
|
|
191
229
|
else
|
|
192
230
|
source
|
|
193
231
|
end
|
|
@@ -211,6 +249,7 @@ module Xlsxrb
|
|
|
211
249
|
@tempfile = nil
|
|
212
250
|
@io = nil
|
|
213
251
|
@catalog = nil
|
|
252
|
+
@ci_catalog = nil
|
|
214
253
|
@cached_entries = nil
|
|
215
254
|
end
|
|
216
255
|
|
|
@@ -227,11 +266,15 @@ module Xlsxrb
|
|
|
227
266
|
# Returns raw bytes for a single entry, or nil if not found.
|
|
228
267
|
#: (String name) -> String?
|
|
229
268
|
def read_entry(name)
|
|
230
|
-
|
|
269
|
+
norm_name = normalize_entry_name(name)
|
|
270
|
+
cached = @cached_entries[norm_name] if @cached_entries&.key?(norm_name)
|
|
231
271
|
|
|
232
|
-
entry =
|
|
272
|
+
entry = lookup_entry(norm_name)
|
|
233
273
|
return nil unless entry
|
|
234
274
|
|
|
275
|
+
cached ||= @cached_entries[entry.name] if @cached_entries&.key?(entry.name)
|
|
276
|
+
return normalize_text_encoding(cached) if cached
|
|
277
|
+
|
|
235
278
|
io.seek(entry.data_offset, IO::SEEK_SET)
|
|
236
279
|
raw = io.read(entry.compressed_size)
|
|
237
280
|
decompress(raw, entry.method)
|
|
@@ -250,7 +293,7 @@ module Xlsxrb
|
|
|
250
293
|
# Opens an EntryStreamIO for an entry by name.
|
|
251
294
|
#: (String entry_name) -> EntryStreamIO
|
|
252
295
|
def open_entry_io(entry_name)
|
|
253
|
-
entry =
|
|
296
|
+
entry = lookup_entry(entry_name)
|
|
254
297
|
raise ArgumentError, "Entry not found in archive: #{entry_name}" unless entry
|
|
255
298
|
|
|
256
299
|
EntryStreamIO.new(io, entry.data_offset, entry.compressed_size, entry.method, max_uncompressed_size: @max_uncompressed_size)
|
|
@@ -272,7 +315,7 @@ module Xlsxrb
|
|
|
272
315
|
# Returns true if the archive contains the specified entry.
|
|
273
316
|
#: (String name) -> bool
|
|
274
317
|
def entry?(name)
|
|
275
|
-
|
|
318
|
+
!lookup_entry(name).nil?
|
|
276
319
|
end
|
|
277
320
|
|
|
278
321
|
# Returns entry metadata catalog.
|
|
@@ -290,7 +333,7 @@ module Xlsxrb
|
|
|
290
333
|
# Copies the specified entry to a ZipWriter directly without decompression.
|
|
291
334
|
#: (String entry_name, ZipWriter writer, ?target_path: String?) -> void
|
|
292
335
|
def copy_to_writer(entry_name, writer, target_path: nil)
|
|
293
|
-
entry =
|
|
336
|
+
entry = lookup_entry(entry_name)
|
|
294
337
|
raise ArgumentError, "Entry not found in archive: #{entry_name}" unless entry
|
|
295
338
|
|
|
296
339
|
if entry.compressed_size.zero? && entry.uncompressed_size.positive?
|
|
@@ -396,7 +439,8 @@ module Xlsxrb
|
|
|
396
439
|
entry_name = io.read(nlen).force_encoding("UTF-8")
|
|
397
440
|
io.read(elen + clen)
|
|
398
441
|
|
|
399
|
-
|
|
442
|
+
norm_name = normalize_entry_name(entry_name)
|
|
443
|
+
next if norm_name.end_with?("/")
|
|
400
444
|
|
|
401
445
|
current_cd_pos = io.pos
|
|
402
446
|
|
|
@@ -411,8 +455,8 @@ module Xlsxrb
|
|
|
411
455
|
local_elen = local_header[24, 2].unpack1("v")
|
|
412
456
|
data_offset = offset + 30 + local_nlen + local_elen
|
|
413
457
|
|
|
414
|
-
|
|
415
|
-
name:
|
|
458
|
+
entry = Entry.new(
|
|
459
|
+
name: norm_name,
|
|
416
460
|
method: method,
|
|
417
461
|
compressed_size: csize,
|
|
418
462
|
uncompressed_size: usize,
|
|
@@ -420,6 +464,8 @@ module Xlsxrb
|
|
|
420
464
|
local_header_offset: offset,
|
|
421
465
|
data_offset: data_offset
|
|
422
466
|
)
|
|
467
|
+
result[norm_name] = entry
|
|
468
|
+
result[entry_name] = entry if entry_name != norm_name
|
|
423
469
|
|
|
424
470
|
io.seek(current_cd_pos, IO::SEEK_SET)
|
|
425
471
|
end
|
|
@@ -453,16 +499,18 @@ module Xlsxrb
|
|
|
453
499
|
io.read(extra_len)
|
|
454
500
|
data_offset = io.pos
|
|
455
501
|
|
|
456
|
-
|
|
502
|
+
norm_name = normalize_entry_name(entry_name)
|
|
503
|
+
next if norm_name.end_with?("/")
|
|
457
504
|
|
|
458
505
|
has_data_descriptor = gp_flag.anybits?(0x08)
|
|
459
506
|
|
|
460
507
|
if has_data_descriptor && compressed_size.zero?
|
|
461
508
|
entry_data, = find_data_descriptor_stream(io, method)
|
|
462
509
|
@cached_entries ||= {}
|
|
463
|
-
@cached_entries[
|
|
464
|
-
|
|
465
|
-
|
|
510
|
+
@cached_entries[norm_name] = entry_data
|
|
511
|
+
@cached_entries[entry_name] = entry_data if entry_name != norm_name
|
|
512
|
+
entry = Entry.new(
|
|
513
|
+
name: norm_name,
|
|
466
514
|
method: method,
|
|
467
515
|
compressed_size: entry_data.bytesize,
|
|
468
516
|
uncompressed_size: entry_data.bytesize,
|
|
@@ -470,9 +518,11 @@ module Xlsxrb
|
|
|
470
518
|
local_header_offset: header_pos,
|
|
471
519
|
data_offset: data_offset
|
|
472
520
|
)
|
|
521
|
+
result[norm_name] = entry
|
|
522
|
+
result[entry_name] = entry if entry_name != norm_name
|
|
473
523
|
else
|
|
474
|
-
|
|
475
|
-
name:
|
|
524
|
+
entry = Entry.new(
|
|
525
|
+
name: norm_name,
|
|
476
526
|
method: method,
|
|
477
527
|
compressed_size: compressed_size,
|
|
478
528
|
uncompressed_size: uncompressed_size,
|
|
@@ -480,6 +530,8 @@ module Xlsxrb
|
|
|
480
530
|
local_header_offset: header_pos,
|
|
481
531
|
data_offset: data_offset
|
|
482
532
|
)
|
|
533
|
+
result[norm_name] = entry
|
|
534
|
+
result[entry_name] = entry if entry_name != norm_name
|
|
483
535
|
|
|
484
536
|
io.seek(compressed_size, IO::SEEK_CUR) if io.respond_to?(:seek)
|
|
485
537
|
end
|
|
@@ -527,11 +579,15 @@ module Xlsxrb
|
|
|
527
579
|
end
|
|
528
580
|
|
|
529
581
|
def decompress(raw, method)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
582
|
+
data = if method.zero?
|
|
583
|
+
raw&.dup || ""
|
|
584
|
+
else
|
|
585
|
+
safe_inflate(raw || "", -Zlib::MAX_WBITS)
|
|
586
|
+
end
|
|
587
|
+
normalize_text_encoding(data)
|
|
533
588
|
rescue Zlib::DataError
|
|
534
|
-
safe_inflate(raw || "", -Zlib::MAX_WBITS)
|
|
589
|
+
data = safe_inflate(raw || "", -Zlib::MAX_WBITS)
|
|
590
|
+
normalize_text_encoding(data)
|
|
535
591
|
end
|
|
536
592
|
|
|
537
593
|
def safe_inflate(raw, wbits)
|
|
@@ -561,6 +617,34 @@ module Xlsxrb
|
|
|
561
617
|
|
|
562
618
|
result.force_encoding("UTF-8")
|
|
563
619
|
end
|
|
620
|
+
|
|
621
|
+
def normalize_entry_name(name)
|
|
622
|
+
name.include?("\\") ? name.tr("\\", "/") : name
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def lookup_entry(name)
|
|
626
|
+
norm_name = normalize_entry_name(name)
|
|
627
|
+
catalog[norm_name] || ci_catalog[norm_name.downcase]
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def ci_catalog
|
|
631
|
+
@ci_catalog ||= catalog.transform_keys(&:downcase)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def normalize_text_encoding(data)
|
|
635
|
+
return data if data.nil? || data.empty?
|
|
636
|
+
|
|
637
|
+
raw_b = data.b
|
|
638
|
+
if raw_b.start_with?("\xFF\xFE".b)
|
|
639
|
+
data.force_encoding(Encoding::UTF_16LE).encode(Encoding::UTF_8).delete_prefix("\uFEFF")
|
|
640
|
+
elsif raw_b.start_with?("\xFE\xFF".b)
|
|
641
|
+
data.force_encoding(Encoding::UTF_16BE).encode(Encoding::UTF_8).delete_prefix("\uFEFF")
|
|
642
|
+
elsif raw_b.start_with?("\xEF\xBB\xBF".b)
|
|
643
|
+
data.b.delete_prefix("\xEF\xBB\xBF".b).force_encoding(Encoding::UTF_8)
|
|
644
|
+
else
|
|
645
|
+
data.force_encoding(Encoding::UTF_8)
|
|
646
|
+
end
|
|
647
|
+
end
|
|
564
648
|
end
|
|
565
649
|
end
|
|
566
650
|
end
|
data/lib/xlsxrb/stream_row.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Xlsxrb
|
|
|
19
19
|
class StreamRow
|
|
20
20
|
[Enumerable].each { |m| include m }
|
|
21
21
|
|
|
22
|
-
attr_reader :index, :height, :hidden, :custom_height, :outline_level
|
|
22
|
+
attr_reader :index, :height, :hidden, :custom_height, :collapsed, :outline_level, :style_index, :pad_empty_cells
|
|
23
23
|
|
|
24
24
|
# @param index [Integer] 0-based row index.
|
|
25
25
|
# @param xml_bytes [String] Raw ASCII-8BIT XML bytes.
|
|
@@ -30,10 +30,14 @@ module Xlsxrb
|
|
|
30
30
|
# @param height [Float, Integer, nil] Row height in points.
|
|
31
31
|
# @param hidden [Boolean] Whether the row is hidden.
|
|
32
32
|
# @param custom_height [Boolean] Whether custom height is set.
|
|
33
|
+
# @param collapsed [Boolean] Whether the row is collapsed.
|
|
33
34
|
# @param outline_level [Integer, nil] Grouping/outline level.
|
|
34
|
-
|
|
35
|
+
# @param style_index [Integer, nil] Style index.
|
|
36
|
+
# @param date1904 [Boolean] Whether the 1904 date system is active.
|
|
37
|
+
# @param pad_empty_cells [Boolean] Whether missing cells in this row are padded.
|
|
38
|
+
#: (index: Integer, xml_bytes: String, from: Integer, to: Integer, shared_strings: Array[String], ?prefix: String, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil, ?style_index: Integer | nil, ?styles: Hash[untyped, untyped]?, ?date1904: bool, ?pad_empty_cells: bool) -> void
|
|
35
39
|
def initialize(index:, xml_bytes:, from:, to:, shared_strings:, prefix: "", height: nil, hidden: false,
|
|
36
|
-
custom_height: false, outline_level: nil)
|
|
40
|
+
custom_height: false, collapsed: false, outline_level: nil, style_index: nil, styles: nil, date1904: false, pad_empty_cells: false)
|
|
37
41
|
@index = index
|
|
38
42
|
@xml = xml_bytes
|
|
39
43
|
@from = from
|
|
@@ -43,12 +47,17 @@ module Xlsxrb
|
|
|
43
47
|
@height = height
|
|
44
48
|
@hidden = hidden
|
|
45
49
|
@custom_height = custom_height
|
|
50
|
+
@collapsed = collapsed
|
|
46
51
|
@outline_level = outline_level
|
|
52
|
+
@style_index = style_index
|
|
53
|
+
@styles = styles
|
|
54
|
+
@date1904 = date1904 ? true : false
|
|
55
|
+
@pad_empty_cells = pad_empty_cells ? true : false
|
|
47
56
|
@cells = nil
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
# rubocop:disable Style/OptionalBooleanParameter
|
|
51
|
-
def self.fast_create(index, xml_bytes, from, to, shared_strings, prefix = "", height = nil, hidden = false, custom_height = false, outline_level = nil)
|
|
60
|
+
def self.fast_create(index, xml_bytes, from, to, shared_strings, prefix = "", height = nil, hidden = false, custom_height = false, outline_level = nil, style_index = nil, styles = nil, date1904 = false, collapsed = false, pad_empty_cells = false)
|
|
52
61
|
inst = allocate
|
|
53
62
|
inst.instance_variable_set(:@index, index)
|
|
54
63
|
inst.instance_variable_set(:@xml, xml_bytes)
|
|
@@ -59,12 +68,80 @@ module Xlsxrb
|
|
|
59
68
|
inst.instance_variable_set(:@height, height)
|
|
60
69
|
inst.instance_variable_set(:@hidden, hidden)
|
|
61
70
|
inst.instance_variable_set(:@custom_height, custom_height)
|
|
71
|
+
inst.instance_variable_set(:@collapsed, collapsed)
|
|
62
72
|
inst.instance_variable_set(:@outline_level, outline_level)
|
|
73
|
+
inst.instance_variable_set(:@style_index, style_index)
|
|
74
|
+
inst.instance_variable_set(:@styles, styles)
|
|
75
|
+
inst.instance_variable_set(:@date1904, date1904 ? true : false)
|
|
76
|
+
inst.instance_variable_set(:@pad_empty_cells, pad_empty_cells ? true : false)
|
|
63
77
|
inst.instance_variable_set(:@cells, nil)
|
|
64
78
|
inst
|
|
65
79
|
end
|
|
66
80
|
# rubocop:enable Style/OptionalBooleanParameter
|
|
67
81
|
|
|
82
|
+
# Returns whether the row is hidden.
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
# @api public
|
|
86
|
+
#: () -> bool
|
|
87
|
+
def hidden?
|
|
88
|
+
@hidden == true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns whether the row is collapsed.
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
# @api public
|
|
95
|
+
#: () -> bool
|
|
96
|
+
def collapsed?
|
|
97
|
+
@collapsed == true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns whether custom height is set.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
# @api public
|
|
104
|
+
#: () -> bool
|
|
105
|
+
def custom_height?
|
|
106
|
+
@custom_height == true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns a frozen Hash of row attributes.
|
|
110
|
+
#
|
|
111
|
+
# @return [Hash{Symbol => Object}]
|
|
112
|
+
# @api public
|
|
113
|
+
#: () -> Hash[Symbol, untyped]
|
|
114
|
+
def attributes
|
|
115
|
+
h = {
|
|
116
|
+
index: @index,
|
|
117
|
+
height: @height,
|
|
118
|
+
hidden: @hidden,
|
|
119
|
+
custom_height: @custom_height,
|
|
120
|
+
collapsed: @collapsed,
|
|
121
|
+
outline_level: @outline_level
|
|
122
|
+
}
|
|
123
|
+
h[:style_index] = @style_index if @style_index
|
|
124
|
+
h.freeze
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Returns whether the row uses the 1904 date system.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean]
|
|
130
|
+
# @api public
|
|
131
|
+
#: () -> bool
|
|
132
|
+
def date1904?
|
|
133
|
+
@date1904 ? true : false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns whether missing cells in this row are padded.
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
# @api public
|
|
140
|
+
#: () -> bool
|
|
141
|
+
def pad_empty_cells?
|
|
142
|
+
@pad_empty_cells ? true : false
|
|
143
|
+
end
|
|
144
|
+
|
|
68
145
|
# Iterate over cells in this streaming row one by one.
|
|
69
146
|
#
|
|
70
147
|
# @yield [cell]
|
|
@@ -76,10 +153,10 @@ module Xlsxrb
|
|
|
76
153
|
def each_cell(&block)
|
|
77
154
|
return enum_for(:each_cell) unless block
|
|
78
155
|
|
|
79
|
-
if @cells
|
|
80
|
-
|
|
156
|
+
if @cells || @pad_empty_cells
|
|
157
|
+
cells.each(&block)
|
|
81
158
|
else
|
|
82
|
-
Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, @index, @prefix, &block)
|
|
159
|
+
Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, @index, @prefix, @styles, @date1904, &block)
|
|
83
160
|
end
|
|
84
161
|
end
|
|
85
162
|
|
|
@@ -103,9 +180,21 @@ module Xlsxrb
|
|
|
103
180
|
def cells
|
|
104
181
|
@cells ||= begin
|
|
105
182
|
arr = []
|
|
106
|
-
Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, @index, @prefix) do |c|
|
|
183
|
+
Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, @index, @prefix, @styles, @date1904) do |c|
|
|
107
184
|
arr << c
|
|
108
185
|
end
|
|
186
|
+
if @pad_empty_cells && !arr.empty?
|
|
187
|
+
max_col = arr.map(&:column_index).max || 0
|
|
188
|
+
if arr.size < max_col + 1
|
|
189
|
+
cell_map = {}
|
|
190
|
+
arr.each { |c| cell_map[c.column_index] = c }
|
|
191
|
+
padded = Array.new(max_col + 1)
|
|
192
|
+
(0..max_col).each do |c_idx|
|
|
193
|
+
padded[c_idx] = cell_map[c_idx] || Elements::Cell.fast_create(@index, c_idx, nil, nil, nil, nil, nil, @date1904)
|
|
194
|
+
end
|
|
195
|
+
arr = padded
|
|
196
|
+
end
|
|
197
|
+
end
|
|
109
198
|
arr.freeze
|
|
110
199
|
end
|
|
111
200
|
end
|
|
@@ -121,12 +210,28 @@ module Xlsxrb
|
|
|
121
210
|
when Symbol
|
|
122
211
|
case col_index
|
|
123
212
|
when :cells then cells
|
|
213
|
+
when :values then values
|
|
214
|
+
when :raw_values then raw_values
|
|
215
|
+
when :formatted_values then formatted_values
|
|
216
|
+
when :formulas then formulas
|
|
124
217
|
when :index then index
|
|
125
218
|
when :height then height
|
|
126
219
|
when :hidden then hidden
|
|
127
220
|
when :custom_height then custom_height
|
|
221
|
+
when :collapsed then collapsed
|
|
128
222
|
when :outline_level then outline_level
|
|
129
|
-
when :
|
|
223
|
+
when :style_index then style_index
|
|
224
|
+
when :date1904 then date1904?
|
|
225
|
+
when :hidden? then hidden?
|
|
226
|
+
when :collapsed? then collapsed?
|
|
227
|
+
when :custom_height? then custom_height?
|
|
228
|
+
when :pad_empty_cells then @pad_empty_cells
|
|
229
|
+
when :pad_empty_cells? then pad_empty_cells?
|
|
230
|
+
when :attributes then attributes
|
|
231
|
+
when :attrs
|
|
232
|
+
h = { height: height, hidden: hidden, custom_height: custom_height, collapsed: collapsed, outline_level: outline_level }
|
|
233
|
+
h[:style_index] = style_index if style_index
|
|
234
|
+
h
|
|
130
235
|
end
|
|
131
236
|
else
|
|
132
237
|
cells[col_index]
|
|
@@ -143,29 +248,107 @@ module Xlsxrb
|
|
|
143
248
|
cells.find { |c| c.column_index == col_index }
|
|
144
249
|
end
|
|
145
250
|
|
|
251
|
+
# Convert row cells to a coordinate-keyed Hash (e.g. {"A1" => "value", "B1" => 123}).
|
|
252
|
+
#
|
|
253
|
+
# @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
|
|
254
|
+
# @return [Hash{String => Object}]
|
|
255
|
+
# @api public
|
|
256
|
+
#: (?type_cast: bool) -> Hash[String, untyped]
|
|
257
|
+
def to_h(type_cast: false)
|
|
258
|
+
result = {}
|
|
259
|
+
each_cell do |c|
|
|
260
|
+
val = c.value
|
|
261
|
+
if type_cast && val.is_a?(Numeric)
|
|
262
|
+
fmt_type = if c.style_index && @styles
|
|
263
|
+
NumberFormatter.format_type_for(c.style_index, @styles)
|
|
264
|
+
elsif c.format_code
|
|
265
|
+
NumberFormatter.format_type(c.format_code)
|
|
266
|
+
end
|
|
267
|
+
begin
|
|
268
|
+
case fmt_type
|
|
269
|
+
when :date
|
|
270
|
+
val = Ooxml::Utils.serial_to_date(val, date1904: @date1904)
|
|
271
|
+
when :datetime, :time
|
|
272
|
+
val = Ooxml::Utils.serial_to_datetime(val, date1904: @date1904)
|
|
273
|
+
end
|
|
274
|
+
rescue StandardError
|
|
275
|
+
# Keep val numeric on failure
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
result[c.ref] = val
|
|
279
|
+
end
|
|
280
|
+
result
|
|
281
|
+
end
|
|
282
|
+
|
|
146
283
|
# Convert row cells to an Array of raw values (sparse columns get nil).
|
|
147
284
|
#
|
|
148
285
|
# @return [Array<Object>]
|
|
149
286
|
# @api public
|
|
150
287
|
#: () -> Array[untyped]
|
|
151
288
|
def to_a
|
|
289
|
+
values
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Returns cell values as an Array (sparse columns get nil).
|
|
293
|
+
#
|
|
294
|
+
# @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
|
|
295
|
+
# @return [Array<Object>]
|
|
296
|
+
# @api public
|
|
297
|
+
#: (?type_cast: bool) -> Array[untyped]
|
|
298
|
+
def values(type_cast: false)
|
|
299
|
+
if @cells
|
|
300
|
+
values_from_cells(type_cast)
|
|
301
|
+
else
|
|
302
|
+
Ooxml::WorksheetParser.fast_scan_row_values(@xml, @from, @to, @shared_strings, @styles, @date1904, type_cast: type_cast)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Convert row cells to an Array of raw string values (sparse columns get nil).
|
|
307
|
+
#
|
|
308
|
+
# @return [Array<String, nil>]
|
|
309
|
+
# @api public
|
|
310
|
+
#: () -> Array[String?]
|
|
311
|
+
def raw_values
|
|
152
312
|
return [] if cells.empty?
|
|
153
313
|
|
|
154
314
|
max_col = cells.map(&:column_index).max || 0
|
|
155
315
|
arr = Array.new(max_col + 1)
|
|
156
316
|
cells.each do |cell|
|
|
157
|
-
arr[cell.column_index] = cell.
|
|
317
|
+
arr[cell.column_index] = cell.raw_value
|
|
158
318
|
end
|
|
159
319
|
arr
|
|
160
320
|
end
|
|
161
321
|
|
|
162
|
-
#
|
|
322
|
+
# Convert row cells to an Array of formatted string representations (sparse columns get nil).
|
|
163
323
|
#
|
|
164
|
-
# @return [Array<
|
|
324
|
+
# @return [Array<String, nil>]
|
|
165
325
|
# @api public
|
|
166
|
-
#: () -> Array[
|
|
167
|
-
def
|
|
168
|
-
|
|
326
|
+
#: () -> Array[String?]
|
|
327
|
+
def formatted_values
|
|
328
|
+
return [] if cells.empty?
|
|
329
|
+
|
|
330
|
+
max_col = cells.map(&:column_index).max || 0
|
|
331
|
+
arr = Array.new(max_col + 1)
|
|
332
|
+
cells.each do |cell|
|
|
333
|
+
arr[cell.column_index] = cell.formatted_value
|
|
334
|
+
end
|
|
335
|
+
arr
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Convert row cells to an Array of formula expressions without leading '=' (sparse columns get nil).
|
|
339
|
+
#
|
|
340
|
+
# @return [Array<String, nil>]
|
|
341
|
+
# @api public
|
|
342
|
+
#: () -> Array[String?]
|
|
343
|
+
def formulas
|
|
344
|
+
return [] if cells.empty?
|
|
345
|
+
|
|
346
|
+
max_col = cells.map(&:column_index).max || 0
|
|
347
|
+
arr = Array.new(max_col + 1)
|
|
348
|
+
cells.each do |cell|
|
|
349
|
+
arr[cell.column_index] = cell.formula_expression
|
|
350
|
+
end
|
|
351
|
+
arr
|
|
169
352
|
end
|
|
170
353
|
|
|
171
354
|
# Returns whether the row is valid according to OOXML specifications.
|
|
@@ -177,13 +360,25 @@ module Xlsxrb
|
|
|
177
360
|
true
|
|
178
361
|
end
|
|
179
362
|
|
|
363
|
+
# Returns whether the row contains no cell values (or all cell values are nil or empty strings).
|
|
364
|
+
#
|
|
365
|
+
# @return [Boolean]
|
|
366
|
+
# @api public
|
|
367
|
+
#: () -> bool
|
|
368
|
+
def empty?
|
|
369
|
+
vals = values
|
|
370
|
+
return true if vals.empty?
|
|
371
|
+
|
|
372
|
+
vals.all? { |v| v.nil? || (v.is_a?(String) && v.empty?) }
|
|
373
|
+
end
|
|
374
|
+
|
|
180
375
|
# Unmapped metadata for compatibility with Elements::Row.
|
|
181
376
|
#
|
|
182
377
|
# @return [Hash]
|
|
183
378
|
# @api public
|
|
184
379
|
#: () -> Hash[untyped, untyped]
|
|
185
380
|
def unmapped_data
|
|
186
|
-
Elements::EMPTY_HASH
|
|
381
|
+
@style_index ? { style_index: @style_index } : Elements::EMPTY_HASH
|
|
187
382
|
end
|
|
188
383
|
|
|
189
384
|
# Validation errors for compatibility with Elements::Row.
|
|
@@ -203,5 +398,37 @@ module Xlsxrb
|
|
|
203
398
|
def inspect
|
|
204
399
|
"#<#{self.class.name} index=#{index} height=#{height.inspect} hidden=#{hidden}>"
|
|
205
400
|
end
|
|
401
|
+
|
|
402
|
+
private
|
|
403
|
+
|
|
404
|
+
#: (bool) -> Array[untyped]
|
|
405
|
+
def values_from_cells(type_cast)
|
|
406
|
+
return [] if @cells.nil? || @cells.empty?
|
|
407
|
+
|
|
408
|
+
max_col = @cells.map(&:column_index).max || 0
|
|
409
|
+
arr = Array.new(max_col + 1)
|
|
410
|
+
@cells.each do |c|
|
|
411
|
+
val = c.value
|
|
412
|
+
if type_cast && val.is_a?(Numeric)
|
|
413
|
+
fmt_type = if c.style_index && @styles
|
|
414
|
+
NumberFormatter.format_type_for(c.style_index, @styles)
|
|
415
|
+
elsif c.format_code
|
|
416
|
+
NumberFormatter.format_type(c.format_code)
|
|
417
|
+
end
|
|
418
|
+
begin
|
|
419
|
+
case fmt_type
|
|
420
|
+
when :date
|
|
421
|
+
val = Ooxml::Utils.serial_to_date(val, date1904: @date1904)
|
|
422
|
+
when :datetime, :time
|
|
423
|
+
val = Ooxml::Utils.serial_to_datetime(val, date1904: @date1904)
|
|
424
|
+
end
|
|
425
|
+
rescue StandardError
|
|
426
|
+
# Keep val numeric on failure
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
arr[c.column_index] = val
|
|
430
|
+
end
|
|
431
|
+
arr
|
|
432
|
+
end
|
|
206
433
|
end
|
|
207
434
|
end
|