xlsxrb 0.1.4 → 0.1.6
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/.devcontainer/Dockerfile +1 -1
- data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +16 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +96 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +68 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +47 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
- data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/csv/3.3/csv.rbs +3871 -0
- data/.gem_rbs_collection/csv/3.3/manifest.yaml +3 -0
- data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +48 -0
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
- data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
- data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/logger/1.7/formatter.rbs +45 -0
- data/.gem_rbs_collection/logger/1.7/log_device.rbs +100 -0
- data/.gem_rbs_collection/logger/1.7/logger.rbs +796 -0
- data/.gem_rbs_collection/logger/1.7/manifest.yaml +2 -0
- data/.gem_rbs_collection/logger/1.7/period.rbs +17 -0
- data/.gem_rbs_collection/logger/1.7/severity.rbs +34 -0
- data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
- data/.gem_rbs_collection/parser/3.2/parser.rbs +194 -0
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +208 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +903 -0
- data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
- data/CHANGELOG.md +42 -3
- data/README.md +130 -49
- data/Rakefile +23 -2
- data/Steepfile +17 -0
- data/benchmark.rb +324 -348
- data/docs/ARCHITECTURE.md +24 -0
- data/docs/DEVELOPMENT.md +58 -0
- data/docs/QUALITY_ASSURANCE.md +26 -0
- data/docs/visual/VisualGallery.md +893 -938
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/xlsxrb/elements/cell.rb +113 -8
- data/lib/xlsxrb/elements/column.rb +2 -0
- data/lib/xlsxrb/elements/row.rb +52 -4
- data/lib/xlsxrb/elements/types.rb +6 -0
- data/lib/xlsxrb/elements/workbook.rb +29 -0
- data/lib/xlsxrb/elements/worksheet.rb +98 -1
- data/lib/xlsxrb/elements.rb +2 -0
- data/lib/xlsxrb/ooxml/reader.rb +8 -6
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +106 -40
- data/lib/xlsxrb/ooxml/styles_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/utils.rb +2 -0
- data/lib/xlsxrb/ooxml/workbook_parser.rb +6 -1
- data/lib/xlsxrb/ooxml/workbook_writer.rb +15 -4
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +156 -23
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +207 -152
- data/lib/xlsxrb/ooxml/writer.rb +217 -5
- data/lib/xlsxrb/ooxml/xml_builder.rb +18 -10
- data/lib/xlsxrb/ooxml/xml_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/zip_generator.rb +5 -8
- data/lib/xlsxrb/ooxml/zip_reader.rb +179 -57
- data/lib/xlsxrb/ooxml/zip_writer.rb +65 -25
- data/lib/xlsxrb/ooxml.rb +3 -1
- data/lib/xlsxrb/style_builder.rb +284 -36
- data/lib/xlsxrb/version.rb +3 -1
- data/lib/xlsxrb.rb +1761 -275
- data/rbs_collection.lock.yaml +252 -0
- data/rbs_collection.yaml +19 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +30 -0
- data/sig/generated/xlsxrb/elements/column.rbs +30 -0
- data/sig/generated/xlsxrb/elements/row.rbs +32 -0
- data/sig/generated/xlsxrb/elements/types.rbs +74 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +25 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +27 -0
- data/sig/generated/xlsxrb/elements.rbs +8 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
- data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
- data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
- data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml.rbs +23 -0
- data/sig/generated/xlsxrb/style_builder.rbs +250 -0
- data/sig/generated/xlsxrb/version.rbs +5 -0
- data/sig/generated/xlsxrb.rbs +1276 -0
- data/sig/rexml.rbs +4 -0
- metadata +145 -4
- data/measure_memory.rb +0 -42
- data/sig/xlsxrb.rbs +0 -23
data/docs/wasm/ruby.wasm
ADDED
|
Binary file
|
data/lib/xlsxrb/elements/cell.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
module Xlsxrb
|
|
4
6
|
module Elements
|
|
5
7
|
# Represents a single cell in a worksheet.
|
|
6
8
|
# All indices are 0-based.
|
|
7
9
|
Cell = Data.define(:row_index, :column_index, :value, :formula, :style_index, :unmapped_data, :errors) do
|
|
8
|
-
def initialize(row_index:, column_index:, value: nil, formula: nil, style_index: nil, unmapped_data:
|
|
10
|
+
def initialize(row_index:, column_index:, value: nil, formula: nil, style_index: nil, unmapped_data: EMPTY_HASH, errors: nil)
|
|
9
11
|
computed_errors = errors || self.class.validate(row_index, column_index, value)
|
|
10
12
|
computed_errors = computed_errors.freeze unless computed_errors.frozen?
|
|
11
13
|
super(row_index: row_index, column_index: column_index, value: value, formula: formula,
|
|
@@ -21,6 +23,69 @@ module Xlsxrb
|
|
|
21
23
|
"#{self.class.column_letter(column_index)}#{row_index + 1}"
|
|
22
24
|
end
|
|
23
25
|
|
|
26
|
+
def [](key)
|
|
27
|
+
case key
|
|
28
|
+
when :value then value
|
|
29
|
+
when :formula then formula
|
|
30
|
+
when :style_index then style_index
|
|
31
|
+
when :ref then ref
|
|
32
|
+
when :column_index then column_index
|
|
33
|
+
when :row_index then row_index
|
|
34
|
+
when :type
|
|
35
|
+
case value
|
|
36
|
+
when String then "s"
|
|
37
|
+
when true, false then "b"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def content
|
|
43
|
+
value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_s
|
|
47
|
+
value.to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_i
|
|
51
|
+
value.to_i
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_f
|
|
55
|
+
value.to_f
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_date
|
|
59
|
+
return value if value.is_a?(Date)
|
|
60
|
+
|
|
61
|
+
# Excel epoch is 1899-12-30
|
|
62
|
+
if value.is_a?(Numeric)
|
|
63
|
+
Date.new(1899, 12, 30) + value.to_i
|
|
64
|
+
else
|
|
65
|
+
begin
|
|
66
|
+
Date.parse(value.to_s)
|
|
67
|
+
rescue StandardError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_time
|
|
74
|
+
return value if value.is_a?(Time)
|
|
75
|
+
|
|
76
|
+
if value.is_a?(Numeric)
|
|
77
|
+
days = value.to_f
|
|
78
|
+
base_time = Time.utc(1899, 12, 30)
|
|
79
|
+
base_time + (days * 86_400)
|
|
80
|
+
else
|
|
81
|
+
begin
|
|
82
|
+
Time.parse(value.to_s)
|
|
83
|
+
rescue StandardError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
24
89
|
# Cache column letters up to Excel's limit (16,384)
|
|
25
90
|
@column_letters = (0...16_384).map do |index|
|
|
26
91
|
result = +""
|
|
@@ -35,6 +100,8 @@ module Xlsxrb
|
|
|
35
100
|
|
|
36
101
|
# Converts a 0-based column index to a letter (0 -> "A", 25 -> "Z", 26 -> "AA").
|
|
37
102
|
def self.column_letter(index)
|
|
103
|
+
raise ArgumentError, "Column index must be a non-negative Integer, got #{index.inspect}" unless index.is_a?(Integer) && index >= 0
|
|
104
|
+
|
|
38
105
|
@column_letters[index] || begin
|
|
39
106
|
result = +""
|
|
40
107
|
i = index
|
|
@@ -47,21 +114,59 @@ module Xlsxrb
|
|
|
47
114
|
end
|
|
48
115
|
end
|
|
49
116
|
|
|
117
|
+
# Converts a column letter (e.g. "A", :AA) to a 0-based column index.
|
|
118
|
+
# If passed an integer or string/symbol representing an integer, it validates and returns the integer.
|
|
119
|
+
def self.column_index(letter)
|
|
120
|
+
if letter.is_a?(Integer)
|
|
121
|
+
raise ArgumentError, "Column index must be >= 0, got #{letter}" if letter.negative?
|
|
122
|
+
|
|
123
|
+
return letter
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
str = letter.to_s
|
|
127
|
+
if str.match?(/\A-?\d+\z/)
|
|
128
|
+
val = str.to_i
|
|
129
|
+
raise ArgumentError, "Column index must be >= 0, got #{val}" if val.negative?
|
|
130
|
+
|
|
131
|
+
return val
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
raise ArgumentError, "Invalid column letter: #{letter.inspect}" unless str.match?(/\A[a-zA-Z]+\z/)
|
|
135
|
+
|
|
136
|
+
str.upcase.chars.reduce(0) { |acc, c| (acc * 26) + (c.ord - "A".ord + 1) } - 1
|
|
137
|
+
end
|
|
138
|
+
|
|
50
139
|
# Parses an Excel-style reference to [row_index, col_index] (both 0-based).
|
|
51
140
|
def self.parse_ref(ref)
|
|
52
|
-
|
|
53
|
-
|
|
141
|
+
return nil unless ref
|
|
142
|
+
|
|
143
|
+
bytes = ref.b
|
|
144
|
+
len = bytes.bytesize
|
|
145
|
+
col = 0
|
|
146
|
+
i = 0
|
|
147
|
+
while i < len
|
|
148
|
+
b = bytes.getbyte(i)
|
|
149
|
+
if b.between?(65, 90)
|
|
150
|
+
col = (col * 26) + (b - 64)
|
|
151
|
+
i += 1
|
|
152
|
+
elsif b.between?(97, 122)
|
|
153
|
+
col = (col * 26) + (b - 96)
|
|
154
|
+
i += 1
|
|
155
|
+
else
|
|
156
|
+
break
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
return nil if i.zero? || i == len
|
|
54
160
|
|
|
55
|
-
|
|
56
|
-
row
|
|
57
|
-
[row, col]
|
|
161
|
+
row = bytes.byteslice(i, len - i).to_i - 1
|
|
162
|
+
[row, col - 1]
|
|
58
163
|
end
|
|
59
164
|
|
|
60
165
|
def self.validate(row_index, column_index, value)
|
|
61
166
|
if row_index.is_a?(Integer) && row_index >= 0 && row_index < 1_048_576 &&
|
|
62
167
|
column_index.is_a?(Integer) && column_index >= 0 && column_index < 16_384 &&
|
|
63
|
-
(value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false || value.is_a?(Date) || value.is_a?(Time))
|
|
64
|
-
return
|
|
168
|
+
(value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false || value.is_a?(Date) || value.is_a?(Time) || value.is_a?(Formula) || (value.is_a?(Hash) && value.key?(:formula)) || value.is_a?(RichText) || value.is_a?(CellError))
|
|
169
|
+
return EMPTY_ERRORS
|
|
65
170
|
end
|
|
66
171
|
|
|
67
172
|
errs = []
|
data/lib/xlsxrb/elements/row.rb
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
module Xlsxrb
|
|
4
6
|
module Elements
|
|
5
7
|
# Represents a single row in a worksheet.
|
|
6
8
|
# index is 0-based.
|
|
7
9
|
Row = Data.define(:index, :cells, :height, :hidden, :custom_height, :outline_level, :unmapped_data, :errors) do
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
def initialize(index:, cells: EMPTY_CELLS, height: nil, hidden: false, custom_height: false, outline_level: nil,
|
|
13
|
+
unmapped_data: EMPTY_HASH, errors: nil)
|
|
10
14
|
computed_errors = errors || self.class.validate(index, cells)
|
|
11
15
|
computed_errors = computed_errors.freeze unless computed_errors.frozen?
|
|
12
16
|
cells = cells.freeze unless cells.frozen?
|
|
@@ -15,6 +19,46 @@ module Xlsxrb
|
|
|
15
19
|
unmapped_data: unmapped_data, errors: computed_errors)
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
def [](col_index)
|
|
23
|
+
case col_index
|
|
24
|
+
when Symbol
|
|
25
|
+
case col_index
|
|
26
|
+
when :cells then cells
|
|
27
|
+
when :index then index
|
|
28
|
+
when :height then height
|
|
29
|
+
when :hidden then hidden
|
|
30
|
+
when :custom_height then custom_height
|
|
31
|
+
when :outline_level then outline_level
|
|
32
|
+
when :attrs then { height: height, hidden: hidden, custom_height: custom_height, outline_level: outline_level }
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
cells[col_index]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def each(&)
|
|
40
|
+
return to_enum(:each) unless block_given?
|
|
41
|
+
|
|
42
|
+
cells.each(&)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def each_cell(&)
|
|
46
|
+
return to_enum(:each_cell) unless block_given?
|
|
47
|
+
|
|
48
|
+
cells.each(&)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_a
|
|
52
|
+
return [] if cells.empty?
|
|
53
|
+
|
|
54
|
+
max_col = cells.map(&:column_index).max
|
|
55
|
+
arr = Array.new(max_col + 1)
|
|
56
|
+
cells.each do |cell|
|
|
57
|
+
arr[cell.column_index] = cell.value
|
|
58
|
+
end
|
|
59
|
+
arr
|
|
60
|
+
end
|
|
61
|
+
|
|
18
62
|
def valid?
|
|
19
63
|
errors.empty?
|
|
20
64
|
end
|
|
@@ -35,10 +79,14 @@ module Xlsxrb
|
|
|
35
79
|
end
|
|
36
80
|
|
|
37
81
|
def self.validate(index, cells)
|
|
38
|
-
return
|
|
82
|
+
return EMPTY_ERRORS if index.is_a?(Integer) && index >= 0 && index < 1_048_576 && cells.is_a?(Array)
|
|
39
83
|
|
|
40
84
|
errs = []
|
|
41
|
-
|
|
85
|
+
if !index.is_a?(Integer) || index.negative?
|
|
86
|
+
errs << "index must be a non-negative Integer (got #{index.inspect})"
|
|
87
|
+
elsif index >= 1_048_576
|
|
88
|
+
errs << "index must be < 1048576 (got #{index}, max row is 1048576)"
|
|
89
|
+
end
|
|
42
90
|
errs << "cells must be an Array (got #{cells.class})" unless cells.is_a?(Array)
|
|
43
91
|
errs
|
|
44
92
|
end
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
module Xlsxrb
|
|
4
6
|
module Elements
|
|
7
|
+
EMPTY_ERRORS = [].freeze
|
|
8
|
+
EMPTY_HASH = {}.freeze
|
|
9
|
+
EMPTY_CELLS = [].freeze
|
|
10
|
+
|
|
5
11
|
# Represents a formula with an optional cached value.
|
|
6
12
|
# Optional: type (:shared, :array), ref (range), shared_index (si for shared formulas)
|
|
7
13
|
Formula = Data.define(:expression, :cached_value, :type, :ref, :shared_index, :calculate_always, :aca, :bx, :dt2d, :dtr, :r1, :r2) do
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
module Xlsxrb
|
|
4
6
|
module Elements
|
|
5
7
|
# Represents an entire XLSX workbook.
|
|
6
8
|
Workbook = Data.define(:sheets, :shared_strings, :styles, :unmapped_data, :errors) do
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
7
11
|
def initialize(sheets: [], shared_strings: [], styles: {}, unmapped_data: {}, errors: nil)
|
|
8
12
|
computed_errors = errors || self.class.validate(sheets)
|
|
9
13
|
super(sheets: sheets.freeze, shared_strings: shared_strings.freeze, styles: styles,
|
|
10
14
|
unmapped_data: unmapped_data, errors: computed_errors.freeze)
|
|
11
15
|
end
|
|
12
16
|
|
|
17
|
+
def each(&)
|
|
18
|
+
sheets.each(&)
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
def valid?
|
|
14
22
|
errors.empty?
|
|
15
23
|
end
|
|
@@ -23,12 +31,33 @@ module Xlsxrb
|
|
|
23
31
|
sheets.find { |s| s.name == identifier }
|
|
24
32
|
end
|
|
25
33
|
end
|
|
34
|
+
alias_method :[], :sheet
|
|
35
|
+
|
|
36
|
+
# Returns a new Workbook with the specified sheet updated.
|
|
37
|
+
# If a block is given, it yields the matched sheet and expects a new Worksheet back.
|
|
38
|
+
def update_sheet(identifier)
|
|
39
|
+
raise ArgumentError, "block is required" unless block_given?
|
|
40
|
+
|
|
41
|
+
sheet_to_update = sheet(identifier)
|
|
42
|
+
raise ArgumentError, "sheet not found: #{identifier}" unless sheet_to_update
|
|
43
|
+
|
|
44
|
+
new_sheet = yield sheet_to_update
|
|
45
|
+
raise TypeError, "block must return a Worksheet" unless new_sheet.is_a?(Worksheet)
|
|
46
|
+
|
|
47
|
+
new_sheets = sheets.map { |s| s == sheet_to_update ? new_sheet : s }
|
|
48
|
+
with(sheets: new_sheets)
|
|
49
|
+
end
|
|
26
50
|
|
|
27
51
|
# Returns sheet names.
|
|
28
52
|
def sheet_names
|
|
29
53
|
sheets.map(&:name)
|
|
30
54
|
end
|
|
31
55
|
|
|
56
|
+
# Save the workbook to a file.
|
|
57
|
+
def save(filepath)
|
|
58
|
+
Xlsxrb.write(filepath, self)
|
|
59
|
+
end
|
|
60
|
+
|
|
32
61
|
def self.validate(sheets)
|
|
33
62
|
errs = []
|
|
34
63
|
errs << "sheets must be an Array (got #{sheets.class})" unless sheets.is_a?(Array)
|
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
module Xlsxrb
|
|
4
6
|
module Elements
|
|
5
7
|
# Represents a single worksheet in a workbook.
|
|
6
8
|
Worksheet = Data.define(:name, :rows, :columns, :charts, :unmapped_data, :errors) do
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
7
11
|
def initialize(name:, rows: [], columns: [], charts: [], unmapped_data: {}, errors: nil)
|
|
8
12
|
computed_errors = errors || self.class.validate(name, rows)
|
|
9
13
|
super(name: name, rows: rows.freeze, columns: columns.freeze, charts: charts.freeze,
|
|
10
14
|
unmapped_data: unmapped_data, errors: computed_errors.freeze)
|
|
11
15
|
end
|
|
12
16
|
|
|
17
|
+
def cells_hash
|
|
18
|
+
h = {}
|
|
19
|
+
rows.each do |r|
|
|
20
|
+
r.cells.each do |c|
|
|
21
|
+
ref = "#{Cell.column_letter(c.column_index)}#{c.row_index + 1}"
|
|
22
|
+
h[ref] = c
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
h
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cells
|
|
29
|
+
# Ensure ordered traversal
|
|
30
|
+
cells_hash.values.sort_by { |c| [c.row_index, c.column_index] }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def [](ref)
|
|
34
|
+
cells_hash[ref.to_s.upcase]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def each(&)
|
|
38
|
+
return to_enum(:each) unless block_given?
|
|
39
|
+
|
|
40
|
+
cells.each(&)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def each_cell(&)
|
|
44
|
+
return to_enum(:each_cell) unless block_given?
|
|
45
|
+
|
|
46
|
+
cells.each(&)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def each_row(&)
|
|
50
|
+
return to_enum(:each_row) unless block_given?
|
|
51
|
+
|
|
52
|
+
rows.each(&)
|
|
53
|
+
end
|
|
54
|
+
|
|
13
55
|
def valid?
|
|
14
56
|
errors.empty?
|
|
15
57
|
end
|
|
@@ -19,6 +61,14 @@ module Xlsxrb
|
|
|
19
61
|
rows.find { |r| r.index == index }
|
|
20
62
|
end
|
|
21
63
|
|
|
64
|
+
def first_row
|
|
65
|
+
rows.min_by(&:index)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def last_row
|
|
69
|
+
rows.max_by(&:index)
|
|
70
|
+
end
|
|
71
|
+
|
|
22
72
|
# Returns cell value at Excel-style reference (e.g. "A1").
|
|
23
73
|
def cell_value(ref)
|
|
24
74
|
parsed = Cell.parse_ref(ref)
|
|
@@ -32,9 +82,56 @@ module Xlsxrb
|
|
|
32
82
|
cell&.value
|
|
33
83
|
end
|
|
34
84
|
|
|
85
|
+
# Returns a new Worksheet with the specified cell updated.
|
|
86
|
+
#
|
|
87
|
+
# @param ref [String] The cell reference (e.g. "B1").
|
|
88
|
+
# @param value [Object] The new cell value.
|
|
89
|
+
# @param style_index [Integer, String, nil] Optional new style index.
|
|
90
|
+
# @param formula [Elements::Formula, nil] Optional new formula.
|
|
91
|
+
# @return [Worksheet] A new Worksheet instance.
|
|
92
|
+
def update_cell(ref, value: nil, style_index: nil, formula: nil)
|
|
93
|
+
parsed = Cell.parse_ref(ref)
|
|
94
|
+
raise ArgumentError, "invalid cell reference: #{ref}" unless parsed
|
|
95
|
+
|
|
96
|
+
row_idx, col_idx = parsed
|
|
97
|
+
existing_row = row_at(row_idx)
|
|
98
|
+
|
|
99
|
+
if existing_row
|
|
100
|
+
existing_cell = existing_row.cell_at(col_idx)
|
|
101
|
+
new_cell = if existing_cell
|
|
102
|
+
existing_cell.with(
|
|
103
|
+
value: value || existing_cell.value,
|
|
104
|
+
style_index: style_index || existing_cell.style_index,
|
|
105
|
+
formula: formula || existing_cell.formula
|
|
106
|
+
)
|
|
107
|
+
else
|
|
108
|
+
Cell.new(row_index: row_idx, column_index: col_idx, value: value, style_index: style_index, formula: formula)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Replace cell in the existing row
|
|
112
|
+
new_cells = existing_row.cells.reject { |c| c.column_index == col_idx }
|
|
113
|
+
new_cells << new_cell
|
|
114
|
+
new_cells.sort_by!(&:column_index)
|
|
115
|
+
|
|
116
|
+
new_row = existing_row.with(cells: new_cells)
|
|
117
|
+
new_rows = rows.map { |r| r.index == row_idx ? new_row : r }
|
|
118
|
+
else
|
|
119
|
+
# Row doesn't exist, create it
|
|
120
|
+
new_cell = Cell.new(row_index: row_idx, column_index: col_idx, value: value, style_index: style_index, formula: formula)
|
|
121
|
+
new_row = Row.new(index: row_idx, cells: [new_cell])
|
|
122
|
+
new_rows = (rows + [new_row]).sort_by!(&:index)
|
|
123
|
+
end
|
|
124
|
+
with(rows: new_rows)
|
|
125
|
+
end
|
|
126
|
+
|
|
35
127
|
def self.validate(name, rows)
|
|
36
128
|
errs = []
|
|
37
|
-
|
|
129
|
+
if name.nil? || !name.is_a?(String) || name.empty?
|
|
130
|
+
errs << "worksheet name must be a non-empty String (got #{name.inspect})"
|
|
131
|
+
else
|
|
132
|
+
errs << "worksheet name cannot exceed 31 characters (got #{name.size})" if name.size > 31
|
|
133
|
+
errs << "worksheet name cannot contain \\, /, ?, *, [, or ]" if name.match?(%r{[\\/?*\[\]]})
|
|
134
|
+
end
|
|
38
135
|
errs << "rows must be an Array (got #{rows.class})" unless rows.is_a?(Array)
|
|
39
136
|
if rows.is_a?(Array)
|
|
40
137
|
indices = rows.map(&:index)
|
data/lib/xlsxrb/elements.rb
CHANGED
data/lib/xlsxrb/ooxml/reader.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "zlib"
|
|
4
6
|
require "rexml/parsers/sax2parser"
|
|
5
7
|
require "rexml/sax2listener"
|
|
@@ -1294,23 +1296,23 @@ module Xlsxrb
|
|
|
1294
1296
|
signature_value = signature.unpack1("V")
|
|
1295
1297
|
break if [0x02014b50, 0x06054b50].include?(signature_value)
|
|
1296
1298
|
|
|
1297
|
-
raise
|
|
1299
|
+
raise ZipError, "invalid ZIP local header signature" unless signature_value == 0x04034b50
|
|
1298
1300
|
|
|
1299
1301
|
header = file.read(26)
|
|
1300
|
-
raise
|
|
1302
|
+
raise ZipError, "truncated ZIP local header" if header.nil? || header.bytesize < 26
|
|
1301
1303
|
|
|
1302
1304
|
_version, flags, compression_method, _mtime, _mdate, _crc32, compressed_size,
|
|
1303
1305
|
_uncompressed_size, file_name_length, extra_field_length = header.unpack("v v v v v V V V v v")
|
|
1304
1306
|
|
|
1305
|
-
raise
|
|
1307
|
+
raise ZipError, "ZIP data descriptor is not supported" if flags.anybits?(0x0008)
|
|
1306
1308
|
|
|
1307
1309
|
file_name = file.read(file_name_length)
|
|
1308
|
-
raise
|
|
1310
|
+
raise ZipError, "truncated ZIP file name" if file_name.nil? || file_name.bytesize < file_name_length
|
|
1309
1311
|
|
|
1310
1312
|
file.read(extra_field_length)
|
|
1311
1313
|
|
|
1312
1314
|
compressed_data = file.read(compressed_size)
|
|
1313
|
-
raise
|
|
1315
|
+
raise ZipError, "truncated ZIP entry data" if compressed_data.nil? || compressed_data.bytesize < compressed_size
|
|
1314
1316
|
|
|
1315
1317
|
next unless file_name == entry_name
|
|
1316
1318
|
|
|
@@ -1325,7 +1327,7 @@ module Xlsxrb
|
|
|
1325
1327
|
inflater.close
|
|
1326
1328
|
end
|
|
1327
1329
|
else
|
|
1328
|
-
raise
|
|
1330
|
+
raise ZipError, "unsupported ZIP compression method: #{compression_method}"
|
|
1329
1331
|
end
|
|
1330
1332
|
end
|
|
1331
1333
|
end
|