roo 2.3.0 → 2.10.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 +5 -5
- data/.codeclimate.yml +17 -0
- data/.github/issue_template.md +16 -0
- data/.github/pull_request_template.md +14 -0
- data/.github/workflows/pull-request.yml +15 -0
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +186 -0
- data/CHANGELOG.md +148 -0
- data/Gemfile +4 -4
- data/LICENSE +2 -0
- data/README.md +84 -27
- data/Rakefile +1 -1
- data/lib/roo/base.rb +111 -237
- data/lib/roo/constants.rb +5 -3
- data/lib/roo/csv.rb +106 -85
- data/lib/roo/errors.rb +2 -0
- data/lib/roo/excelx/cell/base.rb +26 -12
- data/lib/roo/excelx/cell/boolean.rb +9 -6
- data/lib/roo/excelx/cell/date.rb +7 -7
- data/lib/roo/excelx/cell/datetime.rb +50 -44
- data/lib/roo/excelx/cell/empty.rb +3 -2
- data/lib/roo/excelx/cell/number.rb +60 -47
- data/lib/roo/excelx/cell/string.rb +3 -3
- data/lib/roo/excelx/cell/time.rb +17 -16
- data/lib/roo/excelx/cell.rb +11 -7
- data/lib/roo/excelx/comments.rb +3 -3
- data/lib/roo/excelx/coordinate.rb +11 -4
- data/lib/roo/excelx/extractor.rb +20 -3
- data/lib/roo/excelx/format.rb +38 -31
- data/lib/roo/excelx/images.rb +26 -0
- data/lib/roo/excelx/relationships.rb +12 -4
- data/lib/roo/excelx/shared.rb +10 -3
- data/lib/roo/excelx/shared_strings.rb +113 -9
- data/lib/roo/excelx/sheet.rb +49 -10
- data/lib/roo/excelx/sheet_doc.rb +101 -48
- data/lib/roo/excelx/styles.rb +4 -4
- data/lib/roo/excelx/workbook.rb +8 -3
- data/lib/roo/excelx.rb +85 -42
- 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/open_office.rb +41 -27
- data/lib/roo/spreadsheet.rb +8 -2
- data/lib/roo/tempdir.rb +24 -0
- data/lib/roo/utils.rb +76 -26
- data/lib/roo/version.rb +1 -1
- data/lib/roo.rb +5 -0
- data/roo.gemspec +22 -12
- data/spec/lib/roo/base_spec.rb +65 -3
- data/spec/lib/roo/csv_spec.rb +19 -0
- data/spec/lib/roo/excelx/cell/time_spec.rb +15 -0
- data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
- data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
- data/spec/lib/roo/excelx_spec.rb +237 -5
- data/spec/lib/roo/openoffice_spec.rb +2 -2
- data/spec/lib/roo/spreadsheet_spec.rb +1 -1
- data/spec/lib/roo/strict_spec.rb +43 -0
- data/spec/lib/roo/utils_spec.rb +22 -9
- data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
- data/spec/lib/roo_spec.rb +0 -0
- data/spec/spec_helper.rb +2 -7
- data/test/excelx/cell/test_attr_reader_default.rb +72 -0
- data/test/excelx/cell/test_base.rb +6 -2
- data/test/excelx/cell/test_boolean.rb +1 -3
- data/test/excelx/cell/test_date.rb +1 -6
- data/test/excelx/cell/test_datetime.rb +7 -10
- data/test/excelx/cell/test_empty.rb +12 -2
- data/test/excelx/cell/test_number.rb +28 -4
- data/test/excelx/cell/test_string.rb +21 -3
- data/test/excelx/cell/test_time.rb +7 -10
- data/test/excelx/test_coordinate.rb +51 -0
- data/test/formatters/test_csv.rb +136 -0
- data/test/formatters/test_matrix.rb +76 -0
- data/test/formatters/test_xml.rb +78 -0
- data/test/formatters/test_yaml.rb +20 -0
- data/test/helpers/test_accessing_files.rb +81 -0
- data/test/helpers/test_comments.rb +43 -0
- data/test/helpers/test_formulas.rb +9 -0
- data/test/helpers/test_labels.rb +103 -0
- data/test/helpers/test_sheets.rb +55 -0
- data/test/helpers/test_styles.rb +62 -0
- data/test/roo/test_base.rb +182 -0
- data/test/roo/test_csv.rb +88 -0
- data/test/roo/test_excelx.rb +360 -0
- data/test/roo/test_libre_office.rb +9 -0
- data/test/roo/test_open_office.rb +289 -0
- data/test/test_helper.rb +129 -14
- data/test/test_roo.rb +60 -1765
- metadata +91 -21
- data/.travis.yml +0 -14
@@ -0,0 +1,84 @@
|
|
1
|
+
module Roo
|
2
|
+
module Formatters
|
3
|
+
module CSV
|
4
|
+
def to_csv(filename = nil, separator = ",", sheet = default_sheet)
|
5
|
+
if filename
|
6
|
+
File.open(filename, "w") do |file|
|
7
|
+
write_csv_content(file, sheet, separator)
|
8
|
+
end
|
9
|
+
true
|
10
|
+
else
|
11
|
+
sio = ::StringIO.new
|
12
|
+
write_csv_content(sio, sheet, separator)
|
13
|
+
sio.rewind
|
14
|
+
sio.read
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Write all cells to the csv file. File can be a filename or nil. If the
|
21
|
+
# file argument is nil the output goes to STDOUT
|
22
|
+
def write_csv_content(file = nil, sheet = nil, separator = ",")
|
23
|
+
file ||= STDOUT
|
24
|
+
return unless first_row(sheet) # The sheet is empty
|
25
|
+
|
26
|
+
1.upto(last_row(sheet)) do |row|
|
27
|
+
1.upto(last_column(sheet)) do |col|
|
28
|
+
# TODO: use CSV.generate_line
|
29
|
+
file.print(separator) if col > 1
|
30
|
+
file.print cell_to_csv(row, col, sheet)
|
31
|
+
end
|
32
|
+
file.print("\n")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# The content of a cell in the csv output
|
37
|
+
def cell_to_csv(row, col, sheet)
|
38
|
+
return "" if empty?(row, col, sheet)
|
39
|
+
|
40
|
+
onecell = cell(row, col, sheet)
|
41
|
+
|
42
|
+
case celltype(row, col, sheet)
|
43
|
+
when :string
|
44
|
+
%("#{onecell.gsub('"', '""')}") unless onecell.empty?
|
45
|
+
when :boolean
|
46
|
+
# TODO: this only works for excelx
|
47
|
+
onecell = self.sheet_for(sheet).cells[[row, col]].formatted_value
|
48
|
+
%("#{onecell.gsub('"', '""').downcase}")
|
49
|
+
when :float, :percentage
|
50
|
+
if onecell == onecell.to_i
|
51
|
+
onecell.to_i.to_s
|
52
|
+
else
|
53
|
+
onecell.to_s
|
54
|
+
end
|
55
|
+
when :formula
|
56
|
+
case onecell
|
57
|
+
when String
|
58
|
+
%("#{onecell.gsub('"', '""')}") unless onecell.empty?
|
59
|
+
when Integer
|
60
|
+
onecell.to_s
|
61
|
+
when Float
|
62
|
+
if onecell == onecell.to_i
|
63
|
+
onecell.to_i.to_s
|
64
|
+
else
|
65
|
+
onecell.to_s
|
66
|
+
end
|
67
|
+
when Date, DateTime, TrueClass, FalseClass
|
68
|
+
onecell.to_s
|
69
|
+
else
|
70
|
+
fail "unhandled onecell-class #{onecell.class}"
|
71
|
+
end
|
72
|
+
when :date, :datetime
|
73
|
+
onecell.to_s
|
74
|
+
when :time
|
75
|
+
integer_to_timestring(onecell)
|
76
|
+
when :link
|
77
|
+
%("#{onecell.url.gsub('"', '""')}")
|
78
|
+
else
|
79
|
+
fail "unhandled celltype #{celltype(row, col, sheet)}"
|
80
|
+
end || ""
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Roo
|
2
|
+
module Formatters
|
3
|
+
module Matrix
|
4
|
+
# returns a matrix object from the whole sheet or a rectangular area of a sheet
|
5
|
+
def to_matrix(from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet)
|
6
|
+
require 'matrix'
|
7
|
+
|
8
|
+
return ::Matrix.empty unless first_row
|
9
|
+
|
10
|
+
from_row ||= first_row(sheet)
|
11
|
+
to_row ||= last_row(sheet)
|
12
|
+
from_column ||= first_column(sheet)
|
13
|
+
to_column ||= last_column(sheet)
|
14
|
+
|
15
|
+
::Matrix.rows(from_row.upto(to_row).map do |row|
|
16
|
+
from_column.upto(to_column).map do |col|
|
17
|
+
cell(row, col, sheet)
|
18
|
+
end
|
19
|
+
end)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# returns an XML representation of all sheets of a spreadsheet file
|
2
|
+
module Roo
|
3
|
+
module Formatters
|
4
|
+
module XML
|
5
|
+
def to_xml
|
6
|
+
Nokogiri::XML::Builder.new do |xml|
|
7
|
+
xml.spreadsheet do
|
8
|
+
sheets.each do |sheet|
|
9
|
+
self.default_sheet = sheet
|
10
|
+
xml.sheet(name: sheet) do |x|
|
11
|
+
if first_row && last_row && first_column && last_column
|
12
|
+
# sonst gibt es Fehler bei leeren Blaettern
|
13
|
+
first_row.upto(last_row) do |row|
|
14
|
+
first_column.upto(last_column) do |col|
|
15
|
+
next if empty?(row, col)
|
16
|
+
|
17
|
+
x.cell(cell(row, col),
|
18
|
+
row: row,
|
19
|
+
column: col,
|
20
|
+
type: celltype(row, col))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end.to_xml
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Roo
|
2
|
+
module Formatters
|
3
|
+
module YAML
|
4
|
+
# returns a rectangular area (default: all cells) as yaml-output
|
5
|
+
# you can add additional attributes with the prefix parameter like:
|
6
|
+
# oo.to_yaml({"file"=>"flightdata_2007-06-26", "sheet" => "1"})
|
7
|
+
def to_yaml(prefix = {}, from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet)
|
8
|
+
# return an empty string if there is no first_row, i.e. the sheet is empty
|
9
|
+
return "" unless first_row
|
10
|
+
|
11
|
+
from_row ||= first_row(sheet)
|
12
|
+
to_row ||= last_row(sheet)
|
13
|
+
from_column ||= first_column(sheet)
|
14
|
+
to_column ||= last_column(sheet)
|
15
|
+
|
16
|
+
result = "--- \n"
|
17
|
+
from_row.upto(to_row) do |row|
|
18
|
+
from_column.upto(to_column) do |col|
|
19
|
+
next if empty?(row, col, sheet)
|
20
|
+
|
21
|
+
result << "cell_#{row}_#{col}: \n"
|
22
|
+
prefix.each do|k, v|
|
23
|
+
result << " #{k}: #{v} \n"
|
24
|
+
end
|
25
|
+
result << " row: #{row} \n"
|
26
|
+
result << " col: #{col} \n"
|
27
|
+
result << " celltype: #{celltype(row, col, sheet)} \n"
|
28
|
+
value = cell(row, col, sheet)
|
29
|
+
if celltype(row, col, sheet) == :time
|
30
|
+
value = integer_to_timestring(value)
|
31
|
+
end
|
32
|
+
result << " value: #{value} \n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roo
|
4
|
+
module Helpers
|
5
|
+
module DefaultAttrReader
|
6
|
+
def attr_reader_with_default(attr_hash)
|
7
|
+
attr_hash.each do |attr_name, default_value|
|
8
|
+
instance_variable = :"@#{attr_name}"
|
9
|
+
define_method attr_name do
|
10
|
+
if instance_variable_defined? instance_variable
|
11
|
+
instance_variable_get instance_variable
|
12
|
+
else
|
13
|
+
default_value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "weakref"
|
4
|
+
|
5
|
+
module Roo
|
6
|
+
module Helpers
|
7
|
+
module WeakInstanceCache
|
8
|
+
private
|
9
|
+
|
10
|
+
def instance_cache(key)
|
11
|
+
object = nil
|
12
|
+
|
13
|
+
if instance_variable_defined?(key) && (ref = instance_variable_get(key)) && ref.weakref_alive?
|
14
|
+
begin
|
15
|
+
object = ref.__getobj__
|
16
|
+
rescue => e
|
17
|
+
unless (defined?(::WeakRef::RefError) && e.is_a?(::WeakRef::RefError)) || (defined?(RefError) && e.is_a?(RefError))
|
18
|
+
raise e
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
unless object
|
24
|
+
object = yield
|
25
|
+
ObjectSpace.define_finalizer(object, instance_cache_finalizer(key))
|
26
|
+
instance_variable_set(key, WeakRef.new(object))
|
27
|
+
end
|
28
|
+
|
29
|
+
object
|
30
|
+
end
|
31
|
+
|
32
|
+
def instance_cache_finalizer(key)
|
33
|
+
proc do |object_id|
|
34
|
+
if instance_variable_defined?(key) && (ref = instance_variable_get(key)) && (!ref.weakref_alive? || ref.__getobj__.object_id == object_id)
|
35
|
+
remove_instance_variable(key)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/roo/open_office.rb
CHANGED
@@ -1,15 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'date'
|
2
4
|
require 'nokogiri'
|
3
5
|
require 'cgi'
|
4
6
|
require 'zip/filesystem'
|
5
7
|
require 'roo/font'
|
8
|
+
require 'roo/tempdir'
|
6
9
|
require 'base64'
|
10
|
+
require 'openssl'
|
7
11
|
|
8
12
|
module Roo
|
9
13
|
class OpenOffice < Roo::Base
|
10
|
-
|
11
|
-
|
12
|
-
|
14
|
+
extend Roo::Tempdir
|
15
|
+
|
16
|
+
ERROR_MISSING_CONTENT_XML = 'file missing required content.xml'
|
17
|
+
XPATH_FIND_TABLE_STYLES = "//*[local-name()='automatic-styles']"
|
18
|
+
XPATH_LOCAL_NAME_TABLE = "//*[local-name()='table']"
|
13
19
|
|
14
20
|
# initialization and opening of a spreadsheet file
|
15
21
|
# values for packed: :zip
|
@@ -19,15 +25,32 @@ module Roo
|
|
19
25
|
|
20
26
|
@only_visible_sheets = options[:only_visible_sheets]
|
21
27
|
file_type_check(filename, '.ods', 'an Roo::OpenOffice', file_warning, packed)
|
22
|
-
|
28
|
+
# NOTE: Create temp directory and allow Ruby to cleanup the temp directory
|
29
|
+
# when the object is garbage collected. Initially, the finalizer was
|
30
|
+
# created in the Roo::Tempdir module, but that led to a segfault
|
31
|
+
# when testing in Ruby 2.4.0.
|
32
|
+
@tmpdir = self.class.make_tempdir(self, find_basename(filename), options[:tmpdir_root])
|
33
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(object_id))
|
23
34
|
@filename = local_filename(filename, @tmpdir, packed)
|
24
35
|
# TODO: @cells_read[:default] = false
|
25
36
|
open_oo_file(options)
|
26
37
|
super(filename, options)
|
27
38
|
initialize_default_variables
|
28
|
-
|
29
|
-
|
30
|
-
|
39
|
+
|
40
|
+
unless @table_display.any?
|
41
|
+
doc.xpath(XPATH_FIND_TABLE_STYLES).each do |style|
|
42
|
+
read_table_styles(style)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@sheet_names = doc.xpath(XPATH_LOCAL_NAME_TABLE).map do |sheet|
|
47
|
+
if !@only_visible_sheets || @table_display[attribute(sheet, 'style-name')]
|
48
|
+
sheet.attributes['name'].value
|
49
|
+
end
|
50
|
+
end.compact
|
51
|
+
rescue
|
52
|
+
self.class.finalize_tempdirs(object_id)
|
53
|
+
raise
|
31
54
|
end
|
32
55
|
|
33
56
|
def open_oo_file(options)
|
@@ -132,16 +155,7 @@ module Roo
|
|
132
155
|
end
|
133
156
|
|
134
157
|
def sheets
|
135
|
-
|
136
|
-
doc.xpath(XPATH_FIND_TABLE_STYLES).each do |style|
|
137
|
-
read_table_styles(style)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
doc.xpath(XPATH_LOCAL_NAME_TABLE).map do |sheet|
|
141
|
-
if !@only_visible_sheets || @table_display[attribute(sheet, 'style-name')]
|
142
|
-
sheet.attributes['name'].value
|
143
|
-
end
|
144
|
-
end.compact
|
158
|
+
@sheet_names
|
145
159
|
end
|
146
160
|
|
147
161
|
# version of the Roo::OpenOffice document
|
@@ -285,7 +299,6 @@ module Roo
|
|
285
299
|
algorithm_node['manifest:initialisation-vector']
|
286
300
|
)
|
287
301
|
key_derivation_name = key_derivation_node['manifest:key-derivation-name']
|
288
|
-
key_size = key_derivation_node['manifest:key-size'].to_i
|
289
302
|
iteration_count = key_derivation_node['manifest:iteration-count'].to_i
|
290
303
|
salt = Base64.decode64(key_derivation_node['manifest:salt'])
|
291
304
|
|
@@ -294,10 +307,8 @@ module Roo
|
|
294
307
|
start_key_generation_node[
|
295
308
|
'manifest:start-key-generation-name'
|
296
309
|
]
|
297
|
-
key_generation_size = start_key_generation_node['manifest:key-size'].to_i
|
298
310
|
|
299
311
|
hashed_password = password
|
300
|
-
key = nil
|
301
312
|
|
302
313
|
if key_generation_name == 'http://www.w3.org/2000/09/xmldsig#sha256'
|
303
314
|
|
@@ -337,7 +348,7 @@ module Roo
|
|
337
348
|
def find_cipher(*args)
|
338
349
|
fail ArgumentError, 'Unknown algorithm ' + algorithm unless args[0] == 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
|
339
350
|
|
340
|
-
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
351
|
+
cipher = ::OpenSSL::Cipher.new('AES-256-CBC')
|
341
352
|
cipher.decrypt
|
342
353
|
cipher.padding = 0
|
343
354
|
cipher.key = find_cipher_key(cipher, *args[1..4])
|
@@ -350,7 +361,7 @@ module Roo
|
|
350
361
|
def find_cipher_key(*args)
|
351
362
|
fail ArgumentError, 'Unknown key derivation name ', args[1] unless args[1] == 'PBKDF2'
|
352
363
|
|
353
|
-
OpenSSL::PKCS5.pbkdf2_hmac_sha1(args[2], args[3], args[4], args[0].key_len)
|
364
|
+
::OpenSSL::PKCS5.pbkdf2_hmac_sha1(args[2], args[3], args[4], args[0].key_len)
|
354
365
|
end
|
355
366
|
|
356
367
|
# Block decrypt raw bytes from the zip file based on the cipher
|
@@ -412,7 +423,10 @@ module Roo
|
|
412
423
|
@style[sheet][key] = style_name
|
413
424
|
case @cell_type[sheet][key]
|
414
425
|
when :float
|
415
|
-
|
426
|
+
value = (table_cell.attributes['value'].to_s.include?(".") || table_cell.children.first.text.include?(".")) ? v.to_f : v.to_i
|
427
|
+
value = 'true' if formula == '=TRUE()'
|
428
|
+
value = 'false' if formula == '=FALSE()'
|
429
|
+
@cell[sheet][key] = value
|
416
430
|
when :percentage
|
417
431
|
@cell[sheet][key] = v.to_f
|
418
432
|
when :string
|
@@ -506,7 +520,7 @@ module Roo
|
|
506
520
|
str_v += child.content #.text
|
507
521
|
end
|
508
522
|
end
|
509
|
-
str_v.gsub
|
523
|
+
str_v = str_v.gsub(/'/, "'") # special case not supported by unescapeHTML
|
510
524
|
str_v = CGI.unescapeHTML(str_v)
|
511
525
|
end # == 'p'
|
512
526
|
end
|
@@ -552,7 +566,7 @@ module Roo
|
|
552
566
|
end
|
553
567
|
|
554
568
|
def read_labels
|
555
|
-
@label ||=
|
569
|
+
@label ||= doc.xpath('//table:named-range').each_with_object({}) do |ne, hash|
|
556
570
|
#-
|
557
571
|
# $Sheet1.$C$5
|
558
572
|
#+
|
@@ -560,8 +574,8 @@ module Roo
|
|
560
574
|
sheetname, coords = attribute(ne, 'cell-range-address').to_s.split('.$')
|
561
575
|
col, row = coords.split('$')
|
562
576
|
sheetname = sheetname[1..-1] if sheetname[0, 1] == '$'
|
563
|
-
[name
|
564
|
-
end
|
577
|
+
hash[name] = [sheetname, row, col]
|
578
|
+
end
|
565
579
|
end
|
566
580
|
|
567
581
|
def read_styles(style_elements)
|
data/lib/roo/spreadsheet.rb
CHANGED
@@ -24,8 +24,14 @@ module Roo
|
|
24
24
|
options[:file_warning] = :ignore
|
25
25
|
extension.tr('.', '').downcase.to_sym
|
26
26
|
else
|
27
|
-
|
28
|
-
|
27
|
+
parsed_path =
|
28
|
+
if path =~ /\A#{::URI::DEFAULT_PARSER.make_regexp}\z/
|
29
|
+
# path is 7th match
|
30
|
+
Regexp.last_match[7]
|
31
|
+
else
|
32
|
+
path
|
33
|
+
end
|
34
|
+
::File.extname(parsed_path).tr('.', '').downcase.to_sym
|
29
35
|
end
|
30
36
|
end
|
31
37
|
end
|
data/lib/roo/tempdir.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Roo
|
2
|
+
module Tempdir
|
3
|
+
def finalize_tempdirs(object_id)
|
4
|
+
if @tempdirs && (dirs_to_remove = @tempdirs[object_id])
|
5
|
+
@tempdirs.delete(object_id)
|
6
|
+
dirs_to_remove.each do |dir|
|
7
|
+
# Pass force=true to avoid an exception (and thus warnings in Ruby 3.1) if dir has
|
8
|
+
# already been removed. This can occur when the finalizer is called both in a forked
|
9
|
+
# child process and in the parent.
|
10
|
+
::FileUtils.remove_entry(dir, true)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def make_tempdir(object, prefix, root)
|
16
|
+
root ||= ENV["ROO_TMP"]
|
17
|
+
# NOTE: This folder is cleaned up by finalize_tempdirs.
|
18
|
+
::Dir.mktmpdir("#{Roo::TEMP_PREFIX}#{prefix}", root).tap do |tmpdir|
|
19
|
+
@tempdirs ||= Hash.new { |h, k| h[k] = [] }
|
20
|
+
@tempdirs[object.object_id] << tmpdir
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/roo/utils.rb
CHANGED
@@ -1,42 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Roo
|
2
4
|
module Utils
|
3
5
|
extend self
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
+
LETTERS = ('A'..'Z').to_a
|
8
|
+
|
9
|
+
def extract_coordinate(s)
|
10
|
+
num = letter_num = 0
|
11
|
+
num_only = false
|
7
12
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
+
s.each_byte do |b|
|
14
|
+
if !num_only && (index = char_index(b))
|
15
|
+
letter_num *= 26
|
16
|
+
letter_num += index
|
17
|
+
elsif index = num_index(b)
|
18
|
+
num_only = true
|
19
|
+
num *= 10
|
20
|
+
num += index
|
21
|
+
else
|
22
|
+
fail ArgumentError
|
23
|
+
end
|
13
24
|
end
|
25
|
+
fail ArgumentError if letter_num == 0 || !num_only
|
26
|
+
|
27
|
+
Excelx::Coordinate.new(num, letter_num)
|
14
28
|
end
|
15
29
|
|
16
|
-
alias_method :ref_to_key, :
|
30
|
+
alias_method :ref_to_key, :extract_coordinate
|
17
31
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
32
|
+
def split_coordinate(str)
|
33
|
+
warn "[DEPRECATION] `Roo::Utils.split_coordinate` is deprecated. Please use `Roo::Utils.extract_coordinate` instead."
|
34
|
+
extract_coordinate(str)
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
def split_coord(str)
|
40
|
+
coord = extract_coordinate(str)
|
41
|
+
[number_to_letter(coord.column), coord.row]
|
26
42
|
end
|
27
43
|
|
28
44
|
# convert a number to something like 'AB' (1 => 'A', 2 => 'B', ...)
|
29
45
|
def number_to_letter(num)
|
30
|
-
|
31
|
-
num = num.to_i
|
46
|
+
result = +""
|
32
47
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
num = ((num - mod) / 26)
|
48
|
+
until num.zero?
|
49
|
+
num, index = (num - 1).divmod(26)
|
50
|
+
result.prepend(LETTERS[index])
|
37
51
|
end
|
38
52
|
|
39
|
-
|
53
|
+
result
|
40
54
|
end
|
41
55
|
|
42
56
|
def letter_to_number(letters)
|
@@ -56,11 +70,30 @@ module Roo
|
|
56
70
|
cells = str.split(':')
|
57
71
|
return 1 if cells.count == 1
|
58
72
|
raise ArgumentError.new("invalid range string: #{str}. Supported range format 'A1:B2'") if cells.count != 2
|
59
|
-
x1, y1 =
|
60
|
-
x2, y2 =
|
73
|
+
x1, y1 = extract_coordinate(cells[0])
|
74
|
+
x2, y2 = extract_coordinate(cells[1])
|
61
75
|
(x2 - (x1 - 1)) * (y2 - (y1 - 1))
|
62
76
|
end
|
63
77
|
|
78
|
+
def coordinates_in_range(str)
|
79
|
+
return to_enum(:coordinates_in_range, str) unless block_given?
|
80
|
+
coordinates = str.split(":", 2).map! { |s| extract_coordinate s }
|
81
|
+
|
82
|
+
case coordinates.size
|
83
|
+
when 1
|
84
|
+
yield coordinates[0]
|
85
|
+
when 2
|
86
|
+
tl, br = coordinates
|
87
|
+
rows = tl.row..br.row
|
88
|
+
cols = tl.column..br.column
|
89
|
+
rows.each do |row|
|
90
|
+
cols.each do |column|
|
91
|
+
yield Excelx::Coordinate.new(row, column)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
64
97
|
def load_xml(path)
|
65
98
|
::File.open(path, 'rb') do |file|
|
66
99
|
::Nokogiri::XML(file)
|
@@ -69,10 +102,27 @@ module Roo
|
|
69
102
|
|
70
103
|
# Yield each element of a given type ('row', 'c', etc.) to caller
|
71
104
|
def each_element(path, elements)
|
105
|
+
elements = Array(elements)
|
72
106
|
Nokogiri::XML::Reader(::File.open(path, 'rb'), nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).each do |node|
|
73
|
-
next unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT &&
|
107
|
+
next unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT && elements.include?(node.name)
|
74
108
|
yield Nokogiri::XML(node.outer_xml).root if block_given?
|
75
109
|
end
|
76
110
|
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def char_index(byte)
|
115
|
+
if byte >= 65 && byte <= 90
|
116
|
+
byte - 64
|
117
|
+
elsif byte >= 97 && byte <= 122
|
118
|
+
byte - 96
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def num_index(byte)
|
123
|
+
if byte >= 48 && byte <= 57
|
124
|
+
byte - 48
|
125
|
+
end
|
126
|
+
end
|
77
127
|
end
|
78
128
|
end
|
data/lib/roo/version.rb
CHANGED
data/lib/roo.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roo/version'
|
1
4
|
require 'roo/constants'
|
2
5
|
require 'roo/errors'
|
3
6
|
require 'roo/spreadsheet'
|
@@ -9,6 +12,8 @@ module Roo
|
|
9
12
|
autoload :Excelx, 'roo/excelx'
|
10
13
|
autoload :CSV, 'roo/csv'
|
11
14
|
|
15
|
+
TEMP_PREFIX = 'roo_'
|
16
|
+
|
12
17
|
CLASS_FOR_EXTENSION = {
|
13
18
|
ods: Roo::OpenOffice,
|
14
19
|
xlsx: Roo::Excelx,
|
data/roo.gemspec
CHANGED
@@ -4,22 +4,32 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'roo/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
11
|
-
spec.summary
|
12
|
-
spec.description
|
13
|
-
spec.homepage
|
14
|
-
spec.license
|
7
|
+
spec.name = 'roo'
|
8
|
+
spec.version = Roo::VERSION
|
9
|
+
spec.authors = ['Thomas Preymesser', 'Hugh McGowan', 'Ben Woosley', 'Oleksandr Simonov', 'Steven Daniels', 'Anmol Chopra']
|
10
|
+
spec.email = ['ruby.ruby.ruby.roo@gmail.com', 'oleksandr@simonov.me']
|
11
|
+
spec.summary = 'Roo can access the contents of various spreadsheet files.'
|
12
|
+
spec.description = "Roo can access the contents of various spreadsheet files. It can handle\n* OpenOffice\n* Excelx\n* LibreOffice\n* CSV"
|
13
|
+
spec.homepage = 'https://github.com/roo-rb/roo'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
|
-
spec.files
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
17
|
spec.files.reject! { |fn| fn.include?('test/files') }
|
18
|
-
spec.require_paths
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
21
|
+
spec.required_ruby_version = ">= 2.6.0"
|
22
|
+
else
|
23
|
+
spec.required_ruby_version = ">= 2.7.0"
|
24
|
+
end
|
19
25
|
|
20
26
|
spec.add_dependency 'nokogiri', '~> 1'
|
21
|
-
spec.add_dependency 'rubyzip', '
|
27
|
+
spec.add_dependency 'rubyzip', '>= 1.3.0', '< 3.0.0'
|
22
28
|
|
23
|
-
spec.add_development_dependency 'rake'
|
29
|
+
spec.add_development_dependency 'rake'
|
24
30
|
spec.add_development_dependency 'minitest', '~> 5.4', '>= 5.4.3'
|
31
|
+
spec.add_development_dependency 'rack', '~> 1.6', '< 2.0.0'
|
32
|
+
if RUBY_VERSION >= '3.0.0'
|
33
|
+
spec.add_development_dependency 'matrix'
|
34
|
+
end
|
25
35
|
end
|