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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +9 -0
  3. data/CHANGELOG.md +62 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +921 -0
  6. data/lib/calcsheet/book.rb +164 -0
  7. data/lib/calcsheet/cast.rb +578 -0
  8. data/lib/calcsheet/column.rb +117 -0
  9. data/lib/calcsheet/column_letter.rb +34 -0
  10. data/lib/calcsheet/configuration.rb +186 -0
  11. data/lib/calcsheet/decimal_text.rb +19 -0
  12. data/lib/calcsheet/entry_name.rb +28 -0
  13. data/lib/calcsheet/errors.rb +186 -0
  14. data/lib/calcsheet/excel_serial.rb +65 -0
  15. data/lib/calcsheet/format_detector.rb +109 -0
  16. data/lib/calcsheet/formula_protection.rb +31 -0
  17. data/lib/calcsheet/grid.rb +20 -0
  18. data/lib/calcsheet/header_keys.rb +66 -0
  19. data/lib/calcsheet/limited_inflater.rb +326 -0
  20. data/lib/calcsheet/merge_check.rb +41 -0
  21. data/lib/calcsheet/odf.rb +45 -0
  22. data/lib/calcsheet/option_validation.rb +160 -0
  23. data/lib/calcsheet/options.rb +10 -0
  24. data/lib/calcsheet/raw_row.rb +18 -0
  25. data/lib/calcsheet/raw_types.rb +15 -0
  26. data/lib/calcsheet/readers/csv_dialect.rb +264 -0
  27. data/lib/calcsheet/readers/csv_input.rb +128 -0
  28. data/lib/calcsheet/readers/csv_reader.rb +239 -0
  29. data/lib/calcsheet/readers/ods/cell_formats.rb +115 -0
  30. data/lib/calcsheet/readers/ods/content_parser.rb +594 -0
  31. data/lib/calcsheet/readers/ods/count.rb +30 -0
  32. data/lib/calcsheet/readers/ods/cursor.rb +121 -0
  33. data/lib/calcsheet/readers/ods/patterns.rb +70 -0
  34. data/lib/calcsheet/readers/ods/row_seek.rb +157 -0
  35. data/lib/calcsheet/readers/ods/scanner.rb +302 -0
  36. data/lib/calcsheet/readers/ods/style_scan.rb +108 -0
  37. data/lib/calcsheet/readers/ods/tag_text.rb +76 -0
  38. data/lib/calcsheet/readers/ods_reader.rb +71 -0
  39. data/lib/calcsheet/readers/ox_errors.rb +40 -0
  40. data/lib/calcsheet/readers/references.rb +62 -0
  41. data/lib/calcsheet/readers/row_batches.rb +45 -0
  42. data/lib/calcsheet/readers/sax_sheet.rb +108 -0
  43. data/lib/calcsheet/readers/stream_remainder.rb +66 -0
  44. data/lib/calcsheet/readers/tag_scan.rb +119 -0
  45. data/lib/calcsheet/readers/xlsx/escapes.rb +36 -0
  46. data/lib/calcsheet/readers/xlsx/local_name.rb +54 -0
  47. data/lib/calcsheet/readers/xlsx/number_format.rb +89 -0
  48. data/lib/calcsheet/readers/xlsx/row_skipper.rb +162 -0
  49. data/lib/calcsheet/readers/xlsx/shared_strings.rb +209 -0
  50. data/lib/calcsheet/readers/xlsx/sheet_parser.rb +435 -0
  51. data/lib/calcsheet/readers/xlsx/styles.rb +156 -0
  52. data/lib/calcsheet/readers/xlsx/workbook_meta.rb +228 -0
  53. data/lib/calcsheet/readers/xlsx_reader.rb +122 -0
  54. data/lib/calcsheet/reading.rb +281 -0
  55. data/lib/calcsheet/row.rb +110 -0
  56. data/lib/calcsheet/sample.rb +10 -0
  57. data/lib/calcsheet/schema.rb +140 -0
  58. data/lib/calcsheet/sheet_info.rb +21 -0
  59. data/lib/calcsheet/sheet_names.rb +114 -0
  60. data/lib/calcsheet/sheet_selection.rb +58 -0
  61. data/lib/calcsheet/source.rb +263 -0
  62. data/lib/calcsheet/stream_body.rb +81 -0
  63. data/lib/calcsheet/time_zone.rb +96 -0
  64. data/lib/calcsheet/untrusted.rb +32 -0
  65. data/lib/calcsheet/version.rb +6 -0
  66. data/lib/calcsheet/write_book.rb +202 -0
  67. data/lib/calcsheet/write_sheet.rb +282 -0
  68. data/lib/calcsheet/writers/csv_writer.rb +138 -0
  69. data/lib/calcsheet/writers/ods_writer.rb +437 -0
  70. data/lib/calcsheet/writers/xlsx_writer.rb +350 -0
  71. data/lib/calcsheet/writers/xml_text.rb +35 -0
  72. data/lib/calcsheet/writers/zip_buffered.rb +55 -0
  73. data/lib/calcsheet/zip_container.rb +190 -0
  74. data/lib/calcsheet/zip_directory.rb +202 -0
  75. data/lib/calcsheet/zip_end_record.rb +117 -0
  76. data/lib/calcsheet.rb +295 -0
  77. 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