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
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require "date"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module Xlsxrb
|
|
9
|
+
# Formats cell values according to OpenXML formatCode strings.
|
|
10
|
+
# Evaluates and converts numeric, currency, percentage, date/time, scientific,
|
|
11
|
+
# and accounting format patterns into string representations.
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
module NumberFormatter
|
|
15
|
+
# Built-in date and time format code strings.
|
|
16
|
+
DATE_PATTERNS = [
|
|
17
|
+
"mm-dd-yy", "d-mmm-yy", "d-mmm", "mmm-yy", "h:mm AM/PM", "h:mm:ss AM/PM", "h:mm", "h:mm:ss", "m/d/yy h:mm", "mm:ss", "[h]:mm:ss", "mmss.0"
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Formats a cell value according to an Excel format code string.
|
|
22
|
+
#
|
|
23
|
+
# @param value [Object, nil] The cell's raw or typed value.
|
|
24
|
+
# @param format_code [String, nil] Excel format code pattern.
|
|
25
|
+
# @param date1904 [Boolean] Whether the 1904 date system is active.
|
|
26
|
+
# @return [String] Formatted string representation.
|
|
27
|
+
# @api public
|
|
28
|
+
#: (untyped value, ?String? format_code, ?date1904: bool) -> String
|
|
29
|
+
def format(value, format_code = nil, date1904: false)
|
|
30
|
+
return "" if value.nil?
|
|
31
|
+
return value.code.to_s if value.respond_to?(:code)
|
|
32
|
+
return value ? "TRUE" : "FALSE" if [true, false].include?(value)
|
|
33
|
+
|
|
34
|
+
code = format_code.to_s.strip
|
|
35
|
+
return default_format(value) if code.empty? || code.casecmp("general").zero?
|
|
36
|
+
|
|
37
|
+
return format_datetime(value, code) if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
|
38
|
+
|
|
39
|
+
if value.is_a?(Numeric) && date_format?(code)
|
|
40
|
+
dt = if value.is_a?(Float) && (value % 1).positive?
|
|
41
|
+
Ooxml::Utils.serial_to_datetime(value, date1904: date1904)
|
|
42
|
+
else
|
|
43
|
+
Ooxml::Utils.serial_to_date(value.to_i, date1904: date1904)
|
|
44
|
+
end
|
|
45
|
+
return format_datetime(dt, code)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return format_number(value, code) if value.is_a?(Numeric)
|
|
49
|
+
|
|
50
|
+
if code.include?("@")
|
|
51
|
+
sections = split_sections(code)
|
|
52
|
+
pattern = sections[3] || sections[0]
|
|
53
|
+
pattern.gsub("@", value.to_s)
|
|
54
|
+
else
|
|
55
|
+
value.to_s
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Resolves the format code string for a given style index from a styles definition hash.
|
|
60
|
+
#
|
|
61
|
+
# @param style_index [Integer, nil] 0-based style index in cellXfs.
|
|
62
|
+
# @param styles [Hash, nil] Parsed styles hash.
|
|
63
|
+
# @return [String, nil] Format code string, or nil if not found.
|
|
64
|
+
# @api public
|
|
65
|
+
#: (Integer? style_index, untyped styles) -> String?
|
|
66
|
+
def format_code_for(style_index, styles)
|
|
67
|
+
return nil unless style_index && styles
|
|
68
|
+
return styles.number_format(style_index) if styles.respond_to?(:number_format)
|
|
69
|
+
|
|
70
|
+
cell_xfs = styles[:cell_xfs]
|
|
71
|
+
return nil unless cell_xfs.is_a?(Array) && style_index < cell_xfs.size
|
|
72
|
+
|
|
73
|
+
xf = cell_xfs[style_index]
|
|
74
|
+
return nil unless xf.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
num_fmt_id = xf[:num_fmt_id]
|
|
77
|
+
if (num_fmt_id.nil? || num_fmt_id.zero?) && xf[:xf_id] && styles[:cell_style_xfs].is_a?(Array)
|
|
78
|
+
parent_xf = styles[:cell_style_xfs][xf[:xf_id]]
|
|
79
|
+
num_fmt_id = parent_xf[:num_fmt_id] if parent_xf.is_a?(Hash)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return nil unless num_fmt_id
|
|
83
|
+
|
|
84
|
+
num_fmts = styles[:num_fmts] || {}
|
|
85
|
+
num_fmts[num_fmt_id] || Ooxml::Utils::BUILTIN_NUM_FMT_CODES[num_fmt_id]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns the classified format category for a format code string.
|
|
89
|
+
#
|
|
90
|
+
# @param format_code [String, nil] Format code string.
|
|
91
|
+
# @return [Symbol, nil] :date, :datetime, :time, :number, :text, :general, or nil.
|
|
92
|
+
# @api public
|
|
93
|
+
#: (String? format_code) -> Symbol?
|
|
94
|
+
def format_type(format_code)
|
|
95
|
+
return nil if format_code.nil?
|
|
96
|
+
|
|
97
|
+
code = format_code.to_s.strip
|
|
98
|
+
return :general if code.empty? || code.casecmp("general").zero?
|
|
99
|
+
return :text if code == "@"
|
|
100
|
+
|
|
101
|
+
case code
|
|
102
|
+
when "mm-dd-yy", "d-mmm-yy", "d-mmm", "mmm-yy", "yyyy\\-mm\\-dd", "yyyy-mm-dd", "m/d/yy", "m/d/yyyy"
|
|
103
|
+
return :date
|
|
104
|
+
when "m/d/yy h:mm", "m/d/yyyy h:mm", "yyyy\\-mm\\-dd\\ hh:mm:ss", "yyyy-mm-dd hh:mm:ss"
|
|
105
|
+
return :datetime
|
|
106
|
+
when "h:mm AM/PM", "h:mm:ss AM/PM", "h:mm", "h:mm:ss", "mm:ss", "[h]:mm:ss", "mmss.0"
|
|
107
|
+
return :time
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
stripped = code.gsub(/"[^"]*"/, "")
|
|
111
|
+
.gsub(/\\[.]/, "")
|
|
112
|
+
.gsub(/\[(?!h\]|m\]|s\]|hh\]|mm\]|ss\])[^\]]*\]/i, "")
|
|
113
|
+
|
|
114
|
+
has_date = stripped.match?(/[ydYD]/) || (stripped.match?(/[mM]/) && !stripped.match?(/[hsHS]/))
|
|
115
|
+
has_time = stripped.match?(/[hsHS]/)
|
|
116
|
+
|
|
117
|
+
if has_date && has_time
|
|
118
|
+
:datetime
|
|
119
|
+
elsif has_date
|
|
120
|
+
:date
|
|
121
|
+
elsif has_time
|
|
122
|
+
:time
|
|
123
|
+
elsif stripped.include?("@")
|
|
124
|
+
:text
|
|
125
|
+
else
|
|
126
|
+
:number
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Checks whether a format code string represents a date, datetime, or time pattern.
|
|
131
|
+
#
|
|
132
|
+
# @param format_code [String, nil]
|
|
133
|
+
# @param num_fmt_id [Integer, nil]
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
#: (String? format_code, ?Integer? num_fmt_id) -> bool
|
|
136
|
+
def date_format?(format_code, num_fmt_id = nil)
|
|
137
|
+
code = format_code
|
|
138
|
+
code = Ooxml::Utils::BUILTIN_NUM_FMT_CODES[num_fmt_id] if (code.nil? || code.empty?) && num_fmt_id
|
|
139
|
+
return false if code.nil? || code.empty?
|
|
140
|
+
|
|
141
|
+
type = format_type(code)
|
|
142
|
+
%i[date datetime time].include?(type)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Checks whether a format code string represents a date-only pattern.
|
|
146
|
+
#
|
|
147
|
+
# @param format_code [String, nil]
|
|
148
|
+
# @param num_fmt_id [Integer, nil]
|
|
149
|
+
# @return [Boolean]
|
|
150
|
+
# @api public
|
|
151
|
+
#: (String? format_code, ?Integer? num_fmt_id) -> bool
|
|
152
|
+
def date_only_format?(format_code, num_fmt_id = nil)
|
|
153
|
+
code = format_code
|
|
154
|
+
code = Ooxml::Utils::BUILTIN_NUM_FMT_CODES[num_fmt_id] if (code.nil? || code.empty?) && num_fmt_id
|
|
155
|
+
return false if code.nil? || code.empty?
|
|
156
|
+
|
|
157
|
+
format_type(code) == :date
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Checks whether a format code string represents a datetime pattern.
|
|
161
|
+
#
|
|
162
|
+
# @param format_code [String, nil]
|
|
163
|
+
# @param num_fmt_id [Integer, nil]
|
|
164
|
+
# @return [Boolean]
|
|
165
|
+
# @api public
|
|
166
|
+
#: (String? format_code, ?Integer? num_fmt_id) -> bool
|
|
167
|
+
def datetime_format?(format_code, num_fmt_id = nil)
|
|
168
|
+
code = format_code
|
|
169
|
+
code = Ooxml::Utils::BUILTIN_NUM_FMT_CODES[num_fmt_id] if (code.nil? || code.empty?) && num_fmt_id
|
|
170
|
+
return false if code.nil? || code.empty?
|
|
171
|
+
|
|
172
|
+
format_type(code) == :datetime
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Checks whether a format code string represents a time-only pattern.
|
|
176
|
+
#
|
|
177
|
+
# @param format_code [String, nil]
|
|
178
|
+
# @param num_fmt_id [Integer, nil]
|
|
179
|
+
# @return [Boolean]
|
|
180
|
+
# @api public
|
|
181
|
+
#: (String? format_code, ?Integer? num_fmt_id) -> bool
|
|
182
|
+
def time_format?(format_code, num_fmt_id = nil)
|
|
183
|
+
code = format_code
|
|
184
|
+
code = Ooxml::Utils::BUILTIN_NUM_FMT_CODES[num_fmt_id] if (code.nil? || code.empty?) && num_fmt_id
|
|
185
|
+
return false if code.nil? || code.empty?
|
|
186
|
+
|
|
187
|
+
format_type(code) == :time
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Returns the format category Symbol for a style index and styles definition.
|
|
191
|
+
#
|
|
192
|
+
# @param style_index [Integer, nil]
|
|
193
|
+
# @param styles [untyped]
|
|
194
|
+
# @return [Symbol, nil]
|
|
195
|
+
# @api public
|
|
196
|
+
#: (Integer? style_index, untyped styles) -> Symbol?
|
|
197
|
+
def format_type_for(style_index, styles)
|
|
198
|
+
return styles.format_type(style_index) if styles.respond_to?(:format_type)
|
|
199
|
+
|
|
200
|
+
code = format_code_for(style_index, styles)
|
|
201
|
+
format_type(code)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Checks whether a style index represents a date, datetime, or time format.
|
|
205
|
+
#
|
|
206
|
+
# @param style_index [Integer, nil]
|
|
207
|
+
# @param styles [untyped]
|
|
208
|
+
# @return [Boolean]
|
|
209
|
+
# @api public
|
|
210
|
+
#: (Integer? style_index, untyped styles) -> bool
|
|
211
|
+
def date_format_for?(style_index, styles)
|
|
212
|
+
return styles.date_format?(style_index) if styles.respond_to?(:date_format?)
|
|
213
|
+
|
|
214
|
+
code = format_code_for(style_index, styles)
|
|
215
|
+
date_format?(code)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Checks whether a style index represents a datetime format.
|
|
219
|
+
#
|
|
220
|
+
# @param style_index [Integer, nil]
|
|
221
|
+
# @param styles [untyped]
|
|
222
|
+
# @return [Boolean]
|
|
223
|
+
# @api public
|
|
224
|
+
#: (Integer? style_index, untyped styles) -> bool
|
|
225
|
+
def datetime_format_for?(style_index, styles)
|
|
226
|
+
return styles.datetime_format?(style_index) if styles.respond_to?(:datetime_format?)
|
|
227
|
+
|
|
228
|
+
code = format_code_for(style_index, styles)
|
|
229
|
+
datetime_format?(code)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Checks whether a style index represents a time format.
|
|
233
|
+
#
|
|
234
|
+
# @param style_index [Integer, nil]
|
|
235
|
+
# @param styles [untyped]
|
|
236
|
+
# @return [Boolean]
|
|
237
|
+
# @api public
|
|
238
|
+
#: (Integer? style_index, untyped styles) -> bool
|
|
239
|
+
def time_format_for?(style_index, styles)
|
|
240
|
+
return styles.time_format?(style_index) if styles.respond_to?(:time_format?)
|
|
241
|
+
|
|
242
|
+
code = format_code_for(style_index, styles)
|
|
243
|
+
time_format?(code)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Checks whether a style index represents a date-only format.
|
|
247
|
+
#
|
|
248
|
+
# @param style_index [Integer, nil]
|
|
249
|
+
# @param styles [untyped]
|
|
250
|
+
# @return [Boolean]
|
|
251
|
+
# @api public
|
|
252
|
+
#: (Integer? style_index, untyped styles) -> bool
|
|
253
|
+
def date_only_format_for?(style_index, styles)
|
|
254
|
+
return styles.date_only_format?(style_index) if styles.respond_to?(:date_only_format?)
|
|
255
|
+
|
|
256
|
+
code = format_code_for(style_index, styles)
|
|
257
|
+
date_only_format?(code)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
|
|
262
|
+
def default_format(value)
|
|
263
|
+
if value.is_a?(Float)
|
|
264
|
+
value == value.to_i ? value.to_i.to_s : value.to_s
|
|
265
|
+
else
|
|
266
|
+
value.to_s
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def split_sections(code)
|
|
271
|
+
sections = []
|
|
272
|
+
current = String.new
|
|
273
|
+
in_quotes = false
|
|
274
|
+
code.each_char do |ch|
|
|
275
|
+
if ch == "\""
|
|
276
|
+
in_quotes = !in_quotes
|
|
277
|
+
current << ch
|
|
278
|
+
elsif ch == ";" && !in_quotes
|
|
279
|
+
sections << current
|
|
280
|
+
current = String.new
|
|
281
|
+
else
|
|
282
|
+
current << ch
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
sections << current
|
|
286
|
+
sections
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def format_number(value, code)
|
|
290
|
+
sections = split_sections(code)
|
|
291
|
+
if sections.size == 1
|
|
292
|
+
pattern = sections[0]
|
|
293
|
+
is_negative = value.negative?
|
|
294
|
+
num = value.abs
|
|
295
|
+
elsif sections.size >= 2
|
|
296
|
+
if value.negative?
|
|
297
|
+
pattern = sections[1]
|
|
298
|
+
is_negative = false
|
|
299
|
+
num = value.abs
|
|
300
|
+
elsif value.zero? && sections.size >= 3
|
|
301
|
+
pattern = sections[2]
|
|
302
|
+
is_negative = false
|
|
303
|
+
num = 0
|
|
304
|
+
else
|
|
305
|
+
pattern = sections[0]
|
|
306
|
+
is_negative = false
|
|
307
|
+
num = value
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
return (is_negative ? "-#{num}" : num.to_s) if pattern == "@"
|
|
312
|
+
|
|
313
|
+
format_single_number(num, pattern, is_negative)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def format_single_number(num, pattern, is_neg)
|
|
317
|
+
clean = pattern.strip
|
|
318
|
+
prefix = ""
|
|
319
|
+
suffix = ""
|
|
320
|
+
|
|
321
|
+
if clean =~ /\A\\-/ || clean =~ /\A-/
|
|
322
|
+
prefix = "-"
|
|
323
|
+
clean = clean.sub(/\A\\-/, "").sub(/\A-/, "")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
clean = clean.gsub(/_[-_()€$]/, "").gsub(/\* */, "").gsub("\\ ", "").gsub(/"[^"]*"/, "").strip if clean.include?("_-") || clean.include?("*-") || clean.include?("* ")
|
|
327
|
+
|
|
328
|
+
case clean
|
|
329
|
+
when /\A(\[Red\])?\((.*)\)\z/
|
|
330
|
+
prefix = "#{::Regexp.last_match(1)}("
|
|
331
|
+
suffix = ")"
|
|
332
|
+
clean = ::Regexp.last_match(2)
|
|
333
|
+
when /\A"([^"]+)"(.*)\z/, /\A\[\$([^\]-]+)[^\]]*\]\s*(.*)\z/, /\A([$€£¥])(.*)\z/
|
|
334
|
+
prefix = ::Regexp.last_match(1)
|
|
335
|
+
clean = ::Regexp.last_match(2)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
if clean =~ /0\.0+E\+00/i
|
|
339
|
+
dec = clean[/0\.(0+)E/i, 1].size
|
|
340
|
+
formatted = Kernel.format("%.#{dec}E", num)
|
|
341
|
+
return "#{prefix}#{formatted}#{suffix}"
|
|
342
|
+
elsif clean =~ /##0\.0+E\+0/i
|
|
343
|
+
dec = clean[/##0\.(0+)E/i, 1].size
|
|
344
|
+
formatted = Kernel.format("%.#{dec}E", num)
|
|
345
|
+
return "#{prefix}#{formatted}#{suffix}"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
if clean.end_with?("%")
|
|
349
|
+
clean = clean.sub(/%+\z/, "")
|
|
350
|
+
suffix = "%#{suffix}"
|
|
351
|
+
num = num.to_f * 100
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
if clean =~ /\A(0+)\z/
|
|
355
|
+
zeros = ::Regexp.last_match(1).size
|
|
356
|
+
formatted = Kernel.format("%0#{zeros}d", num.round)
|
|
357
|
+
return "#{"-" if is_neg}#{prefix}#{formatted}#{suffix}"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
decimals = 0
|
|
361
|
+
decimals = ::Regexp.last_match(1).size if clean =~ /\.(0+)/
|
|
362
|
+
|
|
363
|
+
has_commas = clean.include?(",")
|
|
364
|
+
|
|
365
|
+
str = Kernel.format("%.#{decimals}f", num)
|
|
366
|
+
if has_commas
|
|
367
|
+
parts = str.split(".")
|
|
368
|
+
parts[0] = parts[0].reverse.gsub(/(\d{3})(?=\d)/, "\\1,").reverse
|
|
369
|
+
str = parts.join(".")
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
"#{"-" if is_neg}#{prefix}#{str}#{suffix}"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def format_datetime(datetime, format_code)
|
|
376
|
+
strftime_pattern = excel_to_strftime(format_code)
|
|
377
|
+
datetime.strftime(strftime_pattern)
|
|
378
|
+
rescue StandardError
|
|
379
|
+
datetime.strftime("%Y-%m-%d %H:%M:%S")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def excel_to_strftime(format_code)
|
|
383
|
+
code = format_code.dup
|
|
384
|
+
|
|
385
|
+
code.gsub!(%r{am/pm}i, "%p")
|
|
386
|
+
code.gsub!(%r{a/p}i, "%p")
|
|
387
|
+
|
|
388
|
+
code.gsub!(/\[h+\]/i, "[%-k]")
|
|
389
|
+
|
|
390
|
+
code.gsub!(/(h+:)(mm)/i, "\\1%M")
|
|
391
|
+
code.gsub!(/(h+:)(m)(?!%)/i, "\\1%-M")
|
|
392
|
+
code.gsub!(/(mm)(:s+)/i, "%M\\2")
|
|
393
|
+
code.gsub!(/(?<!%)(m)(:s+)/i, "%-M\\2")
|
|
394
|
+
|
|
395
|
+
code.gsub!(/ss/i, "%S")
|
|
396
|
+
code.gsub!(/(?<!%)s/i, "%-S")
|
|
397
|
+
|
|
398
|
+
code.gsub!("000", "%3N")
|
|
399
|
+
code.gsub!("00", "%2N")
|
|
400
|
+
code.gsub!("0", "%1N")
|
|
401
|
+
|
|
402
|
+
code.gsub!(/mmmm/i, "%B")
|
|
403
|
+
code.gsub!(/mmm/i, "%^b")
|
|
404
|
+
code.gsub!(/mm/i, "%m")
|
|
405
|
+
code.gsub!(/(?<!%)(?<![a-zA-Z])m(?![a-zA-Z])/i, "%-m")
|
|
406
|
+
|
|
407
|
+
if code.include?("%p")
|
|
408
|
+
code.gsub!(/hh/i, "%I")
|
|
409
|
+
code.gsub!(/(?<!%)h/i, "%-l")
|
|
410
|
+
else
|
|
411
|
+
code.gsub!(/hh/i, "%H")
|
|
412
|
+
code.gsub!(/(?<!%)h/i, "%-k")
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
code.gsub!(/dddd/i, "%A")
|
|
416
|
+
code.gsub!(/ddd/i, "%^a")
|
|
417
|
+
code.gsub!(/dd/i, "%d")
|
|
418
|
+
code.gsub!(/(?<!%)d/i, "%-d")
|
|
419
|
+
|
|
420
|
+
code.gsub!(/yyyy/i, "%Y")
|
|
421
|
+
code.gsub!(/yy/i, "%y")
|
|
422
|
+
|
|
423
|
+
code.gsub!(/\\(.)/, "\\1")
|
|
424
|
+
|
|
425
|
+
code
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
data/lib/xlsxrb/ooxml/cfb.rb
CHANGED
|
@@ -292,11 +292,11 @@ module Xlsxrb
|
|
|
292
292
|
|
|
293
293
|
attrs = {}
|
|
294
294
|
ht = attributes["ht"]
|
|
295
|
-
attrs[:height] = ht.to_f if ht && attributes["customHeight"]
|
|
296
|
-
attrs[:hidden] = true if attributes["hidden"]
|
|
295
|
+
attrs[:height] = ht.to_f if ht && %w[1 true].include?(attributes["customHeight"])
|
|
296
|
+
attrs[:hidden] = true if %w[1 true].include?(attributes["hidden"])
|
|
297
297
|
ol = attributes["outlineLevel"]
|
|
298
298
|
attrs[:outline_level] = ol.to_i if ol && ol != "0"
|
|
299
|
-
attrs[:collapsed] = true if attributes["collapsed"]
|
|
299
|
+
attrs[:collapsed] = true if %w[1 true].include?(attributes["collapsed"])
|
|
300
300
|
s = attributes["s"]
|
|
301
301
|
attrs[:style] = s.to_i if s
|
|
302
302
|
attrs[:thick_top] = true if attributes["thickTop"] == "1"
|
|
@@ -401,7 +401,8 @@ module Xlsxrb
|
|
|
401
401
|
when "workbook"
|
|
402
402
|
@conformance = attributes["conformance"] if attributes["conformance"]
|
|
403
403
|
when "sheet"
|
|
404
|
-
|
|
404
|
+
rid = attributes["r:id"] || attributes["id"] || attributes.find { |k, _| k.end_with?(":id") }&.last
|
|
405
|
+
@sheets << { name: attributes["name"], rid: rid, state: attributes["state"] }
|
|
405
406
|
when "fileVersion"
|
|
406
407
|
an = attributes["appName"]
|
|
407
408
|
@file_version[:app_name] = an if an
|
|
@@ -736,7 +737,8 @@ module Xlsxrb
|
|
|
736
737
|
return unless ref
|
|
737
738
|
|
|
738
739
|
link = { ref: ref }
|
|
739
|
-
|
|
740
|
+
rid = attributes["r:id"] || attributes["id"] || attributes.find { |k, _| k.end_with?(":id") }&.last
|
|
741
|
+
link[:rid] = rid if rid
|
|
740
742
|
link[:display] = attributes["display"] if attributes["display"]
|
|
741
743
|
link[:tooltip] = attributes["tooltip"] if attributes["tooltip"]
|
|
742
744
|
link[:location] = attributes["location"] if attributes["location"]
|
|
@@ -51,7 +51,7 @@ module Xlsxrb
|
|
|
51
51
|
@current_image[:hidden] = %w[1 true].include?(attributes["hidden"]) if attributes["hidden"]
|
|
52
52
|
end
|
|
53
53
|
when "blip"
|
|
54
|
-
rid = attributes["r:embed"] || attributes["embed"]
|
|
54
|
+
rid = attributes["r:embed"] || attributes["embed"] || attributes.find { |k, _| k.end_with?(":embed") }&.last
|
|
55
55
|
@current_image[:embed_rid] = rid if @inside_pic && @current_image && rid
|
|
56
56
|
when "alphaModFix"
|
|
57
57
|
@current_image[:alpha_mod_fix] = attributes["amt"].to_i if @inside_pic && @current_image && attributes["amt"]
|
|
@@ -87,11 +87,14 @@ module Xlsxrb
|
|
|
87
87
|
when "to"
|
|
88
88
|
@inside_to = true if @inside_anchor
|
|
89
89
|
when "ext"
|
|
90
|
+
cx = attributes["cx"]
|
|
91
|
+
cy = attributes["cy"]
|
|
90
92
|
if @inside_pic && @current_image
|
|
91
|
-
cx = attributes["cx"]
|
|
92
|
-
cy = attributes["cy"]
|
|
93
93
|
@current_image[:cx] = cx.to_i if cx
|
|
94
94
|
@current_image[:cy] = cy.to_i if cy
|
|
95
|
+
elsif @inside_anchor
|
|
96
|
+
@anchor_cx = cx.to_i if cx
|
|
97
|
+
@anchor_cy = cy.to_i if cy
|
|
95
98
|
end
|
|
96
99
|
when "col", "colOff", "row", "rowOff"
|
|
97
100
|
@current_field = name
|
|
@@ -116,6 +119,8 @@ module Xlsxrb
|
|
|
116
119
|
@anchor_from.each { |k, v| @current_image[:"from_#{k}"] = v }
|
|
117
120
|
@anchor_to.each { |k, v| @current_image[:"to_#{k}"] = v }
|
|
118
121
|
@current_image[:edit_as] = @anchor_edit_as if @anchor_edit_as
|
|
122
|
+
@current_image[:cx] ||= @anchor_cx if @anchor_cx
|
|
123
|
+
@current_image[:cy] ||= @anchor_cy if @anchor_cy
|
|
119
124
|
@images << @current_image
|
|
120
125
|
end
|
|
121
126
|
@current_image = nil
|
|
@@ -127,6 +132,8 @@ module Xlsxrb
|
|
|
127
132
|
@inside_anchor = false
|
|
128
133
|
@anchor_from = {}
|
|
129
134
|
@anchor_to = {}
|
|
135
|
+
@anchor_cx = nil
|
|
136
|
+
@anchor_cy = nil
|
|
130
137
|
@anchor_locks_with_sheet = nil
|
|
131
138
|
@anchor_prints_with_sheet = nil
|
|
132
139
|
@anchor_published = false
|
|
@@ -203,7 +210,7 @@ module Xlsxrb
|
|
|
203
210
|
@current_chart[:frame_hidden] = %w[1 true].include?(attributes["hidden"]) if attributes["hidden"]
|
|
204
211
|
end
|
|
205
212
|
when "chart"
|
|
206
|
-
rid = attributes["r:id"] || attributes["id"]
|
|
213
|
+
rid = attributes["r:id"] || attributes["id"] || attributes.find { |k, _| k.end_with?(":id") }&.last
|
|
207
214
|
@current_chart[:rid] = rid if @inside_graphic_frame && @current_chart && rid
|
|
208
215
|
when "graphicFrameLocks"
|
|
209
216
|
@current_chart[:frame_no_grp] = true if @inside_graphic_frame && @current_chart && %w[1 true].include?(attributes["noGrp"])
|
|
@@ -204,7 +204,7 @@ module Xlsxrb
|
|
|
204
204
|
def start_element(_uri, local_name, qname, _attributes)
|
|
205
205
|
name = element_name(local_name, qname)
|
|
206
206
|
case name
|
|
207
|
-
when "Application", "AppVersion"
|
|
207
|
+
when "Application", "AppVersion", "Company", "Manager"
|
|
208
208
|
@current_field = name
|
|
209
209
|
@text_buffer = +""
|
|
210
210
|
when "HeadingPairs"
|
|
@@ -232,6 +232,12 @@ module Xlsxrb
|
|
|
232
232
|
when "AppVersion"
|
|
233
233
|
@properties[:app_version] = @text_buffer.dup
|
|
234
234
|
@current_field = nil
|
|
235
|
+
when "Company"
|
|
236
|
+
@properties[:company] = @text_buffer.dup
|
|
237
|
+
@current_field = nil
|
|
238
|
+
when "Manager"
|
|
239
|
+
@properties[:manager] = @text_buffer.dup
|
|
240
|
+
@current_field = nil
|
|
235
241
|
when "lpstr"
|
|
236
242
|
if @inside_titles_of_parts
|
|
237
243
|
@titles_of_parts << @text_buffer.dup
|
data/lib/xlsxrb/ooxml/reader.rb
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
require "zlib"
|
|
6
6
|
require "rexml/parsers/sax2parser"
|
|
7
7
|
require "rexml/sax2listener"
|
|
8
|
+
require_relative "../number_formatter"
|
|
8
9
|
|
|
9
10
|
module Xlsxrb
|
|
10
11
|
module Ooxml
|
|
@@ -89,18 +90,7 @@ module Xlsxrb
|
|
|
89
90
|
return {} if worksheet_xml.nil? || worksheet_xml.empty?
|
|
90
91
|
|
|
91
92
|
# Parse hyperlink elements from worksheet.
|
|
92
|
-
links =
|
|
93
|
-
WorksheetParser.each_event(worksheet_xml, part_name: entry_path) do |event|
|
|
94
|
-
next unless event.type == :hyperlink
|
|
95
|
-
|
|
96
|
-
ref, rid, display, tooltip, location = event.args
|
|
97
|
-
link = { ref: ref }
|
|
98
|
-
link[:rid] = rid if rid
|
|
99
|
-
link[:display] = display if display
|
|
100
|
-
link[:tooltip] = tooltip if tooltip
|
|
101
|
-
link[:location] = location if location
|
|
102
|
-
links << link
|
|
103
|
-
end
|
|
93
|
+
links = WorksheetParser.parse_hyperlinks(worksheet_xml)
|
|
104
94
|
|
|
105
95
|
# Parse rels to resolve rId -> URL.
|
|
106
96
|
rels_path = entry_path.sub(%r{([^/]+)$}, '_rels/\1.rels')
|
|
@@ -554,6 +544,14 @@ module Xlsxrb
|
|
|
554
544
|
parse_workbook_metadata[:workbook_properties]
|
|
555
545
|
end
|
|
556
546
|
|
|
547
|
+
# Returns whether the workbook uses the 1904 date system.
|
|
548
|
+
#
|
|
549
|
+
# @return [Boolean]
|
|
550
|
+
#: () -> bool
|
|
551
|
+
def date1904?
|
|
552
|
+
workbook_properties&.[](:date1904) ? true : false
|
|
553
|
+
end
|
|
554
|
+
|
|
557
555
|
# Returns the workbook conformance class ("transitional" or "strict"), or nil if not set.
|
|
558
556
|
def conformance
|
|
559
557
|
parse_workbook_metadata[:conformance]
|
|
@@ -1227,6 +1225,7 @@ module Xlsxrb
|
|
|
1227
1225
|
end
|
|
1228
1226
|
|
|
1229
1227
|
def resolve_date_cells(raw_cells, cell_style_map, styles)
|
|
1228
|
+
is_date1904 = workbook_properties&.[](:date1904) || false
|
|
1230
1229
|
raw_cells.each do |cell_ref, value|
|
|
1231
1230
|
next unless value.is_a?(Numeric)
|
|
1232
1231
|
|
|
@@ -1240,9 +1239,9 @@ module Xlsxrb
|
|
|
1240
1239
|
next unless date_format?(fmt_id, styles[:num_fmts])
|
|
1241
1240
|
|
|
1242
1241
|
raw_cells[cell_ref] = if value.is_a?(Float) && (value % 1).positive?
|
|
1243
|
-
Xlsxrb::Ooxml::Utils.serial_to_datetime(value)
|
|
1242
|
+
Xlsxrb::Ooxml::Utils.serial_to_datetime(value, date1904: is_date1904)
|
|
1244
1243
|
else
|
|
1245
|
-
Xlsxrb::Ooxml::Utils.serial_to_date(value.to_i)
|
|
1244
|
+
Xlsxrb::Ooxml::Utils.serial_to_date(value.to_i, date1904: is_date1904)
|
|
1246
1245
|
end
|
|
1247
1246
|
end
|
|
1248
1247
|
raw_cells
|
|
@@ -1282,9 +1281,7 @@ module Xlsxrb
|
|
|
1282
1281
|
end
|
|
1283
1282
|
|
|
1284
1283
|
def date_pattern?(code)
|
|
1285
|
-
|
|
1286
|
-
stripped = code.gsub(/"[^"]*"/, "").gsub(/\\[.]/, "")
|
|
1287
|
-
stripped.match?(/[ymdhsYMDHS]/)
|
|
1284
|
+
NumberFormatter.date_format?(code)
|
|
1288
1285
|
end
|
|
1289
1286
|
|
|
1290
1287
|
def extract_zip_entry(entry_name)
|
|
@@ -101,7 +101,7 @@ module Xlsxrb
|
|
|
101
101
|
str = ""
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
yield str.freeze
|
|
104
|
+
yield normalize_newlines(str).freeze
|
|
105
105
|
pos = si_end + 5
|
|
106
106
|
end
|
|
107
107
|
end
|
|
@@ -134,6 +134,12 @@ module Xlsxrb
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
private_class_method :extract_multi_t
|
|
137
|
+
|
|
138
|
+
def self.normalize_newlines(str)
|
|
139
|
+
str.include?("\r") ? str.gsub("\r\n", "\n").tr("\r", "\n") : str
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private_class_method :normalize_newlines
|
|
137
143
|
end
|
|
138
144
|
end
|
|
139
145
|
end
|