ruh-roo 3.0.1
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/CHANGELOG.md +677 -0
- data/Gemfile +24 -0
- data/LICENSE +24 -0
- data/README.md +315 -0
- data/lib/roo/base.rb +607 -0
- data/lib/roo/constants.rb +7 -0
- data/lib/roo/csv.rb +141 -0
- data/lib/roo/errors.rb +11 -0
- data/lib/roo/excelx/cell/base.rb +108 -0
- data/lib/roo/excelx/cell/boolean.rb +30 -0
- data/lib/roo/excelx/cell/date.rb +28 -0
- data/lib/roo/excelx/cell/datetime.rb +107 -0
- data/lib/roo/excelx/cell/empty.rb +20 -0
- data/lib/roo/excelx/cell/number.rb +89 -0
- data/lib/roo/excelx/cell/string.rb +19 -0
- data/lib/roo/excelx/cell/time.rb +44 -0
- data/lib/roo/excelx/cell.rb +110 -0
- data/lib/roo/excelx/comments.rb +55 -0
- data/lib/roo/excelx/coordinate.rb +19 -0
- data/lib/roo/excelx/extractor.rb +39 -0
- data/lib/roo/excelx/format.rb +71 -0
- data/lib/roo/excelx/images.rb +26 -0
- data/lib/roo/excelx/relationships.rb +33 -0
- data/lib/roo/excelx/shared.rb +39 -0
- data/lib/roo/excelx/shared_strings.rb +151 -0
- data/lib/roo/excelx/sheet.rb +151 -0
- data/lib/roo/excelx/sheet_doc.rb +248 -0
- data/lib/roo/excelx/styles.rb +64 -0
- data/lib/roo/excelx/workbook.rb +63 -0
- data/lib/roo/excelx.rb +480 -0
- data/lib/roo/font.rb +17 -0
- data/lib/roo/formatters/base.rb +15 -0
- data/lib/roo/formatters/csv.rb +84 -0
- data/lib/roo/formatters/matrix.rb +23 -0
- data/lib/roo/formatters/xml.rb +31 -0
- data/lib/roo/formatters/yaml.rb +40 -0
- data/lib/roo/helpers/default_attr_reader.rb +20 -0
- data/lib/roo/helpers/weak_instance_cache.rb +41 -0
- data/lib/roo/libre_office.rb +4 -0
- data/lib/roo/link.rb +34 -0
- data/lib/roo/open_office.rb +628 -0
- data/lib/roo/spreadsheet.rb +39 -0
- data/lib/roo/tempdir.rb +21 -0
- data/lib/roo/utils.rb +128 -0
- data/lib/roo/version.rb +3 -0
- data/lib/roo.rb +36 -0
- data/roo.gemspec +28 -0
- metadata +189 -0
data/lib/roo/base.rb
ADDED
@@ -0,0 +1,607 @@
|
|
1
|
+
require "tmpdir"
|
2
|
+
require "stringio"
|
3
|
+
require "nokogiri"
|
4
|
+
require "roo/utils"
|
5
|
+
require "roo/formatters/base"
|
6
|
+
require "roo/formatters/csv"
|
7
|
+
require "roo/formatters/matrix"
|
8
|
+
require "roo/formatters/xml"
|
9
|
+
require "roo/formatters/yaml"
|
10
|
+
|
11
|
+
# Base class for all other types of spreadsheets
|
12
|
+
class Roo::Base
|
13
|
+
include Enumerable
|
14
|
+
include Roo::Formatters::Base
|
15
|
+
include Roo::Formatters::CSV
|
16
|
+
include Roo::Formatters::Matrix
|
17
|
+
include Roo::Formatters::XML
|
18
|
+
include Roo::Formatters::YAML
|
19
|
+
|
20
|
+
MAX_ROW_COL = 999_999
|
21
|
+
MIN_ROW_COL = 0
|
22
|
+
|
23
|
+
attr_reader :headers
|
24
|
+
|
25
|
+
# sets the line with attribute names (default: 1)
|
26
|
+
attr_accessor :header_line
|
27
|
+
|
28
|
+
def self.TEMP_PREFIX
|
29
|
+
warn "[DEPRECATION] please access TEMP_PREFIX via Roo::TEMP_PREFIX"
|
30
|
+
Roo::TEMP_PREFIX
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.finalize(object_id)
|
34
|
+
proc { finalize_tempdirs(object_id) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(filename, options = {}, _file_warning = :error, _tmpdir = nil)
|
38
|
+
@filename = filename
|
39
|
+
@options = options
|
40
|
+
|
41
|
+
@cell = {}
|
42
|
+
@cell_type = {}
|
43
|
+
@cells_read = {}
|
44
|
+
|
45
|
+
@first_row = {}
|
46
|
+
@last_row = {}
|
47
|
+
@first_column = {}
|
48
|
+
@last_column = {}
|
49
|
+
|
50
|
+
@header_line = 1
|
51
|
+
end
|
52
|
+
|
53
|
+
def close
|
54
|
+
if self.class.respond_to?(:finalize_tempdirs)
|
55
|
+
self.class.finalize_tempdirs(object_id)
|
56
|
+
end
|
57
|
+
|
58
|
+
instance_variables.each do |instance_variable|
|
59
|
+
instance_variable_set(instance_variable, nil)
|
60
|
+
end
|
61
|
+
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_sheet
|
66
|
+
@default_sheet ||= sheets.first
|
67
|
+
end
|
68
|
+
|
69
|
+
# sets the working sheet in the document
|
70
|
+
# 'sheet' can be a number (0 = first sheet) or the name of a sheet.
|
71
|
+
def default_sheet=(sheet)
|
72
|
+
validate_sheet!(sheet)
|
73
|
+
@default_sheet = sheet.is_a?(String) ? sheet : sheets[sheet]
|
74
|
+
@first_row[sheet] = @last_row[sheet] = @first_column[sheet] = @last_column[sheet] = nil
|
75
|
+
@cells_read[sheet] = false
|
76
|
+
end
|
77
|
+
|
78
|
+
# first non-empty column as a letter
|
79
|
+
def first_column_as_letter(sheet = default_sheet)
|
80
|
+
::Roo::Utils.number_to_letter(first_column(sheet))
|
81
|
+
end
|
82
|
+
|
83
|
+
# last non-empty column as a letter
|
84
|
+
def last_column_as_letter(sheet = default_sheet)
|
85
|
+
::Roo::Utils.number_to_letter(last_column(sheet))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Set first/last row/column for sheet
|
89
|
+
def first_last_row_col_for_sheet(sheet)
|
90
|
+
@first_last_row_cols ||= {}
|
91
|
+
@first_last_row_cols[sheet] ||= begin
|
92
|
+
result = collect_last_row_col_for_sheet(sheet)
|
93
|
+
{
|
94
|
+
first_row: result[:first_row] == MAX_ROW_COL ? nil : result[:first_row],
|
95
|
+
first_column: result[:first_column] == MAX_ROW_COL ? nil : result[:first_column],
|
96
|
+
last_row: result[:last_row] == MIN_ROW_COL ? nil : result[:last_row],
|
97
|
+
last_column: result[:last_column] == MIN_ROW_COL ? nil : result[:last_column]
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Collect first/last row/column from sheet
|
103
|
+
def collect_last_row_col_for_sheet(sheet)
|
104
|
+
first_row = first_column = MAX_ROW_COL
|
105
|
+
last_row = last_column = MIN_ROW_COL
|
106
|
+
@cell[sheet].each_pair do |key, value|
|
107
|
+
next unless value
|
108
|
+
first_row = [first_row, key.first.to_i].min
|
109
|
+
last_row = [last_row, key.first.to_i].max
|
110
|
+
first_column = [first_column, key.last.to_i].min
|
111
|
+
last_column = [last_column, key.last.to_i].max
|
112
|
+
end if @cell[sheet]
|
113
|
+
{ first_row: first_row, first_column: first_column, last_row: last_row, last_column: last_column }
|
114
|
+
end
|
115
|
+
|
116
|
+
%i(first_row last_row first_column last_column).each do |key|
|
117
|
+
ivar = "@#{key}".to_sym
|
118
|
+
define_method(key) do |sheet = default_sheet|
|
119
|
+
read_cells(sheet)
|
120
|
+
instance_variable_get(ivar)[sheet] ||= first_last_row_col_for_sheet(sheet)[key]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def inspect
|
125
|
+
"<##{self.class}:#{object_id.to_s(8)} #{instance_variables.join(' ')}>"
|
126
|
+
end
|
127
|
+
|
128
|
+
# find a row either by row number or a condition
|
129
|
+
# Caution: this works only within the default sheet -> set default_sheet before you call this method
|
130
|
+
# (experimental. see examples in the test_roo.rb file)
|
131
|
+
def find(*args) # :nodoc
|
132
|
+
options = (args.last.is_a?(Hash) ? args.pop : {})
|
133
|
+
|
134
|
+
case args[0]
|
135
|
+
when Integer
|
136
|
+
find_by_row(args[0])
|
137
|
+
when :all
|
138
|
+
find_by_conditions(options)
|
139
|
+
else
|
140
|
+
fail ArgumentError, "unexpected arg #{args[0].inspect}, pass a row index or :all"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# returns all values in this row as an array
|
145
|
+
# row numbers are 1,2,3,... like in the spreadsheet
|
146
|
+
def row(row_number, sheet = default_sheet)
|
147
|
+
read_cells(sheet)
|
148
|
+
first_column(sheet).upto(last_column(sheet)).map do |col|
|
149
|
+
cell(row_number, col, sheet)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# returns all values in this column as an array
|
154
|
+
# column numbers are 1,2,3,... like in the spreadsheet
|
155
|
+
def column(column_number, sheet = default_sheet)
|
156
|
+
if column_number.is_a?(::String)
|
157
|
+
column_number = ::Roo::Utils.letter_to_number(column_number)
|
158
|
+
end
|
159
|
+
read_cells(sheet)
|
160
|
+
first_row(sheet).upto(last_row(sheet)).map do |row|
|
161
|
+
cell(row, column_number, sheet)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# set a cell to a certain value
|
166
|
+
# (this will not be saved back to the spreadsheet file!)
|
167
|
+
def set(row, col, value, sheet = default_sheet) #:nodoc:
|
168
|
+
read_cells(sheet)
|
169
|
+
row, col = normalize(row, col)
|
170
|
+
cell_type = cell_type_by_value(value)
|
171
|
+
set_value(row, col, value, sheet)
|
172
|
+
set_type(row, col, cell_type, sheet)
|
173
|
+
end
|
174
|
+
|
175
|
+
def cell_type_by_value(value)
|
176
|
+
case value
|
177
|
+
when Integer then :float
|
178
|
+
when String, Float then :string
|
179
|
+
else
|
180
|
+
fail ArgumentError, "Type for #{value} not set"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# reopens and read a spreadsheet document
|
185
|
+
def reload
|
186
|
+
ds = default_sheet
|
187
|
+
reinitialize
|
188
|
+
self.default_sheet = ds
|
189
|
+
end
|
190
|
+
|
191
|
+
# true if cell is empty
|
192
|
+
def empty?(row, col, sheet = default_sheet)
|
193
|
+
read_cells(sheet)
|
194
|
+
row, col = normalize(row, col)
|
195
|
+
contents = cell(row, col, sheet)
|
196
|
+
!contents || (celltype(row, col, sheet) == :string && contents.empty?) \
|
197
|
+
|| (row < first_row(sheet) || row > last_row(sheet) || col < first_column(sheet) || col > last_column(sheet))
|
198
|
+
end
|
199
|
+
|
200
|
+
# returns information of the spreadsheet document and all sheets within
|
201
|
+
# this document.
|
202
|
+
def info
|
203
|
+
without_changing_default_sheet do
|
204
|
+
result = "File: #{File.basename(@filename)}\n"\
|
205
|
+
"Number of sheets: #{sheets.size}\n"\
|
206
|
+
"Sheets: #{sheets.join(', ')}\n"
|
207
|
+
n = 1
|
208
|
+
sheets.each do |sheet|
|
209
|
+
self.default_sheet = sheet
|
210
|
+
result << "Sheet " + n.to_s + ":\n"
|
211
|
+
if first_row
|
212
|
+
result << " First row: #{first_row}\n"
|
213
|
+
result << " Last row: #{last_row}\n"
|
214
|
+
result << " First column: #{::Roo::Utils.number_to_letter(first_column)}\n"
|
215
|
+
result << " Last column: #{::Roo::Utils.number_to_letter(last_column)}"
|
216
|
+
else
|
217
|
+
result << " - empty -"
|
218
|
+
end
|
219
|
+
result << "\n" if sheet != sheets.last
|
220
|
+
n += 1
|
221
|
+
end
|
222
|
+
result
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# when a method like spreadsheet.a42 is called
|
227
|
+
# convert it to a call of spreadsheet.cell('a',42)
|
228
|
+
def method_missing(m, *args)
|
229
|
+
# #aa42 => #cell('aa',42)
|
230
|
+
# #aa42('Sheet1') => #cell('aa',42,'Sheet1')
|
231
|
+
if m =~ /^([a-z]+)(\d+)$/
|
232
|
+
col = ::Roo::Utils.letter_to_number(Regexp.last_match[1])
|
233
|
+
row = Regexp.last_match[2].to_i
|
234
|
+
if args.empty?
|
235
|
+
cell(row, col)
|
236
|
+
else
|
237
|
+
cell(row, col, args.first)
|
238
|
+
end
|
239
|
+
else
|
240
|
+
super
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# access different worksheets by calling spreadsheet.sheet(1)
|
245
|
+
# or spreadsheet.sheet('SHEETNAME')
|
246
|
+
def sheet(index, name = false)
|
247
|
+
self.default_sheet = index.is_a?(::String) ? index : sheets[index]
|
248
|
+
name ? [default_sheet, self] : self
|
249
|
+
end
|
250
|
+
|
251
|
+
# iterate through all worksheets of a document
|
252
|
+
def each_with_pagename
|
253
|
+
sheets.each do |s|
|
254
|
+
yield sheet(s, true)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# by passing in headers as options, this method returns
|
259
|
+
# specific columns from your header assignment
|
260
|
+
# for example:
|
261
|
+
# xls.sheet('New Prices').parse(:upc => 'UPC', :price => 'Price') would return:
|
262
|
+
# [{:upc => 123456789012, :price => 35.42},..]
|
263
|
+
|
264
|
+
# the queries are matched with regex, so regex options can be passed in
|
265
|
+
# such as :price => '^(Cost|Price)'
|
266
|
+
# case insensitive by default
|
267
|
+
|
268
|
+
# by using the :header_search option, you can query for headers
|
269
|
+
# and return a hash of every row with the keys set to the header result
|
270
|
+
# for example:
|
271
|
+
# xls.sheet('New Prices').parse(:header_search => ['UPC*SKU','^Price*\sCost\s'])
|
272
|
+
|
273
|
+
# that example searches for a column titled either UPC or SKU and another
|
274
|
+
# column titled either Price or Cost (regex characters allowed)
|
275
|
+
# * is the wildcard character
|
276
|
+
|
277
|
+
# you can also pass in a :clean => true option to strip the sheet of
|
278
|
+
# control characters and white spaces around columns
|
279
|
+
|
280
|
+
def each(options = {})
|
281
|
+
return to_enum(:each, options) unless block_given?
|
282
|
+
|
283
|
+
if options.empty?
|
284
|
+
1.upto(last_row) do |line|
|
285
|
+
yield row(line)
|
286
|
+
end
|
287
|
+
else
|
288
|
+
clean_sheet_if_need(options)
|
289
|
+
search_or_set_header(options)
|
290
|
+
headers = @headers ||
|
291
|
+
(first_column..last_column).each_with_object({}) do |col, hash|
|
292
|
+
hash[cell(@header_line, col)] = col
|
293
|
+
end
|
294
|
+
|
295
|
+
@header_line.upto(last_row) do |line|
|
296
|
+
yield(headers.each_with_object({}) { |(k, v), hash| hash[k] = cell(line, v) })
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def parse(options = {})
|
302
|
+
results = each(options).map do |row|
|
303
|
+
block_given? ? yield(row) : row
|
304
|
+
end
|
305
|
+
|
306
|
+
options[:headers] == true ? results : results.drop(1)
|
307
|
+
end
|
308
|
+
|
309
|
+
def row_with(query, return_headers = false)
|
310
|
+
line_no = 0
|
311
|
+
closest_mismatched_headers = []
|
312
|
+
each do |row|
|
313
|
+
line_no += 1
|
314
|
+
headers = query.map { |q| row.grep(q)[0] }.compact
|
315
|
+
if headers.length == query.length
|
316
|
+
@header_line = line_no
|
317
|
+
return return_headers ? headers : line_no
|
318
|
+
else
|
319
|
+
closest_mismatched_headers = headers if headers.length > closest_mismatched_headers.length
|
320
|
+
if line_no > 100
|
321
|
+
break
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
missing_headers = query.select { |q| closest_mismatched_headers.grep(q).empty? }
|
326
|
+
raise Roo::HeaderRowNotFoundError, missing_headers
|
327
|
+
end
|
328
|
+
|
329
|
+
protected
|
330
|
+
|
331
|
+
def file_type_check(filename, exts, name, warning_level, packed = nil)
|
332
|
+
if packed == :zip
|
333
|
+
# spreadsheet.ods.zip => spreadsheet.ods
|
334
|
+
# Decompression is not performed here, only the 'zip' extension
|
335
|
+
# is removed from the file.
|
336
|
+
filename = File.basename(filename, File.extname(filename))
|
337
|
+
end
|
338
|
+
|
339
|
+
if uri?(filename) && (qs_begin = filename.rindex("?"))
|
340
|
+
filename = filename[0..qs_begin - 1]
|
341
|
+
end
|
342
|
+
exts = Array(exts)
|
343
|
+
|
344
|
+
return if exts.include?(File.extname(filename).downcase)
|
345
|
+
|
346
|
+
case warning_level
|
347
|
+
when :error
|
348
|
+
warn file_type_warning_message(filename, exts)
|
349
|
+
fail TypeError, "#{filename} is not #{name} file"
|
350
|
+
when :warning
|
351
|
+
warn "are you sure, this is #{name} spreadsheet file?"
|
352
|
+
warn file_type_warning_message(filename, exts)
|
353
|
+
when :ignore
|
354
|
+
# ignore
|
355
|
+
else
|
356
|
+
fail "#{warning_level} illegal state of file_warning"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# konvertiert einen Key in der Form "12,45" (=row,column) in
|
361
|
+
# ein Array mit numerischen Werten ([12,45])
|
362
|
+
# Diese Methode ist eine temp. Loesung, um zu erforschen, ob der
|
363
|
+
# Zugriff mit numerischen Keys schneller ist.
|
364
|
+
def key_to_num(str)
|
365
|
+
r, c = str.split(",")
|
366
|
+
[r.to_i, c.to_i]
|
367
|
+
end
|
368
|
+
|
369
|
+
# see: key_to_num
|
370
|
+
def key_to_string(arr)
|
371
|
+
"#{arr[0]},#{arr[1]}"
|
372
|
+
end
|
373
|
+
|
374
|
+
def is_stream?(filename_or_stream)
|
375
|
+
filename_or_stream.respond_to?(:seek)
|
376
|
+
end
|
377
|
+
|
378
|
+
private
|
379
|
+
|
380
|
+
def clean_sheet_if_need(options)
|
381
|
+
return unless options[:clean]
|
382
|
+
options.delete(:clean)
|
383
|
+
@cleaned ||= {}
|
384
|
+
clean_sheet(default_sheet) unless @cleaned[default_sheet]
|
385
|
+
end
|
386
|
+
|
387
|
+
def search_or_set_header(options)
|
388
|
+
if options[:header_search]
|
389
|
+
@headers = nil
|
390
|
+
@header_line = row_with(options[:header_search])
|
391
|
+
elsif [:first_row, true].include?(options[:headers])
|
392
|
+
@headers = []
|
393
|
+
row(first_row).each_with_index { |x, i| @headers << [x, i + 1] }
|
394
|
+
else
|
395
|
+
set_headers(options)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def local_filename(filename, tmpdir, packed)
|
400
|
+
return if is_stream?(filename)
|
401
|
+
filename = download_uri(filename, tmpdir) if uri?(filename)
|
402
|
+
filename = unzip(filename, tmpdir) if packed == :zip
|
403
|
+
|
404
|
+
fail IOError, "file #{filename} does not exist" unless File.file?(filename)
|
405
|
+
|
406
|
+
filename
|
407
|
+
end
|
408
|
+
|
409
|
+
def file_type_warning_message(filename, exts)
|
410
|
+
*rest, last_ext = exts
|
411
|
+
ext_list = rest.any? ? "#{rest.join(', ')} or #{last_ext}" : last_ext
|
412
|
+
"use #{Roo::CLASS_FOR_EXTENSION.fetch(last_ext.sub('.', '').to_sym)}.new to handle #{ext_list} spreadsheet files. This has #{File.extname(filename).downcase}"
|
413
|
+
rescue KeyError
|
414
|
+
raise "unknown file types: #{ext_list}"
|
415
|
+
end
|
416
|
+
|
417
|
+
def find_by_row(row_index)
|
418
|
+
row_index += (header_line - 1) if @header_line
|
419
|
+
|
420
|
+
row(row_index).size.times.map do |cell_index|
|
421
|
+
cell(row_index, cell_index + 1)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def find_by_conditions(options)
|
426
|
+
rows = first_row.upto(last_row)
|
427
|
+
header_for = 1.upto(last_column).each_with_object({}) do |col, hash|
|
428
|
+
hash[col] = cell(@header_line, col)
|
429
|
+
end
|
430
|
+
|
431
|
+
# are all conditions met?
|
432
|
+
conditions = options[:conditions]
|
433
|
+
if conditions && !conditions.empty?
|
434
|
+
column_with = header_for.invert
|
435
|
+
rows = rows.select do |i|
|
436
|
+
conditions.all? { |key, val| cell(i, column_with[key]) == val }
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
if options[:array]
|
441
|
+
rows.map { |i| row(i) }
|
442
|
+
else
|
443
|
+
rows.map do |i|
|
444
|
+
1.upto(row(i).size).each_with_object({}) do |j, hash|
|
445
|
+
hash[header_for.fetch(j)] = cell(i, j)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def without_changing_default_sheet
|
452
|
+
original_default_sheet = default_sheet
|
453
|
+
yield
|
454
|
+
ensure
|
455
|
+
self.default_sheet = original_default_sheet
|
456
|
+
end
|
457
|
+
|
458
|
+
def reinitialize
|
459
|
+
initialize(@filename)
|
460
|
+
end
|
461
|
+
|
462
|
+
def find_basename(filename)
|
463
|
+
if uri?(filename)
|
464
|
+
require "uri"
|
465
|
+
uri = URI.parse filename
|
466
|
+
File.basename(uri.path)
|
467
|
+
elsif !is_stream?(filename)
|
468
|
+
File.basename(filename)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def make_tmpdir(prefix = nil, root = nil, &block)
|
473
|
+
warn "[DEPRECATION] extend Roo::Tempdir and use its .make_tempdir instead"
|
474
|
+
prefix = "#{Roo::TEMP_PREFIX}#{prefix}"
|
475
|
+
root ||= ENV["ROO_TMP"]
|
476
|
+
|
477
|
+
if block_given?
|
478
|
+
# folder is deleted at end of block
|
479
|
+
::Dir.mktmpdir(prefix, root, &block)
|
480
|
+
else
|
481
|
+
self.class.make_tempdir(self, prefix, root)
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def clean_sheet(sheet)
|
486
|
+
read_cells(sheet)
|
487
|
+
@cell[sheet].each_pair do |coord, value|
|
488
|
+
@cell[sheet][coord] = sanitize_value(value) if value.is_a?(::String)
|
489
|
+
end
|
490
|
+
@cleaned[sheet] = true
|
491
|
+
end
|
492
|
+
|
493
|
+
def sanitize_value(v)
|
494
|
+
v.gsub(/[[:cntrl:]]|^[\p{Space}]+|[\p{Space}]+$/, "")
|
495
|
+
end
|
496
|
+
|
497
|
+
def set_headers(hash = {})
|
498
|
+
# try to find header row with all values or give an error
|
499
|
+
# then create new hash by indexing strings and keeping integers for header array
|
500
|
+
header_row = row_with(hash.values, true)
|
501
|
+
@headers = {}
|
502
|
+
hash.each_with_index do |(key, _), index|
|
503
|
+
@headers[key] = header_index(header_row[index])
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
def header_index(query)
|
508
|
+
row(@header_line).index(query) + first_column
|
509
|
+
end
|
510
|
+
|
511
|
+
def set_value(row, col, value, sheet = default_sheet)
|
512
|
+
@cell[sheet][[row, col]] = value
|
513
|
+
end
|
514
|
+
|
515
|
+
def set_type(row, col, type, sheet = default_sheet)
|
516
|
+
@cell_type[sheet][[row, col]] = type
|
517
|
+
end
|
518
|
+
|
519
|
+
# converts cell coordinate to numeric values of row,col
|
520
|
+
def normalize(row, col)
|
521
|
+
if row.is_a?(::String)
|
522
|
+
if col.is_a?(::Integer)
|
523
|
+
# ('A',1):
|
524
|
+
# ('B', 5) -> (5, 2)
|
525
|
+
row, col = col, row
|
526
|
+
else
|
527
|
+
fail ArgumentError
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
col = ::Roo::Utils.letter_to_number(col) if col.is_a?(::String)
|
532
|
+
|
533
|
+
[row, col]
|
534
|
+
end
|
535
|
+
|
536
|
+
def uri?(filename)
|
537
|
+
filename.start_with?("http://", "https://", "ftp://")
|
538
|
+
rescue
|
539
|
+
false
|
540
|
+
end
|
541
|
+
|
542
|
+
def download_uri(uri, tmpdir)
|
543
|
+
require "open-uri"
|
544
|
+
tempfilename = File.join(tmpdir, find_basename(uri))
|
545
|
+
begin
|
546
|
+
File.open(tempfilename, "wb") do |file|
|
547
|
+
URI.open(uri, "User-Agent" => "Ruby/#{RUBY_VERSION}") do |net|
|
548
|
+
file.write(net.read)
|
549
|
+
end
|
550
|
+
end
|
551
|
+
rescue OpenURI::HTTPError
|
552
|
+
raise "could not open #{uri}"
|
553
|
+
end
|
554
|
+
tempfilename
|
555
|
+
end
|
556
|
+
|
557
|
+
def open_from_stream(stream, tmpdir)
|
558
|
+
tempfilename = File.join(tmpdir, "spreadsheet")
|
559
|
+
File.open(tempfilename, "wb") do |file|
|
560
|
+
file.write(stream[7..-1])
|
561
|
+
end
|
562
|
+
File.join(tmpdir, "spreadsheet")
|
563
|
+
end
|
564
|
+
|
565
|
+
def unzip(filename, tmpdir)
|
566
|
+
require "zip/filesystem"
|
567
|
+
|
568
|
+
Zip::File.open(filename) do |zip|
|
569
|
+
process_zipfile_packed(zip, tmpdir)
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# check if default_sheet was set and exists in sheets-array
|
574
|
+
def validate_sheet!(sheet)
|
575
|
+
case sheet
|
576
|
+
when nil
|
577
|
+
fail ArgumentError, "Error: sheet 'nil' not valid"
|
578
|
+
when Integer
|
579
|
+
sheets.fetch(sheet) do
|
580
|
+
fail RangeError, "sheet index #{sheet} not found"
|
581
|
+
end
|
582
|
+
when String
|
583
|
+
unless sheets.include?(sheet)
|
584
|
+
fail RangeError, "sheet '#{sheet}' not found"
|
585
|
+
end
|
586
|
+
else
|
587
|
+
fail TypeError, "not a valid sheet type: #{sheet.inspect}"
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
def process_zipfile_packed(zip, tmpdir, path = "")
|
592
|
+
if zip.file.file? path
|
593
|
+
# extract and return filename
|
594
|
+
File.open(File.join(tmpdir, path), "wb") do |file|
|
595
|
+
file.write(zip.read(path))
|
596
|
+
end
|
597
|
+
File.join(tmpdir, path)
|
598
|
+
else
|
599
|
+
ret = nil
|
600
|
+
path += "/" unless path.empty?
|
601
|
+
zip.dir.foreach(path) do |filename|
|
602
|
+
ret = process_zipfile_packed(zip, tmpdir, path + filename)
|
603
|
+
end
|
604
|
+
ret
|
605
|
+
end
|
606
|
+
end
|
607
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roo
|
4
|
+
ROO_EXCEL_NOTICE = "Excel support has been extracted to roo-xls due to its dependency on the GPL'd spreadsheet gem. Install roo-xls to use Roo::Excel."
|
5
|
+
ROO_EXCELML_NOTICE = "Excel SpreadsheetML support has been extracted to roo-xls. Install roo-xls to use Roo::Excel2003XML."
|
6
|
+
ROO_GOOGLE_NOTICE = "Google support has been extracted to roo-google. Install roo-google to use Roo::Google."
|
7
|
+
end
|