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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module Xlsxrb
|
|
6
|
+
module Elements
|
|
7
|
+
# Represents an embedded image anchored to worksheet cells.
|
|
8
|
+
# Coordinates (from_row, from_col, to_row, to_col) are 0-based.
|
|
9
|
+
#
|
|
10
|
+
# @example Access image properties
|
|
11
|
+
# image = sheet.images.first
|
|
12
|
+
# image.filename #=> "image1.png"
|
|
13
|
+
# image.cell_ref #=> "B3"
|
|
14
|
+
# image.content_type #=> "image/png"
|
|
15
|
+
# image.data #=> "\x89PNG..."
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
class Image < Data.define(:name, :filename, :content_type, :data, :from_col, :from_row, :to_col, :to_row, :cx, :cy, :unmapped_data)
|
|
19
|
+
MIME_TYPES = {
|
|
20
|
+
"png" => "image/png",
|
|
21
|
+
"jpg" => "image/jpeg",
|
|
22
|
+
"jpeg" => "image/jpeg",
|
|
23
|
+
"gif" => "image/gif",
|
|
24
|
+
"svg" => "image/svg+xml",
|
|
25
|
+
"tif" => "image/tiff",
|
|
26
|
+
"tiff" => "image/tiff",
|
|
27
|
+
"bmp" => "image/bmp",
|
|
28
|
+
"webp" => "image/webp",
|
|
29
|
+
"emf" => "image/x-emf",
|
|
30
|
+
"wmf" => "image/x-wmf"
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# @param filename [String] Target filename (e.g. "image1.png").
|
|
34
|
+
# @param name [String, nil] Image or shape name.
|
|
35
|
+
# @param content_type [String, nil] MIME content type (e.g. "image/png").
|
|
36
|
+
# @param data [String, nil] Raw binary image data.
|
|
37
|
+
# @param from_col [Integer] 0-based starting column index.
|
|
38
|
+
# @param from_row [Integer] 0-based starting row index.
|
|
39
|
+
# @param to_col [Integer, nil] 0-based ending column index (nil for 1-cell anchor).
|
|
40
|
+
# @param to_row [Integer, nil] 0-based ending row index (nil for 1-cell anchor).
|
|
41
|
+
# @param cx [Integer, nil] Extent width in EMUs (English Metric Units).
|
|
42
|
+
# @param cy [Integer, nil] Extent height in EMUs (English Metric Units).
|
|
43
|
+
# @param unmapped_data [Hash] Additional metadata from drawing XML.
|
|
44
|
+
#: (filename: String, ?name: String?, ?content_type: String?, ?data: String?, ?from_col: Integer, ?from_row: Integer, ?to_col: Integer?, ?to_row: Integer?, ?cx: Integer?, ?cy: Integer?, ?unmapped_data: Hash[untyped, untyped]) -> void
|
|
45
|
+
# rubocop:disable Naming/MethodParameterName
|
|
46
|
+
def initialize(filename:, name: nil, content_type: nil, data: nil, from_col: 0, from_row: 0,
|
|
47
|
+
to_col: nil, to_row: nil, cx: nil, cy: nil, unmapped_data: EMPTY_HASH)
|
|
48
|
+
# rubocop:enable Naming/MethodParameterName
|
|
49
|
+
ext = File.extname(filename).delete_prefix(".").downcase
|
|
50
|
+
mime = content_type || MIME_TYPES[ext] || "application/octet-stream"
|
|
51
|
+
super(
|
|
52
|
+
name: name,
|
|
53
|
+
filename: filename,
|
|
54
|
+
content_type: mime,
|
|
55
|
+
data: data&.b,
|
|
56
|
+
from_col: from_col,
|
|
57
|
+
from_row: from_row,
|
|
58
|
+
to_col: to_col,
|
|
59
|
+
to_row: to_row,
|
|
60
|
+
cx: cx,
|
|
61
|
+
cy: cy,
|
|
62
|
+
unmapped_data: unmapped_data
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns the top-left cell coordinate reference string (e.g. "A1").
|
|
67
|
+
#
|
|
68
|
+
# @return [String]
|
|
69
|
+
# @api public
|
|
70
|
+
#: () -> String
|
|
71
|
+
def cell_ref
|
|
72
|
+
"#{Cell.column_letter(from_col)}#{from_row + 1}"
|
|
73
|
+
end
|
|
74
|
+
alias cell cell_ref
|
|
75
|
+
alias ref cell_ref
|
|
76
|
+
|
|
77
|
+
# Returns the bottom-right cell coordinate reference string (e.g. "B5"), or nil for 1-cell anchors.
|
|
78
|
+
#
|
|
79
|
+
# @return [String, nil]
|
|
80
|
+
# @api public
|
|
81
|
+
#: () -> String?
|
|
82
|
+
def to_cell_ref
|
|
83
|
+
return nil if to_col.nil? || to_row.nil?
|
|
84
|
+
|
|
85
|
+
"#{Cell.column_letter(to_col)}#{to_row + 1}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns whether this image uses a one-cell anchor.
|
|
89
|
+
#
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
# @api public
|
|
92
|
+
#: () -> bool
|
|
93
|
+
def one_cell_anchor?
|
|
94
|
+
to_col.nil? || to_row.nil?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns whether this image uses a two-cell anchor.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
# @api public
|
|
101
|
+
#: () -> bool
|
|
102
|
+
def two_cell_anchor?
|
|
103
|
+
!one_cell_anchor?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the size of the binary image data in bytes.
|
|
107
|
+
#
|
|
108
|
+
# @return [Integer]
|
|
109
|
+
# @api public
|
|
110
|
+
#: () -> Integer
|
|
111
|
+
def bytesize
|
|
112
|
+
data ? data.bytesize : 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Writes the image data directly to a file on disk.
|
|
116
|
+
#
|
|
117
|
+
# @param path [String] Target file path.
|
|
118
|
+
# @return [Integer] Number of bytes written.
|
|
119
|
+
# @api public
|
|
120
|
+
#: (String path) -> Integer
|
|
121
|
+
def write_to(path)
|
|
122
|
+
raise Error, "No image binary data available to write" unless data
|
|
123
|
+
|
|
124
|
+
File.binwrite(path, data)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/xlsxrb/elements/row.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Xlsxrb
|
|
|
14
14
|
#
|
|
15
15
|
# @api public
|
|
16
16
|
# rubocop:disable Style/DataInheritance -- Required as class syntax for mutant subject matcher
|
|
17
|
-
class Row < Data.define(:index, :cells, :height, :hidden, :custom_height, :outline_level, :unmapped_data, :errors)
|
|
17
|
+
class Row < Data.define(:index, :cells, :height, :hidden, :custom_height, :collapsed, :outline_level, :unmapped_data, :errors)
|
|
18
18
|
# rubocop:enable Style/DataInheritance
|
|
19
19
|
[Enumerable].each { |m| include m }
|
|
20
20
|
|
|
@@ -23,17 +23,18 @@ module Xlsxrb
|
|
|
23
23
|
# @param height [Float, Integer, nil] Row height in points.
|
|
24
24
|
# @param hidden [Boolean] Whether the row is hidden.
|
|
25
25
|
# @param custom_height [Boolean] Whether custom height is set.
|
|
26
|
+
# @param collapsed [Boolean] Whether the row is collapsed.
|
|
26
27
|
# @param outline_level [Integer, nil] Grouping/outline level.
|
|
27
28
|
# @param unmapped_data [Hash] Additional metadata.
|
|
28
29
|
# @param errors [Array<String>, nil] Validation errors.
|
|
29
|
-
#: (index: Integer, ?cells: Array[Elements::Cell], ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
|
|
30
|
-
def initialize(index:, cells: EMPTY_CELLS, height: nil, hidden: false, custom_height: false,
|
|
31
|
-
unmapped_data: EMPTY_HASH, errors: nil)
|
|
30
|
+
#: (index: Integer, ?cells: Array[Elements::Cell], ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
|
|
31
|
+
def initialize(index:, cells: EMPTY_CELLS, height: nil, hidden: false, custom_height: false, collapsed: false,
|
|
32
|
+
outline_level: nil, unmapped_data: EMPTY_HASH, errors: nil)
|
|
32
33
|
computed_errors = errors || self.class.validate(index, cells)
|
|
33
34
|
computed_errors = computed_errors.freeze unless computed_errors.frozen?
|
|
34
35
|
cells = cells.freeze unless cells.frozen?
|
|
35
36
|
super(index: index, cells: cells, height: height, hidden: hidden,
|
|
36
|
-
custom_height: custom_height, outline_level: outline_level,
|
|
37
|
+
custom_height: custom_height, collapsed: collapsed, outline_level: outline_level,
|
|
37
38
|
unmapped_data: unmapped_data, errors: computed_errors)
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -53,18 +54,144 @@ module Xlsxrb
|
|
|
53
54
|
when Symbol
|
|
54
55
|
case col_index
|
|
55
56
|
when :cells then cells
|
|
57
|
+
when :values then values
|
|
58
|
+
when :raw_values then raw_values
|
|
59
|
+
when :formatted_values then formatted_values
|
|
60
|
+
when :formulas then formulas
|
|
56
61
|
when :index then index
|
|
57
62
|
when :height then height
|
|
58
63
|
when :hidden then hidden
|
|
59
64
|
when :custom_height then custom_height
|
|
65
|
+
when :collapsed then collapsed
|
|
60
66
|
when :outline_level then outline_level
|
|
61
|
-
when :
|
|
67
|
+
when :style_index then style_index
|
|
68
|
+
when :hidden? then hidden?
|
|
69
|
+
when :collapsed? then collapsed?
|
|
70
|
+
when :custom_height? then custom_height?
|
|
71
|
+
when :attributes then attributes
|
|
72
|
+
when :cells_hash then cells_hash
|
|
73
|
+
when :attrs
|
|
74
|
+
h = { height: height, hidden: hidden, custom_height: custom_height, collapsed: collapsed, outline_level: outline_level }
|
|
75
|
+
h[:style_index] = style_index if style_index
|
|
76
|
+
h
|
|
62
77
|
end
|
|
63
78
|
else
|
|
64
79
|
cells[col_index]
|
|
65
80
|
end
|
|
66
81
|
end
|
|
67
82
|
|
|
83
|
+
# Returns a new Row with missing cells padded up to the maximum column index.
|
|
84
|
+
#
|
|
85
|
+
# @return [Elements::Row]
|
|
86
|
+
# @api public
|
|
87
|
+
#: () -> Elements::Row
|
|
88
|
+
def pad_empty_cells
|
|
89
|
+
return self if cells.empty?
|
|
90
|
+
|
|
91
|
+
max_col = cells.map(&:column_index).max || 0
|
|
92
|
+
return self if cells.size == max_col + 1
|
|
93
|
+
|
|
94
|
+
is_d1904 = cells.first ? cells.first.date1904? : false
|
|
95
|
+
cell_map = {}
|
|
96
|
+
cells.each { |c| cell_map[c.column_index] = c }
|
|
97
|
+
padded = Array.new(max_col + 1)
|
|
98
|
+
(0..max_col).each do |c_idx|
|
|
99
|
+
padded[c_idx] = cell_map[c_idx] || Cell.fast_create(index, c_idx, nil, nil, nil, nil, nil, is_d1904)
|
|
100
|
+
end
|
|
101
|
+
Row.new(
|
|
102
|
+
index: index,
|
|
103
|
+
cells: padded,
|
|
104
|
+
height: height,
|
|
105
|
+
hidden: hidden,
|
|
106
|
+
custom_height: custom_height,
|
|
107
|
+
collapsed: collapsed,
|
|
108
|
+
outline_level: outline_level,
|
|
109
|
+
unmapped_data: unmapped_data,
|
|
110
|
+
errors: errors
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns a coordinate-keyed Hash of cell values (e.g. {"A1" => "value", "B1" => 123}).
|
|
115
|
+
#
|
|
116
|
+
# @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
|
|
117
|
+
# @return [Hash{String => Object}]
|
|
118
|
+
# @api public
|
|
119
|
+
#: (?type_cast: bool) -> Hash[String, untyped]
|
|
120
|
+
def cells_hash(type_cast: false)
|
|
121
|
+
result = {}
|
|
122
|
+
cells.each do |c|
|
|
123
|
+
val = c.value
|
|
124
|
+
if type_cast && val.is_a?(Numeric) && c.format_code
|
|
125
|
+
fmt_type = NumberFormatter.format_type(c.format_code)
|
|
126
|
+
begin
|
|
127
|
+
case fmt_type
|
|
128
|
+
when :date
|
|
129
|
+
val = Ooxml::Utils.serial_to_date(val, date1904: c.date1904?)
|
|
130
|
+
when :datetime, :time
|
|
131
|
+
val = Ooxml::Utils.serial_to_datetime(val, date1904: c.date1904?)
|
|
132
|
+
end
|
|
133
|
+
rescue StandardError
|
|
134
|
+
# Keep val numeric on failure
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
result[c.ref] = val
|
|
138
|
+
end
|
|
139
|
+
result
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Returns whether the row is hidden.
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
# @api public
|
|
146
|
+
#: () -> bool
|
|
147
|
+
def hidden?
|
|
148
|
+
hidden == true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Returns whether the row is collapsed.
|
|
152
|
+
#
|
|
153
|
+
# @return [Boolean]
|
|
154
|
+
# @api public
|
|
155
|
+
#: () -> bool
|
|
156
|
+
def collapsed?
|
|
157
|
+
collapsed == true
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns whether custom height is set.
|
|
161
|
+
#
|
|
162
|
+
# @return [Boolean]
|
|
163
|
+
# @api public
|
|
164
|
+
#: () -> bool
|
|
165
|
+
def custom_height?
|
|
166
|
+
custom_height == true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Returns a frozen Hash of row attributes.
|
|
170
|
+
#
|
|
171
|
+
# @return [Hash{Symbol => Object}]
|
|
172
|
+
# @api public
|
|
173
|
+
#: () -> Hash[Symbol, untyped]
|
|
174
|
+
def attributes
|
|
175
|
+
h = {
|
|
176
|
+
index: index,
|
|
177
|
+
height: height,
|
|
178
|
+
hidden: hidden,
|
|
179
|
+
custom_height: custom_height,
|
|
180
|
+
collapsed: collapsed,
|
|
181
|
+
outline_level: outline_level
|
|
182
|
+
}
|
|
183
|
+
h[:style_index] = style_index if style_index
|
|
184
|
+
h.freeze
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Returns style_index if present in unmapped_data.
|
|
188
|
+
#
|
|
189
|
+
# @return [Integer, nil]
|
|
190
|
+
#: () -> Integer?
|
|
191
|
+
def style_index
|
|
192
|
+
unmapped_data[:style_index]
|
|
193
|
+
end
|
|
194
|
+
|
|
68
195
|
# Iterate over cells in this row.
|
|
69
196
|
#
|
|
70
197
|
# @example
|
|
@@ -130,6 +257,17 @@ module Xlsxrb
|
|
|
130
257
|
errors.empty?
|
|
131
258
|
end
|
|
132
259
|
|
|
260
|
+
# Returns whether the row contains no cell values (or all cell values are nil or empty strings).
|
|
261
|
+
#
|
|
262
|
+
# @return [Boolean]
|
|
263
|
+
# @api public
|
|
264
|
+
#: () -> bool
|
|
265
|
+
def empty?
|
|
266
|
+
return true if cells.empty?
|
|
267
|
+
|
|
268
|
+
cells.all? { |c| c.value.nil? || (c.value.is_a?(String) && c.value.empty?) }
|
|
269
|
+
end
|
|
270
|
+
|
|
133
271
|
# Returns the cell at the given 0-based column index, or nil.
|
|
134
272
|
#
|
|
135
273
|
# @param column_index [Integer] 0-based column index.
|
|
@@ -142,15 +280,74 @@ module Xlsxrb
|
|
|
142
280
|
|
|
143
281
|
# Returns cell values as an Array (sparse columns get nil).
|
|
144
282
|
#
|
|
283
|
+
# @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
|
|
145
284
|
# @return [Array<Object>]
|
|
146
285
|
# @api public
|
|
147
|
-
#: () -> Array[untyped]
|
|
148
|
-
def values
|
|
286
|
+
#: (?type_cast: bool) -> Array[untyped]
|
|
287
|
+
def values(type_cast: false)
|
|
288
|
+
return [] if cells.empty?
|
|
289
|
+
|
|
290
|
+
max_col = cells.max_by(&:column_index)&.column_index || 0
|
|
291
|
+
result = Array.new(max_col + 1)
|
|
292
|
+
cells.each do |c|
|
|
293
|
+
val = c.value
|
|
294
|
+
if type_cast && val.is_a?(Numeric) && c.format_code
|
|
295
|
+
fmt_type = NumberFormatter.format_type(c.format_code)
|
|
296
|
+
begin
|
|
297
|
+
case fmt_type
|
|
298
|
+
when :date
|
|
299
|
+
val = Ooxml::Utils.serial_to_date(val, date1904: c.date1904?)
|
|
300
|
+
when :datetime, :time
|
|
301
|
+
val = Ooxml::Utils.serial_to_datetime(val, date1904: c.date1904?)
|
|
302
|
+
end
|
|
303
|
+
rescue StandardError
|
|
304
|
+
# Keep val numeric on failure
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
result[c.column_index] = val
|
|
308
|
+
end
|
|
309
|
+
result
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Returns unparsed raw cell values as an Array (sparse columns get nil).
|
|
313
|
+
#
|
|
314
|
+
# @return [Array<String, nil>]
|
|
315
|
+
# @api public
|
|
316
|
+
#: () -> Array[String?]
|
|
317
|
+
def raw_values
|
|
318
|
+
return [] if cells.empty?
|
|
319
|
+
|
|
320
|
+
max_col = cells.max_by(&:column_index).column_index
|
|
321
|
+
result = Array.new(max_col + 1)
|
|
322
|
+
cells.each { |c| result[c.column_index] = c.raw_value }
|
|
323
|
+
result
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Returns formatted string representations of cell values as an Array (sparse columns get nil).
|
|
327
|
+
#
|
|
328
|
+
# @return [Array<String, nil>]
|
|
329
|
+
# @api public
|
|
330
|
+
#: () -> Array[String?]
|
|
331
|
+
def formatted_values
|
|
332
|
+
return [] if cells.empty?
|
|
333
|
+
|
|
334
|
+
max_col = cells.max_by(&:column_index).column_index
|
|
335
|
+
result = Array.new(max_col + 1)
|
|
336
|
+
cells.each { |c| result[c.column_index] = c.formatted_value }
|
|
337
|
+
result
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Returns formula expressions without leading '=' as an Array (sparse columns get nil).
|
|
341
|
+
#
|
|
342
|
+
# @return [Array<String, nil>]
|
|
343
|
+
# @api public
|
|
344
|
+
#: () -> Array[String?]
|
|
345
|
+
def formulas
|
|
149
346
|
return [] if cells.empty?
|
|
150
347
|
|
|
151
348
|
max_col = cells.max_by(&:column_index).column_index
|
|
152
349
|
result = Array.new(max_col + 1)
|
|
153
|
-
cells.each { |c| result[c.column_index] = c.
|
|
350
|
+
cells.each { |c| result[c.column_index] = c.formula_expression }
|
|
154
351
|
result
|
|
155
352
|
end
|
|
156
353
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative "../number_formatter"
|
|
6
|
+
require_relative "../ooxml/utils"
|
|
7
|
+
|
|
8
|
+
module Xlsxrb
|
|
9
|
+
module Elements
|
|
10
|
+
# Represents the workbook stylesheet definitions (fonts, fills, borders, cellXfs, and numFmts).
|
|
11
|
+
# Inherits from Hash for backward compatibility with existing styles hash structures,
|
|
12
|
+
# while providing precomputed O(1) number format classifications.
|
|
13
|
+
#
|
|
14
|
+
# @api public
|
|
15
|
+
class Styles < Hash #[untyped, untyped]
|
|
16
|
+
# Initializes the styles container, optionally wrapping an existing styles hash.
|
|
17
|
+
#
|
|
18
|
+
# @param initial_hash [Hash, nil] Initial styles dictionary.
|
|
19
|
+
#: (?Hash[untyped, untyped]? initial_hash) -> void
|
|
20
|
+
def initialize(initial_hash = nil)
|
|
21
|
+
super()
|
|
22
|
+
merge!(initial_hash) if initial_hash.is_a?(Hash)
|
|
23
|
+
@format_codes = nil
|
|
24
|
+
@format_types = nil
|
|
25
|
+
@date_flags = nil
|
|
26
|
+
@datetime_flags = nil
|
|
27
|
+
@time_flags = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the format code string for a given style index.
|
|
31
|
+
#
|
|
32
|
+
# @param style_index [Integer, nil]
|
|
33
|
+
# @return [String, nil]
|
|
34
|
+
# @api public
|
|
35
|
+
#: (Integer? style_index) -> String?
|
|
36
|
+
def number_format(style_index)
|
|
37
|
+
return nil unless style_index.is_a?(Integer) && style_index >= 0
|
|
38
|
+
|
|
39
|
+
precompute! unless @format_codes
|
|
40
|
+
@format_codes[style_index]
|
|
41
|
+
end
|
|
42
|
+
alias format_code number_format
|
|
43
|
+
alias format_code_for number_format
|
|
44
|
+
|
|
45
|
+
# Returns the classified format category (:date, :datetime, :time, :number, :text, :general) for a style index.
|
|
46
|
+
#
|
|
47
|
+
# @param style_index [Integer, nil]
|
|
48
|
+
# @return [Symbol, nil]
|
|
49
|
+
# @api public
|
|
50
|
+
#: (Integer? style_index) -> Symbol?
|
|
51
|
+
def format_type(style_index)
|
|
52
|
+
return nil unless style_index.is_a?(Integer) && style_index >= 0
|
|
53
|
+
|
|
54
|
+
precompute! unless @format_types
|
|
55
|
+
@format_types[style_index]
|
|
56
|
+
end
|
|
57
|
+
alias classification_for format_type
|
|
58
|
+
|
|
59
|
+
# Returns whether the style index represents a date, datetime, or time pattern.
|
|
60
|
+
#
|
|
61
|
+
# @param style_index [Integer, nil]
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
# @api public
|
|
64
|
+
#: (Integer? style_index) -> bool
|
|
65
|
+
def date_format?(style_index)
|
|
66
|
+
return false unless style_index.is_a?(Integer) && style_index >= 0
|
|
67
|
+
|
|
68
|
+
precompute! unless @date_flags
|
|
69
|
+
@date_flags[style_index] || false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns whether the style index represents a datetime pattern.
|
|
73
|
+
#
|
|
74
|
+
# @param style_index [Integer, nil]
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
# @api public
|
|
77
|
+
#: (Integer? style_index) -> bool
|
|
78
|
+
def datetime_format?(style_index)
|
|
79
|
+
return false unless style_index.is_a?(Integer) && style_index >= 0
|
|
80
|
+
|
|
81
|
+
precompute! unless @datetime_flags
|
|
82
|
+
@datetime_flags[style_index] || false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns whether the style index represents a time pattern.
|
|
86
|
+
#
|
|
87
|
+
# @param style_index [Integer, nil]
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
# @api public
|
|
90
|
+
#: (Integer? style_index) -> bool
|
|
91
|
+
def time_format?(style_index)
|
|
92
|
+
return false unless style_index.is_a?(Integer) && style_index >= 0
|
|
93
|
+
|
|
94
|
+
precompute! unless @time_flags
|
|
95
|
+
@time_flags[style_index] || false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns whether the style index represents a date-only pattern.
|
|
99
|
+
#
|
|
100
|
+
# @param style_index [Integer, nil]
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
# @api public
|
|
103
|
+
#: (Integer? style_index) -> bool
|
|
104
|
+
def date_only_format?(style_index)
|
|
105
|
+
format_type(style_index) == :date
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Precomputes format code and type classification lookups for all cellXfs.
|
|
109
|
+
#
|
|
110
|
+
# @return [self]
|
|
111
|
+
# @api public
|
|
112
|
+
#: () -> self
|
|
113
|
+
def precompute!
|
|
114
|
+
cell_xfs = self[:cell_xfs]
|
|
115
|
+
size = cell_xfs.is_a?(Array) ? cell_xfs.size : 0
|
|
116
|
+
|
|
117
|
+
codes = Array.new(size)
|
|
118
|
+
types = Array.new(size)
|
|
119
|
+
d_flags = Array.new(size, false)
|
|
120
|
+
dt_flags = Array.new(size, false)
|
|
121
|
+
t_flags = Array.new(size, false)
|
|
122
|
+
|
|
123
|
+
if size.positive?
|
|
124
|
+
cell_style_xfs = self[:cell_style_xfs]
|
|
125
|
+
num_fmts = self[:num_fmts] || {}
|
|
126
|
+
|
|
127
|
+
size.times do |i|
|
|
128
|
+
xf = cell_xfs[i]
|
|
129
|
+
next unless xf.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
num_fmt_id = xf[:num_fmt_id]
|
|
132
|
+
if (num_fmt_id.nil? || num_fmt_id.zero?) && xf[:xf_id] && cell_style_xfs.is_a?(Array)
|
|
133
|
+
parent_xf = cell_style_xfs[xf[:xf_id]]
|
|
134
|
+
num_fmt_id = parent_xf[:num_fmt_id] if parent_xf.is_a?(Hash)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
code = num_fmt_id ? (num_fmts[num_fmt_id] || Ooxml::Utils::BUILTIN_NUM_FMT_CODES[num_fmt_id]) : nil
|
|
138
|
+
|
|
139
|
+
codes[i] = code
|
|
140
|
+
type = NumberFormatter.format_type(code)
|
|
141
|
+
types[i] = type
|
|
142
|
+
d_flags[i] = %i[date datetime time].include?(type)
|
|
143
|
+
dt_flags[i] = (type == :datetime)
|
|
144
|
+
t_flags[i] = (type == :time)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
@format_codes = codes
|
|
149
|
+
@format_types = types
|
|
150
|
+
@date_flags = d_flags
|
|
151
|
+
@datetime_flags = dt_flags
|
|
152
|
+
@time_flags = t_flags
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -53,6 +53,43 @@ module Xlsxrb
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Represents a formatted text run within a rich text string.
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# run = Elements::RichTextRun.new(text: "Hello", font: { bold: true })
|
|
60
|
+
#
|
|
61
|
+
# @api public
|
|
62
|
+
RichTextRun = Data.define(:text, :font) do
|
|
63
|
+
# @param text [String] Plain text for this run.
|
|
64
|
+
# @param font [Hash, nil] Optional font styling properties.
|
|
65
|
+
#: (?text: String, ?font: Hash[Symbol, untyped]?) -> void
|
|
66
|
+
def initialize(text: "", font: nil)
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Supports hash-like key access for backwards compatibility.
|
|
71
|
+
#
|
|
72
|
+
# @param key [Symbol]
|
|
73
|
+
# @return [Object, nil]
|
|
74
|
+
# @api public
|
|
75
|
+
#: (Symbol key) -> untyped
|
|
76
|
+
def [](key)
|
|
77
|
+
case key
|
|
78
|
+
when :text then text
|
|
79
|
+
when :font then font
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns the plain text of this run.
|
|
84
|
+
#
|
|
85
|
+
# @return [String]
|
|
86
|
+
# @api public
|
|
87
|
+
#: () -> String
|
|
88
|
+
def to_s
|
|
89
|
+
text
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
56
93
|
# Represents a rich text string with multiple formatting runs.
|
|
57
94
|
#
|
|
58
95
|
# @example
|
|
@@ -66,7 +103,64 @@ module Xlsxrb
|
|
|
66
103
|
# @api public
|
|
67
104
|
#: () -> String
|
|
68
105
|
def to_s
|
|
69
|
-
runs.map { |r| r[:text] }.join
|
|
106
|
+
runs.map { |r| r[:text] || r.to_s }.join
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Represents a worksheet hyperlink with an optional target URL, display text, tooltip (ScreenTip), and internal location.
|
|
111
|
+
#
|
|
112
|
+
# @example Create a hyperlink
|
|
113
|
+
# link = Elements::Hyperlink.new(url: "https://example.com", tooltip: "Visit website")
|
|
114
|
+
#
|
|
115
|
+
# @api public
|
|
116
|
+
# rubocop:disable Lint/DataDefineOverride -- :display represents the hyperlink display text attribute in OOXML
|
|
117
|
+
Hyperlink = Data.define(:url, :display, :tooltip, :location) do
|
|
118
|
+
# rubocop:enable Lint/DataDefineOverride
|
|
119
|
+
# @param url [String, nil] Target external URL or URI.
|
|
120
|
+
# @param display [String, nil] Optional friendly display label.
|
|
121
|
+
# @param tooltip [String, nil] Optional hover ScreenTip text.
|
|
122
|
+
# @param location [String, nil] Optional internal sheet/cell reference.
|
|
123
|
+
#: (?url: String?, ?display: String?, ?tooltip: String?, ?location: String?) -> void
|
|
124
|
+
def initialize(url: nil, display: nil, tooltip: nil, location: nil)
|
|
125
|
+
super
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Supports hash-like key access for backwards compatibility.
|
|
129
|
+
#
|
|
130
|
+
# @param key [Symbol]
|
|
131
|
+
# @return [Object, nil]
|
|
132
|
+
# @api public
|
|
133
|
+
#: (Symbol key) -> untyped
|
|
134
|
+
def [](key)
|
|
135
|
+
case key
|
|
136
|
+
when :url then url
|
|
137
|
+
when :display then display
|
|
138
|
+
when :tooltip then tooltip
|
|
139
|
+
when :location then location
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Supports Hash conversion.
|
|
144
|
+
#
|
|
145
|
+
# @return [Hash{Symbol => String}]
|
|
146
|
+
# @api public
|
|
147
|
+
#: () -> Hash[Symbol, String]
|
|
148
|
+
def to_h
|
|
149
|
+
h = {}
|
|
150
|
+
h[:url] = url if url
|
|
151
|
+
h[:display] = display if display
|
|
152
|
+
h[:tooltip] = tooltip if tooltip
|
|
153
|
+
h[:location] = location if location
|
|
154
|
+
h
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns the target URL or location string.
|
|
158
|
+
#
|
|
159
|
+
# @return [String, nil]
|
|
160
|
+
# @api public
|
|
161
|
+
#: () -> String?
|
|
162
|
+
def to_s
|
|
163
|
+
url || location
|
|
70
164
|
end
|
|
71
165
|
end
|
|
72
166
|
end
|