calcsheet 0.1.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 +7 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +921 -0
- data/lib/calcsheet/book.rb +164 -0
- data/lib/calcsheet/cast.rb +578 -0
- data/lib/calcsheet/column.rb +117 -0
- data/lib/calcsheet/column_letter.rb +34 -0
- data/lib/calcsheet/configuration.rb +186 -0
- data/lib/calcsheet/decimal_text.rb +19 -0
- data/lib/calcsheet/entry_name.rb +28 -0
- data/lib/calcsheet/errors.rb +186 -0
- data/lib/calcsheet/excel_serial.rb +65 -0
- data/lib/calcsheet/format_detector.rb +109 -0
- data/lib/calcsheet/formula_protection.rb +31 -0
- data/lib/calcsheet/grid.rb +20 -0
- data/lib/calcsheet/header_keys.rb +66 -0
- data/lib/calcsheet/limited_inflater.rb +326 -0
- data/lib/calcsheet/merge_check.rb +41 -0
- data/lib/calcsheet/odf.rb +45 -0
- data/lib/calcsheet/option_validation.rb +160 -0
- data/lib/calcsheet/options.rb +10 -0
- data/lib/calcsheet/raw_row.rb +18 -0
- data/lib/calcsheet/raw_types.rb +15 -0
- data/lib/calcsheet/readers/csv_dialect.rb +264 -0
- data/lib/calcsheet/readers/csv_input.rb +128 -0
- data/lib/calcsheet/readers/csv_reader.rb +239 -0
- data/lib/calcsheet/readers/ods/cell_formats.rb +115 -0
- data/lib/calcsheet/readers/ods/content_parser.rb +594 -0
- data/lib/calcsheet/readers/ods/count.rb +30 -0
- data/lib/calcsheet/readers/ods/cursor.rb +121 -0
- data/lib/calcsheet/readers/ods/patterns.rb +70 -0
- data/lib/calcsheet/readers/ods/row_seek.rb +157 -0
- data/lib/calcsheet/readers/ods/scanner.rb +302 -0
- data/lib/calcsheet/readers/ods/style_scan.rb +108 -0
- data/lib/calcsheet/readers/ods/tag_text.rb +76 -0
- data/lib/calcsheet/readers/ods_reader.rb +71 -0
- data/lib/calcsheet/readers/ox_errors.rb +40 -0
- data/lib/calcsheet/readers/references.rb +62 -0
- data/lib/calcsheet/readers/row_batches.rb +45 -0
- data/lib/calcsheet/readers/sax_sheet.rb +108 -0
- data/lib/calcsheet/readers/stream_remainder.rb +66 -0
- data/lib/calcsheet/readers/tag_scan.rb +119 -0
- data/lib/calcsheet/readers/xlsx/escapes.rb +36 -0
- data/lib/calcsheet/readers/xlsx/local_name.rb +54 -0
- data/lib/calcsheet/readers/xlsx/number_format.rb +89 -0
- data/lib/calcsheet/readers/xlsx/row_skipper.rb +162 -0
- data/lib/calcsheet/readers/xlsx/shared_strings.rb +209 -0
- data/lib/calcsheet/readers/xlsx/sheet_parser.rb +435 -0
- data/lib/calcsheet/readers/xlsx/styles.rb +156 -0
- data/lib/calcsheet/readers/xlsx/workbook_meta.rb +228 -0
- data/lib/calcsheet/readers/xlsx_reader.rb +122 -0
- data/lib/calcsheet/reading.rb +281 -0
- data/lib/calcsheet/row.rb +110 -0
- data/lib/calcsheet/sample.rb +10 -0
- data/lib/calcsheet/schema.rb +140 -0
- data/lib/calcsheet/sheet_info.rb +21 -0
- data/lib/calcsheet/sheet_names.rb +114 -0
- data/lib/calcsheet/sheet_selection.rb +58 -0
- data/lib/calcsheet/source.rb +263 -0
- data/lib/calcsheet/stream_body.rb +81 -0
- data/lib/calcsheet/time_zone.rb +96 -0
- data/lib/calcsheet/untrusted.rb +32 -0
- data/lib/calcsheet/version.rb +6 -0
- data/lib/calcsheet/write_book.rb +202 -0
- data/lib/calcsheet/write_sheet.rb +282 -0
- data/lib/calcsheet/writers/csv_writer.rb +138 -0
- data/lib/calcsheet/writers/ods_writer.rb +437 -0
- data/lib/calcsheet/writers/xlsx_writer.rb +350 -0
- data/lib/calcsheet/writers/xml_text.rb +35 -0
- data/lib/calcsheet/writers/zip_buffered.rb +55 -0
- data/lib/calcsheet/zip_container.rb +190 -0
- data/lib/calcsheet/zip_directory.rb +202 -0
- data/lib/calcsheet/zip_end_record.rb +117 -0
- data/lib/calcsheet.rb +295 -0
- metadata +200 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "reading"
|
|
4
|
+
|
|
5
|
+
module Calcsheet
|
|
6
|
+
# An opened workbook, returned by Calcsheet.open. Every read method takes the read options
|
|
7
|
+
# (+sheet:+, +header:+, +columns:+, +types:+, +on_error:+, +start_at:+, +skip_blank:+, +include_hidden:+,
|
|
8
|
+
# +required_headers:+) and any Options key but Reading::OPEN_KEYS as an override for that read; #sample and
|
|
9
|
+
# #merge_check take only the sheet selection (and #sample Options overrides).
|
|
10
|
+
#
|
|
11
|
+
# The read options are checked, and the sheets selected, when a read method is called: an invalid one raises
|
|
12
|
+
# ArgumentError, a missing sheet SheetNotFoundError, even for the lazy #rows. What depends on the content is raised
|
|
13
|
+
# while the rows are read: HeaderMismatchError, ColumnNotFoundError and AmbiguousColumnError before the first row
|
|
14
|
+
# is yielded, then TooManyRowsError, CellError (+on_error: :raise+), MalformedFileError and the zip limits as the
|
|
15
|
+
# rows come.
|
|
16
|
+
class Book
|
|
17
|
+
# @return [Symbol] :csv, :xlsx, :ods, or the registry key of the backend
|
|
18
|
+
attr_reader :format
|
|
19
|
+
# @return [Options]
|
|
20
|
+
attr_reader :options
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
# @param backend [Object] a reader backend (see Reading)
|
|
24
|
+
# @param source [Source] closed with the book
|
|
25
|
+
def initialize(backend, format:, options:, source:)
|
|
26
|
+
@backend = backend
|
|
27
|
+
@format = format
|
|
28
|
+
@options = options
|
|
29
|
+
@source = source
|
|
30
|
+
@closed = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Array<SheetInfo>] every sheet in workbook order, hidden ones included (a CSV file has one, named after
|
|
34
|
+
# the file); read once and kept
|
|
35
|
+
# @raise [IOError] after #close
|
|
36
|
+
# @raise [MalformedFileError] an ODS whose content.xml cannot be scanned (ODS lists its sheets at the first call)
|
|
37
|
+
# @raise [TooManySheetsError] an ODS with more sheets than +max_sheets+
|
|
38
|
+
def sheets
|
|
39
|
+
check_open
|
|
40
|
+
@sheets ||= @backend.sheets.dup.freeze
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The header and the first data rows of each selected sheet, with native values, reading only the beginning of
|
|
44
|
+
# each sheet: for previews and column mapping.
|
|
45
|
+
#
|
|
46
|
+
# @param rows [Integer] data rows per sheet; default +options.sample_rows+
|
|
47
|
+
# @param sheets [Integer, String, Array, :all] the sheets to sample, by index or name; :all means the visible
|
|
48
|
+
# ones
|
|
49
|
+
# @param include_hidden [Boolean] :all includes the hidden sheets
|
|
50
|
+
# @param overrides [Hash] Options overrides (not Reading::OPEN_KEYS)
|
|
51
|
+
# @return [Array<Sample>] one per selected sheet, in the given order
|
|
52
|
+
# @raise [ArgumentError] an invalid +rows:+, sheet selection or override
|
|
53
|
+
# @raise [SheetNotFoundError] a selected sheet the workbook does not have
|
|
54
|
+
# @raise [IOError] after #close
|
|
55
|
+
# @raise [Calcsheet::Error] reading the sheets (MalformedFileError, ZipBombError...)
|
|
56
|
+
def sample(rows: nil, sheets: :all, include_hidden: false, **overrides)
|
|
57
|
+
reading = Reading.new(@backend, self.sheets, @options, overrides, sheet: sheets, include_hidden: include_hidden)
|
|
58
|
+
rows ||= reading.options.sample_rows
|
|
59
|
+
raise ArgumentError, "rows: must be a non-negative Integer, got #{rows.inspect}" unless rows.is_a?(Integer) && rows >= 0
|
|
60
|
+
|
|
61
|
+
reading.sample(rows)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Whether the selected sheets can be read as one (+sheet: :all+ or an Array): compares their header rows without
|
|
65
|
+
# reading the data rows.
|
|
66
|
+
#
|
|
67
|
+
# @param sheets [Integer, String, Array, :all] the sheets to compare; :all means the visible ones
|
|
68
|
+
# @param include_hidden [Boolean] :all includes the hidden sheets
|
|
69
|
+
# @return [MergeCheck]
|
|
70
|
+
# @raise [ArgumentError] an invalid sheet selection
|
|
71
|
+
# @raise [SheetNotFoundError] a selected sheet the workbook does not have
|
|
72
|
+
# @raise [IOError] after #close
|
|
73
|
+
# @raise [Calcsheet::Error] reading the header rows (MalformedFileError, ZipBombError...)
|
|
74
|
+
def merge_check(sheets: :all, include_hidden: false)
|
|
75
|
+
Reading.new(@backend, self.sheets, @options, sheet: sheets, include_hidden: include_hidden).merge_check
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param read [Hash] read options (Reading::KEYS: +sheet:+, +header:+, +columns:+, +types:+, +on_error:+,
|
|
79
|
+
# +start_at:+, +skip_blank:+, +include_hidden:+, +required_headers:+) and Options overrides; see the README
|
|
80
|
+
# @return [Enumerator<Row>] a lazy enumerator: rows are read when it is iterated, one batch at a time
|
|
81
|
+
# @raise [ArgumentError] at the call: an invalid read option or override, or an override of Reading::OPEN_KEYS
|
|
82
|
+
# @raise [SheetNotFoundError] at the call: a selected sheet the workbook does not have
|
|
83
|
+
# @raise [IOError] at the call after #close; when iterated after #close; or at the next batch when the book is
|
|
84
|
+
# closed while it is iterated (likewise #each_row and #each_batch)
|
|
85
|
+
# @raise [Calcsheet::Error] while iterated, as described for Book (HeaderMismatchError, ColumnNotFoundError,
|
|
86
|
+
# AmbiguousColumnError, TooManyRowsError, CellError, MalformedFileError, ZipBombError...)
|
|
87
|
+
def rows(**read)
|
|
88
|
+
reading = reading(read)
|
|
89
|
+
Enumerator.new do |yielder|
|
|
90
|
+
check_open
|
|
91
|
+
reading.each(reading.options.batch_size) { |row| yielder << row }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @param read [Hash] read options and Options overrides, as #rows
|
|
96
|
+
# @yieldparam row [Row]
|
|
97
|
+
# @return [self, Enumerator<Row>] an enumerator without a block (#rows)
|
|
98
|
+
# @raise [ArgumentError] an invalid read option or override (also without a block)
|
|
99
|
+
# @raise [SheetNotFoundError] a selected sheet the workbook does not have (also without a block)
|
|
100
|
+
# @raise [Calcsheet::Error] while the rows are read, as #rows
|
|
101
|
+
def each_row(**read, &block)
|
|
102
|
+
return rows(**read) unless block
|
|
103
|
+
|
|
104
|
+
reading = reading(read)
|
|
105
|
+
reading.each(reading.options.batch_size, &block)
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @param size [Integer] rows per batch, the last one may be shorter; default +options.batch_size+
|
|
110
|
+
# @param read [Hash] read options and Options overrides, as #rows
|
|
111
|
+
# @yieldparam rows [Array<Row>] a new Array per batch
|
|
112
|
+
# @return [self, Enumerator<Array<Row>>] an enumerator without a block
|
|
113
|
+
# @raise [ArgumentError] an invalid +size:+, read option or override (also without a block)
|
|
114
|
+
# @raise [SheetNotFoundError] a selected sheet the workbook does not have (also without a block)
|
|
115
|
+
# @raise [Calcsheet::Error] while the rows are read, as #rows
|
|
116
|
+
def each_batch(size: nil, **read)
|
|
117
|
+
reading = reading(read)
|
|
118
|
+
size ||= reading.options.batch_size
|
|
119
|
+
raise ArgumentError, "size: must be a positive Integer, got #{size.inspect}" unless size.is_a?(Integer) && size.positive?
|
|
120
|
+
return enum_for(:each_batch, size: size, **read) unless block_given?
|
|
121
|
+
|
|
122
|
+
batch = []
|
|
123
|
+
reading.each(size) do |row|
|
|
124
|
+
batch << row
|
|
125
|
+
next if batch.size < size
|
|
126
|
+
|
|
127
|
+
yield batch
|
|
128
|
+
batch = []
|
|
129
|
+
end
|
|
130
|
+
yield batch unless batch.empty?
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Closes the backend and the source (deleting the Tempfile copy of a stream; an IO given by the caller stays
|
|
135
|
+
# open). Idempotent.
|
|
136
|
+
# @return [void]
|
|
137
|
+
def close
|
|
138
|
+
return if @closed
|
|
139
|
+
|
|
140
|
+
@closed = true
|
|
141
|
+
begin
|
|
142
|
+
@backend.close
|
|
143
|
+
ensure
|
|
144
|
+
@source.close
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @return [Boolean] whether #close was called
|
|
149
|
+
def closed?
|
|
150
|
+
@closed
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def reading(read)
|
|
156
|
+
settings, overrides = Reading.split(read)
|
|
157
|
+
Reading.new(@backend, sheets, @options, overrides, **settings).tap { _1.open_check = method(:check_open) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def check_open
|
|
161
|
+
raise IOError, "closed book" if @closed
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|