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,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"])
|
data/lib/xlsxrb/ooxml/reader.rb
CHANGED
|
@@ -89,18 +89,7 @@ module Xlsxrb
|
|
|
89
89
|
return {} if worksheet_xml.nil? || worksheet_xml.empty?
|
|
90
90
|
|
|
91
91
|
# 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
|
|
92
|
+
links = WorksheetParser.parse_hyperlinks(worksheet_xml)
|
|
104
93
|
|
|
105
94
|
# Parse rels to resolve rId -> URL.
|
|
106
95
|
rels_path = entry_path.sub(%r{([^/]+)$}, '_rels/\1.rels')
|
|
@@ -554,6 +543,14 @@ module Xlsxrb
|
|
|
554
543
|
parse_workbook_metadata[:workbook_properties]
|
|
555
544
|
end
|
|
556
545
|
|
|
546
|
+
# Returns whether the workbook uses the 1904 date system.
|
|
547
|
+
#
|
|
548
|
+
# @return [Boolean]
|
|
549
|
+
#: () -> bool
|
|
550
|
+
def date1904?
|
|
551
|
+
workbook_properties&.[](:date1904) ? true : false
|
|
552
|
+
end
|
|
553
|
+
|
|
557
554
|
# Returns the workbook conformance class ("transitional" or "strict"), or nil if not set.
|
|
558
555
|
def conformance
|
|
559
556
|
parse_workbook_metadata[:conformance]
|
|
@@ -1227,6 +1224,7 @@ module Xlsxrb
|
|
|
1227
1224
|
end
|
|
1228
1225
|
|
|
1229
1226
|
def resolve_date_cells(raw_cells, cell_style_map, styles)
|
|
1227
|
+
is_date1904 = workbook_properties&.[](:date1904) || false
|
|
1230
1228
|
raw_cells.each do |cell_ref, value|
|
|
1231
1229
|
next unless value.is_a?(Numeric)
|
|
1232
1230
|
|
|
@@ -1240,9 +1238,9 @@ module Xlsxrb
|
|
|
1240
1238
|
next unless date_format?(fmt_id, styles[:num_fmts])
|
|
1241
1239
|
|
|
1242
1240
|
raw_cells[cell_ref] = if value.is_a?(Float) && (value % 1).positive?
|
|
1243
|
-
Xlsxrb::Ooxml::Utils.serial_to_datetime(value)
|
|
1241
|
+
Xlsxrb::Ooxml::Utils.serial_to_datetime(value, date1904: is_date1904)
|
|
1244
1242
|
else
|
|
1245
|
-
Xlsxrb::Ooxml::Utils.serial_to_date(value.to_i)
|
|
1243
|
+
Xlsxrb::Ooxml::Utils.serial_to_date(value.to_i, date1904: is_date1904)
|
|
1246
1244
|
end
|
|
1247
1245
|
end
|
|
1248
1246
|
raw_cells
|
|
@@ -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
|