calcsheet 0.1.0 → 0.2.0
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 +42 -1
- data/README.md +124 -9
- data/lib/calcsheet/book.rb +25 -9
- data/lib/calcsheet/cell_range.rb +54 -0
- data/lib/calcsheet/configuration.rb +10 -0
- data/lib/calcsheet/display_format.rb +75 -0
- data/lib/calcsheet/errors.rb +5 -0
- data/lib/calcsheet/formula.rb +40 -0
- data/lib/calcsheet/limited_inflater.rb +16 -0
- data/lib/calcsheet/native_backends.rb +139 -0
- data/lib/calcsheet/option_validation.rb +4 -1
- data/lib/calcsheet/raw_row.rb +1 -1
- data/lib/calcsheet/readers/csv_reader.rb +43 -20
- data/lib/calcsheet/readers/csv_records.rb +193 -0
- data/lib/calcsheet/readers/ods/content_parser.rb +49 -69
- data/lib/calcsheet/readers/ods/scanner.rb +33 -6
- data/lib/calcsheet/readers/ods_reader.rb +16 -1
- data/lib/calcsheet/readers/sax_sheet.rb +6 -48
- data/lib/calcsheet/readers/xlsx/sheet_parser.rb +31 -64
- data/lib/calcsheet/reading.rb +29 -7
- data/lib/calcsheet/row.rb +2 -2
- data/lib/calcsheet/row_builder.rb +235 -0
- data/lib/calcsheet/sheet_list.rb +25 -0
- data/lib/calcsheet/sheet_selection.rb +3 -2
- data/lib/calcsheet/style.rb +114 -0
- data/lib/calcsheet/version.rb +1 -1
- data/lib/calcsheet/write_book.rb +49 -17
- data/lib/calcsheet/write_sheet.rb +220 -18
- data/lib/calcsheet/writers/csv_writer.rb +18 -7
- data/lib/calcsheet/writers/ods_writer.rb +249 -16
- data/lib/calcsheet/writers/open_formula.rb +67 -0
- data/lib/calcsheet/writers/xlsx_writer.rb +226 -37
- data/lib/calcsheet/writers/zip_buffered.rb +20 -1
- data/lib/calcsheet.rb +31 -8
- metadata +11 -2
|
@@ -8,7 +8,9 @@ require_relative "../column_letter"
|
|
|
8
8
|
require_relative "../decimal_text"
|
|
9
9
|
require_relative "../errors"
|
|
10
10
|
require_relative "../excel_serial"
|
|
11
|
+
require_relative "../formula"
|
|
11
12
|
require_relative "../grid"
|
|
13
|
+
require_relative "../style"
|
|
12
14
|
require_relative "xml_text"
|
|
13
15
|
require_relative "zip_buffered"
|
|
14
16
|
|
|
@@ -20,9 +22,10 @@ module Calcsheet
|
|
|
20
22
|
# "+39..." and "=..." stay verbatim. The parts that list every sheet (workbook, relationships, content
|
|
21
23
|
# types) and the styles, known only at the end, are written after the sheets; zip entries have no required order.
|
|
22
24
|
#
|
|
23
|
-
# Styles: the default, the bold header,
|
|
24
|
-
# full value is stored, the format shows +scale+ decimals), dates and datetimes
|
|
25
|
-
# the
|
|
25
|
+
# Styles: the default, the bold header, one per number format a column needs: decimals with +scale+ (the
|
|
26
|
+
# full value is stored, the format shows +scale+ decimals), dates and datetimes, and one per Style and number
|
|
27
|
+
# format the rows use (fonts, fills and borders shared between them). They are numbered in the order they are
|
|
28
|
+
# first used. A formula is written without a cached value, and the workbook asks to be computed when it opens.
|
|
26
29
|
#
|
|
27
30
|
# @api private
|
|
28
31
|
class XlsxWriter
|
|
@@ -65,30 +68,45 @@ module Calcsheet
|
|
|
65
68
|
@styles = {}
|
|
66
69
|
@cell_formats = []
|
|
67
70
|
@number_formats = {}
|
|
71
|
+
@fonts = { [ nil, nil, nil, nil ] => 0, [ true, nil, nil, nil ] => 1 }
|
|
72
|
+
@fills = { nil => 0, gray125: 1 }
|
|
73
|
+
@borders = { nil => 0 }
|
|
74
|
+
@letters_cache = {}
|
|
75
|
+
@formulas = false
|
|
68
76
|
@buffer = new_buffer
|
|
69
77
|
@entry = nil
|
|
78
|
+
@deferred = nil
|
|
70
79
|
end
|
|
71
80
|
|
|
72
|
-
# Opens the sheet's zip entry and writes its start, column widths and header row
|
|
81
|
+
# Opens the sheet's zip entry and writes its start, column widths and header row; with +auto_width+, the rows
|
|
82
|
+
# go to a temporary file and the entry is written when the sheet ends, its widths known.
|
|
73
83
|
# @raise [ArgumentError] for more columns than XLSX holds
|
|
74
84
|
# @raise [WriteError] for a header longer than a cell holds
|
|
75
|
-
def start_sheet(name, columns, header:)
|
|
76
|
-
check_sheet(name, columns)
|
|
85
|
+
def start_sheet(name, columns, header:, widths: nil, auto_width: false)
|
|
86
|
+
check_sheet(name, columns) if columns
|
|
77
87
|
@sheet_names << name
|
|
78
88
|
@name = name
|
|
79
89
|
@columns = columns
|
|
80
|
-
@types = columns
|
|
90
|
+
@types = columns&.map(&:type)
|
|
91
|
+
@header = header
|
|
81
92
|
@row_number = 0
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
@merges = []
|
|
94
|
+
prepare_cells(columns) if columns
|
|
95
|
+
if auto_width
|
|
96
|
+
defer_rows
|
|
97
|
+
else
|
|
98
|
+
open_sheet_entry(cols(columns ? columns.map(&:width) : widths || []))
|
|
99
|
+
end
|
|
85
100
|
write_header(columns) if header
|
|
86
101
|
end
|
|
87
102
|
|
|
88
|
-
# Writes one row of checked values (see the contract in WriteBook)
|
|
103
|
+
# Writes one row of checked values (see the contract in WriteBook): a typed row without styles or formulas from
|
|
104
|
+
# the constant parts of #prepare_cells, any other cell by cell.
|
|
89
105
|
# @raise [WriteError] for a date beyond the serials or a text longer than a cell; nothing of the row is written
|
|
90
|
-
def row(values)
|
|
106
|
+
def row(values, styles = nil)
|
|
91
107
|
mark = @buffer.bytesize
|
|
108
|
+
return any_row(values, styles) if styles || @types.nil? || values.any?(Formula)
|
|
109
|
+
|
|
92
110
|
number = (@row_number + 1).to_s
|
|
93
111
|
@buffer << '<row r="' << number << '">'
|
|
94
112
|
index = 0
|
|
@@ -110,10 +128,25 @@ module Calcsheet
|
|
|
110
128
|
raise
|
|
111
129
|
end
|
|
112
130
|
|
|
113
|
-
#
|
|
114
|
-
def
|
|
115
|
-
@
|
|
116
|
-
|
|
131
|
+
# @param range [CellRange] written after the rows, as XLSX keeps them
|
|
132
|
+
def merge(range)
|
|
133
|
+
@merges << range
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Ends the sheet's XML and closes its zip entry; with +auto_width+, writes the entry with +widths+ first.
|
|
137
|
+
def end_sheet(widths = nil)
|
|
138
|
+
@buffer << "</sheetData>" << merge_cells << "</worksheet>"
|
|
139
|
+
if @deferred
|
|
140
|
+
tail = @buffer.dup
|
|
141
|
+
@buffer.clear
|
|
142
|
+
flush
|
|
143
|
+
open_sheet_entry(cols(widths || []))
|
|
144
|
+
flush
|
|
145
|
+
replay_deferred
|
|
146
|
+
@entry << tail
|
|
147
|
+
else
|
|
148
|
+
flush
|
|
149
|
+
end
|
|
117
150
|
@entry.close
|
|
118
151
|
@entry = nil
|
|
119
152
|
end
|
|
@@ -137,7 +170,7 @@ module Calcsheet
|
|
|
137
170
|
next if value.nil?
|
|
138
171
|
|
|
139
172
|
reason =
|
|
140
|
-
case
|
|
173
|
+
case type_of(value, index)
|
|
141
174
|
# A text of at most 32,767 bytes has at most as many UTF-16 code units (a UTF-8 character takes at least as
|
|
142
175
|
# many bytes as it takes units): only a longer one is counted.
|
|
143
176
|
when :string then length_reason(excel_length(value)) if value.bytesize > Grid::MAX_CELL_CHARACTERS && excel_length(value) > Grid::MAX_CELL_CHARACTERS
|
|
@@ -197,8 +230,10 @@ module Calcsheet
|
|
|
197
230
|
# +[numFmtId, fontId]+: the header is the General format (0) in the bold font (1, see #styles_xml), a number
|
|
198
231
|
# format its custom format in the default font (0).
|
|
199
232
|
def style(key)
|
|
233
|
+
return 0 if key.nil?
|
|
234
|
+
|
|
200
235
|
@styles[key] ||= begin
|
|
201
|
-
@cell_formats << (key == HEADER_STYLE ? [ 0, 1 ] : [ number_format(key), 0 ])
|
|
236
|
+
@cell_formats << (key == HEADER_STYLE ? [ 0, 1, 0, 0, nil ] : [ number_format(key), 0, 0, 0, nil ])
|
|
202
237
|
@cell_formats.size
|
|
203
238
|
end
|
|
204
239
|
end
|
|
@@ -207,30 +242,151 @@ module Calcsheet
|
|
|
207
242
|
@number_formats[code] ||= FIRST_CUSTOM_FORMAT + @number_formats.size
|
|
208
243
|
end
|
|
209
244
|
|
|
210
|
-
|
|
245
|
+
# Opens the sheet's zip entry with its start, up to the rows.
|
|
246
|
+
def open_sheet_entry(cols_xml)
|
|
247
|
+
@entry = @zip.write_deflated_file("xl/worksheets/sheet#{@sheet_names.size}.xml")
|
|
248
|
+
@entry << sheet_start(cols_xml, @header)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def sheet_start(cols_xml, header)
|
|
211
252
|
xml = +DECLARATION
|
|
212
253
|
xml << %(<worksheet xmlns="#{MAIN_NS}" xmlns:r="#{REL_NS}"><sheetViews><sheetView)
|
|
213
254
|
xml << ' tabSelected="1"' if @sheet_names.size == 1
|
|
214
255
|
xml << ' workbookViewId="0"'
|
|
215
256
|
xml << (header ? ">#{FROZEN_HEADER}</sheetView>" : "/>")
|
|
216
257
|
xml << "</sheetViews>"
|
|
217
|
-
xml <<
|
|
258
|
+
xml << cols_xml
|
|
218
259
|
xml << "<sheetData>"
|
|
219
260
|
end
|
|
220
261
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
262
|
+
# @param widths [Array<Numeric, nil>] by column index
|
|
263
|
+
def cols(widths)
|
|
264
|
+
cols = widths.each_with_index.filter_map do |width, index|
|
|
265
|
+
next if width.nil?
|
|
224
266
|
|
|
225
|
-
%(<col min="#{index + 1}" max="#{index + 1}" width="#{number_text(
|
|
267
|
+
%(<col min="#{index + 1}" max="#{index + 1}" width="#{number_text(width)}" customWidth="1"/>)
|
|
226
268
|
end
|
|
227
269
|
cols.empty? ? "" : "<cols>#{cols.join}</cols>"
|
|
228
270
|
end
|
|
229
271
|
|
|
272
|
+
def merge_cells
|
|
273
|
+
return "" if @merges.empty?
|
|
274
|
+
|
|
275
|
+
%(<mergeCells count="#{@merges.size}">#{@merges.map { %(<mergeCell ref="#{_1}"/>) }.join}</mergeCells>)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# A row of any sheet, cell by cell: the type of each value is its column's, or its class's in a free sheet,
|
|
279
|
+
# a formula's whatever the column; a styled nil is a styled empty cell.
|
|
280
|
+
def any_row(values, styles)
|
|
281
|
+
mark = @buffer.bytesize
|
|
282
|
+
number = (@row_number + 1).to_s
|
|
283
|
+
@buffer << '<row r="' << number << '">'
|
|
284
|
+
count = [ values.size, styles&.size || 0 ].max
|
|
285
|
+
index = 0
|
|
286
|
+
while index < count
|
|
287
|
+
value = values[index]
|
|
288
|
+
style = styles&.[](index)
|
|
289
|
+
if value.nil?
|
|
290
|
+
@buffer << '<c r="' << letter(index) << number << '" s="' << xf(style, nil).to_s << '"/>' if style
|
|
291
|
+
else
|
|
292
|
+
any_cell(value, index, number, style)
|
|
293
|
+
end
|
|
294
|
+
index += 1
|
|
295
|
+
end
|
|
296
|
+
@buffer << "</row>"
|
|
297
|
+
@row_number += 1
|
|
298
|
+
flush if @buffer.bytesize >= BUFFER_SIZE
|
|
299
|
+
rescue StandardError
|
|
300
|
+
rollback(mark)
|
|
301
|
+
raise
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def any_cell(value, index, number, style)
|
|
305
|
+
type = type_of(value, index)
|
|
306
|
+
code = style&.format&.code || default_format(type, index)
|
|
307
|
+
@buffer << '<c r="' << letter(index) << number << '"'
|
|
308
|
+
@buffer << ' s="' << xf(style, code).to_s << '"' if style || code
|
|
309
|
+
case type
|
|
310
|
+
when :formula
|
|
311
|
+
@formulas = true
|
|
312
|
+
@buffer << "><f>" << XmlText.escape(value.text) << "</f></c>"
|
|
313
|
+
when :string
|
|
314
|
+
@buffer << ' t="inlineStr"><is><t xml:space="preserve">'
|
|
315
|
+
value_text(value, :string, index)
|
|
316
|
+
@buffer << "</t></is></c>"
|
|
317
|
+
when :boolean then @buffer << ' t="b"><v>' << (value ? "1" : "0") << "</v></c>"
|
|
318
|
+
else
|
|
319
|
+
@buffer << "><v>"
|
|
320
|
+
value_text(value, type, index)
|
|
321
|
+
@buffer << "</v></c>"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# The type a value is written as: a formula, its column's type, or in a free sheet its class's.
|
|
326
|
+
def type_of(value, index)
|
|
327
|
+
return :formula if value.is_a?(Formula)
|
|
328
|
+
return @types[index] if @types
|
|
329
|
+
|
|
330
|
+
case value
|
|
331
|
+
when String then :string
|
|
332
|
+
when Integer then :integer
|
|
333
|
+
when BigDecimal then :decimal
|
|
334
|
+
when Time then :datetime
|
|
335
|
+
when Date then :date
|
|
336
|
+
else :boolean
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# The number format of a cell without a style that gives one: its column's, or a date's or time's.
|
|
341
|
+
def default_format(type, index)
|
|
342
|
+
return format_of(@columns[index]) if @columns && type != :formula
|
|
343
|
+
|
|
344
|
+
case type
|
|
345
|
+
when :date then DATE_FORMAT
|
|
346
|
+
when :datetime then DATETIME_FORMAT
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def letter(index)
|
|
351
|
+
@letters_cache[index] ||= ColumnLetter.from_index(index).freeze
|
|
352
|
+
end
|
|
353
|
+
|
|
230
354
|
def number_text(number)
|
|
231
355
|
number.is_a?(Integer) ? number.to_s : Float(number).to_s
|
|
232
356
|
end
|
|
233
357
|
|
|
358
|
+
# The cellXfs index of a Style with a number format code (either may be nil), registered at its first use; a
|
|
359
|
+
# Style that sets only the number format shares the index of that format.
|
|
360
|
+
def xf(style, code)
|
|
361
|
+
return style(code) if style.nil? || style.plain?
|
|
362
|
+
|
|
363
|
+
@styles[[ style, code ]] ||= begin
|
|
364
|
+
@cell_formats << [ code ? number_format(code) : 0, font(style), fill(style), border(style), alignment(style) ]
|
|
365
|
+
@cell_formats.size
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def font(style)
|
|
370
|
+
key = [ style.bold || nil, style.italic || nil, style.size, style.color ]
|
|
371
|
+
@fonts[key] ||= @fonts.size
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def fill(style)
|
|
375
|
+
@fills[style.fill] ||= @fills.size
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def border(style)
|
|
379
|
+
@borders[style.border] ||= @borders.size
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def alignment(style)
|
|
383
|
+
attributes = []
|
|
384
|
+
attributes << %(horizontal="#{style.align}") if style.align
|
|
385
|
+
attributes << %(vertical="#{style.valign}") if style.valign
|
|
386
|
+
attributes << 'wrapText="1"' if style.wrap
|
|
387
|
+
attributes.empty? ? nil : "<alignment #{attributes.join(" ")}/>"
|
|
388
|
+
end
|
|
389
|
+
|
|
234
390
|
def write_header(columns)
|
|
235
391
|
style = style_attribute(HEADER_STYLE)
|
|
236
392
|
@buffer << '<row r="1">'
|
|
@@ -243,7 +399,11 @@ module Calcsheet
|
|
|
243
399
|
end
|
|
244
400
|
|
|
245
401
|
def cell(value, index)
|
|
246
|
-
|
|
402
|
+
value_text(value, @types[index], index)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def value_text(value, type, index)
|
|
406
|
+
case type
|
|
247
407
|
when :string
|
|
248
408
|
check_length(value, index) if value.bytesize > Grid::MAX_CELL_CHARACTERS
|
|
249
409
|
@buffer << XmlText.escape(value, ooxml: true)
|
|
@@ -284,7 +444,7 @@ module Calcsheet
|
|
|
284
444
|
# @raise [WriteError] at the row being written
|
|
285
445
|
def refuse(reason, index)
|
|
286
446
|
row = @row_number + 1
|
|
287
|
-
raise WriteError.at(reason, sheet: @name, row: row, column_index: index, header: @columns[index
|
|
447
|
+
raise WriteError.at(reason, sheet: @name, row: row, column_index: index, header: @columns&.[](index)&.header)
|
|
288
448
|
end
|
|
289
449
|
|
|
290
450
|
# The characters of +text+ as Excel counts them: one beyond the Basic Multilingual Plane counts twice.
|
|
@@ -296,29 +456,58 @@ module Calcsheet
|
|
|
296
456
|
|
|
297
457
|
def styles_xml
|
|
298
458
|
formats = @number_formats.map { |code, id| %(<numFmt numFmtId="#{id}" formatCode="#{XmlText.escape(code)}"/>) }
|
|
299
|
-
xfs = [ [ 0, 0 ], *@cell_formats ].map
|
|
300
|
-
attributes = %(numFmtId="#{format}" fontId="#{font}" fillId="0" borderId="0" xfId="0")
|
|
301
|
-
attributes += ' applyNumberFormat="1"' unless format.zero?
|
|
302
|
-
attributes += ' applyFont="1"' unless font.zero?
|
|
303
|
-
"<xf #{attributes}/>"
|
|
304
|
-
end
|
|
305
|
-
font = '<sz val="11"/><name val="Calibri"/><family val="2"/>'
|
|
459
|
+
xfs = [ [ 0, 0, 0, 0, nil ], *@cell_formats ].map { |format, font, fill, border, alignment| xf_xml(format, font, fill, border, alignment) }
|
|
306
460
|
xml = +%(#{DECLARATION}<styleSheet xmlns="#{MAIN_NS}">)
|
|
307
461
|
xml << %(<numFmts count="#{formats.size}">#{formats.join}</numFmts>) unless formats.empty?
|
|
308
|
-
xml << %(<fonts count="
|
|
309
|
-
xml <<
|
|
310
|
-
xml <<
|
|
462
|
+
xml << %(<fonts count="#{@fonts.size}">#{@fonts.keys.map { font_xml(*_1) }.join}</fonts>)
|
|
463
|
+
xml << %(<fills count="#{@fills.size}">#{@fills.keys.map { fill_xml(_1) }.join}</fills>)
|
|
464
|
+
xml << %(<borders count="#{@borders.size}">#{@borders.keys.map { border_xml(_1) }.join}</borders>)
|
|
311
465
|
xml << '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
|
|
312
466
|
xml << %(<cellXfs count="#{xfs.size}">#{xfs.join}</cellXfs>)
|
|
313
467
|
xml << '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>'
|
|
314
468
|
end
|
|
315
469
|
|
|
470
|
+
def xf_xml(format, font, fill, border, alignment)
|
|
471
|
+
attributes = %(numFmtId="#{format}" fontId="#{font}" fillId="#{fill}" borderId="#{border}" xfId="0")
|
|
472
|
+
attributes += ' applyNumberFormat="1"' unless format.zero?
|
|
473
|
+
attributes += ' applyFont="1"' unless font.zero?
|
|
474
|
+
attributes += ' applyFill="1"' unless fill.zero?
|
|
475
|
+
attributes += ' applyBorder="1"' unless border.zero?
|
|
476
|
+
alignment ? %(<xf #{attributes} applyAlignment="1">#{alignment}</xf>) : "<xf #{attributes}/>"
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def font_xml(bold, italic, size, color)
|
|
480
|
+
xml = +"<font>"
|
|
481
|
+
xml << "<b/>" if bold
|
|
482
|
+
xml << "<i/>" if italic
|
|
483
|
+
xml << %(<sz val="#{number_text(size || 11)}"/>)
|
|
484
|
+
xml << %(<color rgb="FF#{color}"/>) if color
|
|
485
|
+
xml << '<name val="Calibri"/><family val="2"/></font>'
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def fill_xml(key)
|
|
489
|
+
case key
|
|
490
|
+
when nil then '<fill><patternFill patternType="none"/></fill>'
|
|
491
|
+
when :gray125 then '<fill><patternFill patternType="gray125"/></fill>'
|
|
492
|
+
else %(<fill><patternFill patternType="solid"><fgColor rgb="FF#{key}"/><bgColor indexed="64"/></patternFill></fill>)
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def border_xml(border)
|
|
497
|
+
edges = %i[left right top bottom].map do |edge|
|
|
498
|
+
next "<#{edge}/>" unless border&.edges&.include?(edge)
|
|
499
|
+
|
|
500
|
+
%(<#{edge} style="#{border.style}"><color rgb="FF#{border.color}"/></#{edge}>)
|
|
501
|
+
end
|
|
502
|
+
"<border>#{edges.join}<diagonal/></border>"
|
|
503
|
+
end
|
|
504
|
+
|
|
316
505
|
def workbook_xml
|
|
317
506
|
sheets = @sheet_names.each_with_index.map do |name, index|
|
|
318
507
|
%(<sheet name="#{XmlText.escape(name)}" sheetId="#{index + 1}" r:id="rId#{index + 1}"/>)
|
|
319
508
|
end
|
|
320
509
|
%(#{DECLARATION}<workbook xmlns="#{MAIN_NS}" xmlns:r="#{REL_NS}"><bookViews><workbookView activeTab="0"/></bookViews>) +
|
|
321
|
-
"<sheets>#{sheets.join}</sheets
|
|
510
|
+
"<sheets>#{sheets.join}</sheets>#{'<calcPr fullCalcOnLoad="1"/>' if @formulas}</workbook>"
|
|
322
511
|
end
|
|
323
512
|
|
|
324
513
|
def workbook_relationships_xml
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "tempfile"
|
|
3
4
|
require_relative "../grid"
|
|
4
5
|
|
|
5
6
|
module Calcsheet
|
|
@@ -36,10 +37,28 @@ module Calcsheet
|
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def flush
|
|
39
|
-
@entry << @buffer
|
|
40
|
+
@deferred ? @deferred.write(@buffer) : @entry << @buffer
|
|
40
41
|
@buffer.clear
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
# From now on the flushed rows go to a temporary file (+auto_width+: what precedes them is known only after
|
|
45
|
+
# them), until #replay_deferred. A Tempfile: one left behind by a book whose block raised is deleted when it is
|
|
46
|
+
# collected.
|
|
47
|
+
def defer_rows
|
|
48
|
+
@deferred = Tempfile.new("calcsheet-rows", binmode: true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Writes the deferred rows to the entry and deletes the file.
|
|
52
|
+
def replay_deferred
|
|
53
|
+
file = @deferred
|
|
54
|
+
@deferred = nil
|
|
55
|
+
file.rewind
|
|
56
|
+
chunk = String.new(capacity: BUFFER_SIZE)
|
|
57
|
+
@entry << chunk while file.read(BUFFER_SIZE, chunk)
|
|
58
|
+
ensure
|
|
59
|
+
file&.close!
|
|
60
|
+
end
|
|
61
|
+
|
|
43
62
|
# The sheet of a book whose block wrote none.
|
|
44
63
|
def empty_book_sheet
|
|
45
64
|
start_sheet(EMPTY_BOOK_SHEET, [], header: false)
|
data/lib/calcsheet.rb
CHANGED
|
@@ -28,13 +28,19 @@ require_relative "calcsheet/readers/xlsx/workbook_meta"
|
|
|
28
28
|
require_relative "calcsheet/readers/xlsx/shared_strings"
|
|
29
29
|
require_relative "calcsheet/readers/xlsx_reader"
|
|
30
30
|
require_relative "calcsheet/readers/ods_reader"
|
|
31
|
+
require_relative "calcsheet/native_backends"
|
|
31
32
|
require_relative "calcsheet/column"
|
|
33
|
+
require_relative "calcsheet/formula"
|
|
34
|
+
require_relative "calcsheet/cell_range"
|
|
35
|
+
require_relative "calcsheet/display_format"
|
|
36
|
+
require_relative "calcsheet/style"
|
|
32
37
|
require_relative "calcsheet/sheet_names"
|
|
33
38
|
require_relative "calcsheet/write_sheet"
|
|
34
39
|
require_relative "calcsheet/write_book"
|
|
35
40
|
require_relative "calcsheet/stream_body"
|
|
36
41
|
require_relative "calcsheet/writers/csv_writer"
|
|
37
42
|
require_relative "calcsheet/writers/xml_text"
|
|
43
|
+
require_relative "calcsheet/writers/open_formula"
|
|
38
44
|
require_relative "calcsheet/writers/xlsx_writer"
|
|
39
45
|
require_relative "calcsheet/writers/ods_writer"
|
|
40
46
|
|
|
@@ -45,14 +51,22 @@ require_relative "calcsheet/writers/ods_writer"
|
|
|
45
51
|
module Calcsheet
|
|
46
52
|
# The formats Calcsheet.write infers from the extension of a path.
|
|
47
53
|
WRITE_EXTENSIONS = { ".csv" => :csv, ".xlsx" => :xlsx, ".ods" => :ods }.freeze
|
|
54
|
+
# @api private
|
|
55
|
+
# calcsheet's own reader backends by format: the initial Calcsheet.backends, and the readers calcsheet-native
|
|
56
|
+
# replaces (NativeBackends).
|
|
57
|
+
RUBY_BACKENDS = { csv: Readers::CsvReader, xlsx: Readers::XlsxReader, ods: Readers::OdsReader }.freeze
|
|
48
58
|
|
|
49
59
|
class << self
|
|
50
60
|
# The reader backends by format (see Reading for their contract). A mutable registry: an application may add or
|
|
51
61
|
# replace a backend before it opens files (+Calcsheet.backends[:tsv] = MyReader+). Detection only ever answers
|
|
52
62
|
# +:csv+, +:xlsx+ or +:ods+, so a backend under another key is used only with an explicit +format:+.
|
|
63
|
+
#
|
|
64
|
+
# The registry holds the Ruby backends: calcsheet-native, when it is installed and compatible, reads in place of
|
|
65
|
+
# calcsheet's own readers under +backend: :auto+ (the default) and +:native+ (see Book#backend). A backend an
|
|
66
|
+
# application registered is never replaced (+backend: :native+ refuses its format).
|
|
53
67
|
# @return [Hash{Symbol => Class}]
|
|
54
68
|
def backends
|
|
55
|
-
@backends ||=
|
|
69
|
+
@backends ||= RUBY_BACKENDS.dup
|
|
56
70
|
end
|
|
57
71
|
|
|
58
72
|
# Opens a spreadsheet for reading. With a block, yields the book, closes it afterwards and returns the
|
|
@@ -65,7 +79,9 @@ module Calcsheet
|
|
|
65
79
|
# caller is never closed
|
|
66
80
|
# @param format [Symbol, String, nil] a key of Calcsheet.backends, which skips detection; nil detects +:csv+,
|
|
67
81
|
# +:xlsx+ or +:ods+ from the content (the extension only breaks a tie)
|
|
68
|
-
# @param options [Hash] Options overrides for this book (any key of Configuration::DEFAULTS)
|
|
82
|
+
# @param options [Hash] Options overrides for this book (any key of Configuration::DEFAULTS); +backend:+ chooses
|
|
83
|
+
# the reader: +:auto+ calcsheet-native when it is installed and compatible, else the Ruby backend; +:ruby+ the
|
|
84
|
+
# backend of Calcsheet.backends; +:native+ calcsheet-native or BackendUnavailableError
|
|
69
85
|
# @yieldparam book [Book]
|
|
70
86
|
# @return [Object, Book] the block's value with a block; the open book without one
|
|
71
87
|
# @raise [ArgumentError] an unknown or invalid option, a source that is neither a path nor an IO, or a +format:+
|
|
@@ -75,6 +91,8 @@ module Calcsheet
|
|
|
75
91
|
# it is read instead)
|
|
76
92
|
# @raise [UnsupportedFormatError] an XLS or encrypted (OLE2) file, a zip that is neither XLSX nor ODS, an encrypted
|
|
77
93
|
# or unsupported zip entry, or a +format:+ without a backend
|
|
94
|
+
# @raise [BackendUnavailableError] +backend: :native+ and calcsheet-native cannot read the book (not installed, an
|
|
95
|
+
# unsupported protocol, a format it does not read, or an application's own backend for the format)
|
|
78
96
|
# @raise [MalformedFileError] a damaged zip archive or workbook part
|
|
79
97
|
# @raise [ZipBombError] the archive has more entries than +max_zip_entries+, or reading its workbook parts
|
|
80
98
|
# exceeds a zip limit
|
|
@@ -82,12 +100,12 @@ module Calcsheet
|
|
|
82
100
|
# raises it, at the first Book#sheets or read)
|
|
83
101
|
def open(source, format: nil, **options)
|
|
84
102
|
options = config.options(**options)
|
|
85
|
-
|
|
86
|
-
input = Source.open(source, options, needs_seek:
|
|
103
|
+
selected = format && select_backend(format, options.backend)
|
|
104
|
+
input = Source.open(source, options, needs_seek: selected.nil? || selected.first.needs_seek?)
|
|
87
105
|
begin
|
|
88
106
|
format = format.nil? ? FormatDetector.detect(input.io, path: input.path, max_zip_entries: options.max_zip_entries) : format.to_sym
|
|
89
|
-
backend_class
|
|
90
|
-
book = Book.new(backend_class.new(input, options), format: format, options: options, source: input)
|
|
107
|
+
backend_class, backend = selected || select_backend(format, options.backend)
|
|
108
|
+
book = Book.new(backend_class.new(input, options), format: format, options: options, source: input, backend: backend)
|
|
91
109
|
# Exception, not StandardError: an Interrupt or a timeout during detection must also close the Source,
|
|
92
110
|
# which deletes the Tempfile copy of a non-seekable stream (as Source#initialize does).
|
|
93
111
|
rescue Exception # rubocop:disable Lint/RescueException
|
|
@@ -287,9 +305,14 @@ module Calcsheet
|
|
|
287
305
|
raise ArgumentError, "format: expected a Symbol or a String, got #{format.inspect}"
|
|
288
306
|
end
|
|
289
307
|
|
|
290
|
-
|
|
308
|
+
# @param mode [Symbol] the +backend:+ option
|
|
309
|
+
# @return [Array(Class, Symbol)] the backend class of +format+ and +:ruby+ or +:native+ (Book#backend)
|
|
310
|
+
def select_backend(format, mode)
|
|
291
311
|
check_format_type(format)
|
|
292
|
-
|
|
312
|
+
format = format.to_sym
|
|
313
|
+
backend_class = backends.fetch(format) { raise UnsupportedFormatError, "no reader for format #{format.inspect}" }
|
|
314
|
+
native = NativeBackends.backend_for(format, mode) unless mode == :ruby
|
|
315
|
+
native ? [ native, :native ] : [ backend_class, :ruby ]
|
|
293
316
|
end
|
|
294
317
|
end
|
|
295
318
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: calcsheet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Demetra Opinioni.net Srl
|
|
@@ -101,19 +101,23 @@ files:
|
|
|
101
101
|
- lib/calcsheet.rb
|
|
102
102
|
- lib/calcsheet/book.rb
|
|
103
103
|
- lib/calcsheet/cast.rb
|
|
104
|
+
- lib/calcsheet/cell_range.rb
|
|
104
105
|
- lib/calcsheet/column.rb
|
|
105
106
|
- lib/calcsheet/column_letter.rb
|
|
106
107
|
- lib/calcsheet/configuration.rb
|
|
107
108
|
- lib/calcsheet/decimal_text.rb
|
|
109
|
+
- lib/calcsheet/display_format.rb
|
|
108
110
|
- lib/calcsheet/entry_name.rb
|
|
109
111
|
- lib/calcsheet/errors.rb
|
|
110
112
|
- lib/calcsheet/excel_serial.rb
|
|
111
113
|
- lib/calcsheet/format_detector.rb
|
|
114
|
+
- lib/calcsheet/formula.rb
|
|
112
115
|
- lib/calcsheet/formula_protection.rb
|
|
113
116
|
- lib/calcsheet/grid.rb
|
|
114
117
|
- lib/calcsheet/header_keys.rb
|
|
115
118
|
- lib/calcsheet/limited_inflater.rb
|
|
116
119
|
- lib/calcsheet/merge_check.rb
|
|
120
|
+
- lib/calcsheet/native_backends.rb
|
|
117
121
|
- lib/calcsheet/odf.rb
|
|
118
122
|
- lib/calcsheet/option_validation.rb
|
|
119
123
|
- lib/calcsheet/options.rb
|
|
@@ -122,6 +126,7 @@ files:
|
|
|
122
126
|
- lib/calcsheet/readers/csv_dialect.rb
|
|
123
127
|
- lib/calcsheet/readers/csv_input.rb
|
|
124
128
|
- lib/calcsheet/readers/csv_reader.rb
|
|
129
|
+
- lib/calcsheet/readers/csv_records.rb
|
|
125
130
|
- lib/calcsheet/readers/ods/cell_formats.rb
|
|
126
131
|
- lib/calcsheet/readers/ods/content_parser.rb
|
|
127
132
|
- lib/calcsheet/readers/ods/count.rb
|
|
@@ -149,13 +154,16 @@ files:
|
|
|
149
154
|
- lib/calcsheet/readers/xlsx_reader.rb
|
|
150
155
|
- lib/calcsheet/reading.rb
|
|
151
156
|
- lib/calcsheet/row.rb
|
|
157
|
+
- lib/calcsheet/row_builder.rb
|
|
152
158
|
- lib/calcsheet/sample.rb
|
|
153
159
|
- lib/calcsheet/schema.rb
|
|
154
160
|
- lib/calcsheet/sheet_info.rb
|
|
161
|
+
- lib/calcsheet/sheet_list.rb
|
|
155
162
|
- lib/calcsheet/sheet_names.rb
|
|
156
163
|
- lib/calcsheet/sheet_selection.rb
|
|
157
164
|
- lib/calcsheet/source.rb
|
|
158
165
|
- lib/calcsheet/stream_body.rb
|
|
166
|
+
- lib/calcsheet/style.rb
|
|
159
167
|
- lib/calcsheet/time_zone.rb
|
|
160
168
|
- lib/calcsheet/untrusted.rb
|
|
161
169
|
- lib/calcsheet/version.rb
|
|
@@ -163,6 +171,7 @@ files:
|
|
|
163
171
|
- lib/calcsheet/write_sheet.rb
|
|
164
172
|
- lib/calcsheet/writers/csv_writer.rb
|
|
165
173
|
- lib/calcsheet/writers/ods_writer.rb
|
|
174
|
+
- lib/calcsheet/writers/open_formula.rb
|
|
166
175
|
- lib/calcsheet/writers/xlsx_writer.rb
|
|
167
176
|
- lib/calcsheet/writers/xml_text.rb
|
|
168
177
|
- lib/calcsheet/writers/zip_buffered.rb
|
|
@@ -194,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
194
203
|
- !ruby/object:Gem::Version
|
|
195
204
|
version: '0'
|
|
196
205
|
requirements: []
|
|
197
|
-
rubygems_version:
|
|
206
|
+
rubygems_version: 4.0.16
|
|
198
207
|
specification_version: 4
|
|
199
208
|
summary: Streaming CSV, XLSX and ODS reader and writer.
|
|
200
209
|
test_files: []
|