roo 2.7.0 → 2.8.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/issue_template.md +16 -0
- data/.github/pull_request_template.md +14 -0
- data/.rubocop.yml +186 -0
- data/.travis.yml +12 -7
- data/CHANGELOG.md +53 -2
- data/LICENSE +2 -0
- data/README.md +29 -13
- data/lib/roo/base.rb +69 -61
- data/lib/roo/constants.rb +5 -3
- data/lib/roo/csv.rb +20 -12
- 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 +14 -18
- data/lib/roo/excelx/cell/empty.rb +3 -2
- data/lib/roo/excelx/cell/number.rb +35 -34
- data/lib/roo/excelx/cell/string.rb +3 -3
- data/lib/roo/excelx/cell/time.rb +4 -3
- data/lib/roo/excelx/cell.rb +10 -6
- data/lib/roo/excelx/comments.rb +3 -3
- data/lib/roo/excelx/coordinate.rb +11 -4
- data/lib/roo/excelx/extractor.rb +21 -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 +9 -15
- data/lib/roo/excelx/sheet.rb +49 -10
- data/lib/roo/excelx/sheet_doc.rb +89 -48
- data/lib/roo/excelx/styles.rb +3 -3
- data/lib/roo/excelx/workbook.rb +7 -3
- data/lib/roo/excelx.rb +42 -16
- 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 +8 -6
- data/lib/roo/spreadsheet.rb +1 -1
- data/lib/roo/utils.rb +70 -20
- data/lib/roo/version.rb +1 -1
- data/lib/roo.rb +4 -1
- data/roo.gemspec +13 -11
- data/spec/lib/roo/base_spec.rb +45 -3
- 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 +150 -31
- data/spec/lib/roo/strict_spec.rb +43 -0
- data/spec/lib/roo/utils_spec.rb +25 -3
- data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
- data/spec/lib/roo_spec.rb +0 -0
- data/spec/spec_helper.rb +1 -1
- data/test/excelx/cell/test_attr_reader_default.rb +72 -0
- data/test/excelx/cell/test_base.rb +5 -0
- data/test/excelx/cell/test_datetime.rb +6 -6
- data/test/excelx/cell/test_empty.rb +11 -0
- data/test/excelx/cell/test_number.rb +9 -0
- data/test/excelx/cell/test_string.rb +20 -0
- data/test/excelx/cell/test_time.rb +4 -4
- data/test/excelx/test_coordinate.rb +51 -0
- data/test/formatters/test_csv.rb +19 -2
- data/test/formatters/test_xml.rb +13 -9
- data/test/helpers/test_accessing_files.rb +60 -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 +37 -1
- data/test/roo/test_excelx.rb +157 -13
- data/test/roo/test_open_office.rb +196 -33
- data/test/test_helper.rb +66 -22
- data/test/test_roo.rb +32 -881
- metadata +32 -14
- data/.github/ISSUE_TEMPLATE +0 -10
- data/Gemfile_ruby2 +0 -30
data/lib/roo/excelx/sheet_doc.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
require 'roo/excelx/extractor'
|
3
5
|
|
@@ -5,7 +7,7 @@ module Roo
|
|
5
7
|
class Excelx
|
6
8
|
class SheetDoc < Excelx::Extractor
|
7
9
|
extend Forwardable
|
8
|
-
delegate [:
|
10
|
+
delegate [:workbook] => :@shared
|
9
11
|
|
10
12
|
def initialize(path, relationships, shared, options = {})
|
11
13
|
super(path)
|
@@ -19,7 +21,12 @@ module Roo
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def hyperlinks(relationships)
|
22
|
-
|
24
|
+
# If you're sure you're not going to need this hyperlinks you can discard it
|
25
|
+
@hyperlinks ||= if @options[:no_hyperlinks] || !relationships.include_type?("hyperlink")
|
26
|
+
{}
|
27
|
+
else
|
28
|
+
extract_hyperlinks(relationships)
|
29
|
+
end
|
23
30
|
end
|
24
31
|
|
25
32
|
# Get the dimensions for the sheet.
|
@@ -39,13 +46,10 @@ module Roo
|
|
39
46
|
def each_cell(row_xml)
|
40
47
|
return [] unless row_xml
|
41
48
|
row_xml.children.each do |cell_element|
|
42
|
-
|
43
|
-
hyperlinks =
|
44
|
-
key = ::Roo::Utils.ref_to_key(cell_element['r'])
|
45
|
-
hyperlinks(@relationships)[key]
|
46
|
-
end
|
49
|
+
coordinate = ::Roo::Utils.extract_coordinate(cell_element["r"])
|
50
|
+
hyperlinks = hyperlinks(@relationships)[coordinate]
|
47
51
|
|
48
|
-
yield cell_from_xml(cell_element, hyperlinks)
|
52
|
+
yield cell_from_xml(cell_element, hyperlinks, coordinate)
|
49
53
|
end
|
50
54
|
end
|
51
55
|
|
@@ -53,13 +57,13 @@ module Roo
|
|
53
57
|
|
54
58
|
def cell_value_type(type, format)
|
55
59
|
case type
|
56
|
-
when 's'
|
60
|
+
when 's'
|
57
61
|
:shared
|
58
|
-
when 'b'
|
62
|
+
when 'b'
|
59
63
|
:boolean
|
60
|
-
when 'str'
|
64
|
+
when 'str'
|
61
65
|
:string
|
62
|
-
when 'inlineStr'
|
66
|
+
when 'inlineStr'
|
63
67
|
:inlinestr
|
64
68
|
else
|
65
69
|
Excelx::Format.to_type(format)
|
@@ -74,42 +78,58 @@ module Roo
|
|
74
78
|
# </c>
|
75
79
|
# hyperlink - a String for the hyperlink for the cell or nil when no
|
76
80
|
# hyperlink is present.
|
81
|
+
# coordinate - a Roo::Excelx::Coordinate for the coordinate for the cell
|
82
|
+
# or nil to extract coordinate from cell_xml.
|
83
|
+
# empty_cell - an Optional Boolean value.
|
77
84
|
#
|
78
85
|
# Examples
|
79
86
|
#
|
80
|
-
# cells_from_xml(<Nokogiri::XML::Element>, nil)
|
87
|
+
# cells_from_xml(<Nokogiri::XML::Element>, nil, nil)
|
81
88
|
# # => <Excelx::Cell::String>
|
82
89
|
#
|
83
90
|
# Returns a type of <Excelx::Cell>.
|
84
|
-
def cell_from_xml(cell_xml, hyperlink)
|
85
|
-
coordinate
|
86
|
-
|
91
|
+
def cell_from_xml(cell_xml, hyperlink, coordinate, empty_cell=true)
|
92
|
+
coordinate ||= ::Roo::Utils.extract_coordinate(cell_xml["r"])
|
93
|
+
cell_xml_children = cell_xml.children
|
94
|
+
return create_empty_cell(coordinate, empty_cell) if cell_xml_children.empty?
|
87
95
|
|
88
96
|
# NOTE: This is error prone, to_i will silently turn a nil into a 0.
|
89
97
|
# This works by coincidence because Format[0] is General.
|
90
|
-
style = cell_xml[
|
91
|
-
format = styles.style_format(style)
|
92
|
-
value_type = cell_value_type(cell_xml['t'], format)
|
98
|
+
style = cell_xml["s"].to_i
|
93
99
|
formula = nil
|
94
100
|
|
95
|
-
|
101
|
+
cell_xml_children.each do |cell|
|
96
102
|
case cell.name
|
97
103
|
when 'is'
|
98
|
-
|
99
|
-
|
100
|
-
|
104
|
+
content = +""
|
105
|
+
cell.children.each do |inline_str|
|
106
|
+
if inline_str.name == 't'
|
107
|
+
content << inline_str.content
|
108
|
+
end
|
109
|
+
end
|
110
|
+
unless content.empty?
|
111
|
+
return Excelx::Cell.cell_class(:string).new(content, formula, style, hyperlink, coordinate)
|
101
112
|
end
|
102
113
|
when 'f'
|
103
114
|
formula = cell.content
|
104
115
|
when 'v'
|
105
|
-
|
116
|
+
format = style_format(style)
|
117
|
+
value_type = cell_value_type(cell_xml["t"], format)
|
118
|
+
|
119
|
+
return create_cell_from_value(value_type, cell, formula, format, style, hyperlink, coordinate)
|
106
120
|
end
|
107
121
|
end
|
108
122
|
|
109
|
-
|
123
|
+
create_empty_cell(coordinate, empty_cell)
|
124
|
+
end
|
125
|
+
|
126
|
+
def create_empty_cell(coordinate, empty_cell)
|
127
|
+
if empty_cell
|
128
|
+
Excelx::Cell::Empty.new(coordinate)
|
129
|
+
end
|
110
130
|
end
|
111
131
|
|
112
|
-
def create_cell_from_value(value_type, cell, formula, format, style, hyperlink,
|
132
|
+
def create_cell_from_value(value_type, cell, formula, format, style, hyperlink, coordinate)
|
113
133
|
# NOTE: format.to_s can replace excelx_type as an argument for
|
114
134
|
# Cell::Time, Cell::DateTime, Cell::Date or Cell::Number, but
|
115
135
|
# it will break some brittle tests.
|
@@ -125,11 +145,12 @@ module Roo
|
|
125
145
|
# 3. formula
|
126
146
|
case value_type
|
127
147
|
when :shared
|
128
|
-
|
129
|
-
|
148
|
+
cell_content = cell.content.to_i
|
149
|
+
value = shared_strings.use_html?(cell_content) ? shared_strings.to_html[cell_content] : shared_strings[cell_content]
|
150
|
+
Excelx::Cell.cell_class(:string).new(value, formula, style, hyperlink, coordinate)
|
130
151
|
when :boolean, :string
|
131
152
|
value = cell.content
|
132
|
-
Excelx::Cell.
|
153
|
+
Excelx::Cell.cell_class(value_type).new(value, formula, style, hyperlink, coordinate)
|
133
154
|
when :time, :datetime
|
134
155
|
cell_content = cell.content.to_f
|
135
156
|
# NOTE: A date will be a whole number. A time will have be > 1. And
|
@@ -148,35 +169,35 @@ module Roo
|
|
148
169
|
else
|
149
170
|
:date
|
150
171
|
end
|
151
|
-
|
172
|
+
base_value = cell_type == :date ? base_date : base_timestamp
|
173
|
+
Excelx::Cell.cell_class(cell_type).new(cell_content, formula, excelx_type, style, hyperlink, base_value, coordinate)
|
152
174
|
when :date
|
153
|
-
Excelx::Cell.
|
175
|
+
Excelx::Cell.cell_class(:date).new(cell.content, formula, excelx_type, style, hyperlink, base_date, coordinate)
|
154
176
|
else
|
155
|
-
Excelx::Cell.
|
177
|
+
Excelx::Cell.cell_class(:number).new(cell.content, formula, excelx_type, style, hyperlink, coordinate)
|
156
178
|
end
|
157
179
|
end
|
158
180
|
|
159
|
-
def extract_coordinate(coordinate)
|
160
|
-
row, column = ::Roo::Utils.split_coordinate(coordinate)
|
161
|
-
|
162
|
-
Excelx::Coordinate.new(row, column)
|
163
|
-
end
|
164
|
-
|
165
181
|
def extract_hyperlinks(relationships)
|
166
182
|
return {} unless (hyperlinks = doc.xpath('/worksheet/hyperlinks/hyperlink'))
|
167
183
|
|
168
|
-
|
169
|
-
if
|
170
|
-
|
184
|
+
hyperlinks.each_with_object({}) do |hyperlink, hash|
|
185
|
+
if relationship = relationships[hyperlink['id']]
|
186
|
+
target_link = relationship['Target']
|
187
|
+
target_link += "##{hyperlink['location']}" if hyperlink['location']
|
188
|
+
|
189
|
+
Roo::Utils.coordinates_in_range(hyperlink["ref"].to_s) do |coord|
|
190
|
+
hash[coord] = target_link
|
191
|
+
end
|
171
192
|
end
|
172
|
-
end
|
193
|
+
end
|
173
194
|
end
|
174
195
|
|
175
196
|
def expand_merged_ranges(cells)
|
176
197
|
# Extract merged ranges from xml
|
177
198
|
merges = {}
|
178
199
|
doc.xpath('/worksheet/mergeCells/mergeCell').each do |mergecell_xml|
|
179
|
-
tl, br = mergecell_xml[
|
200
|
+
tl, br = mergecell_xml["ref"].split(/:/).map { |ref| ::Roo::Utils.ref_to_key(ref) }
|
180
201
|
for row in tl[0]..br[0] do
|
181
202
|
for col in tl[1]..br[1] do
|
182
203
|
next if row == tl[0] && col == tl[1]
|
@@ -191,10 +212,14 @@ module Roo
|
|
191
212
|
end
|
192
213
|
|
193
214
|
def extract_cells(relationships)
|
194
|
-
extracted_cells =
|
195
|
-
|
196
|
-
|
197
|
-
|
215
|
+
extracted_cells = {}
|
216
|
+
empty_cell = @options[:empty_cell]
|
217
|
+
|
218
|
+
doc.xpath('/worksheet/sheetData/row/c').each do |cell_xml|
|
219
|
+
coordinate = ::Roo::Utils.extract_coordinate(cell_xml["r"])
|
220
|
+
cell = cell_from_xml(cell_xml, hyperlinks(relationships)[coordinate], coordinate, empty_cell)
|
221
|
+
extracted_cells[coordinate] = cell if cell
|
222
|
+
end
|
198
223
|
|
199
224
|
expand_merged_ranges(extracted_cells) if @options[:expand_merged_ranges]
|
200
225
|
|
@@ -203,9 +228,25 @@ module Roo
|
|
203
228
|
|
204
229
|
def extract_dimensions
|
205
230
|
Roo::Utils.each_element(@path, 'dimension') do |dimension|
|
206
|
-
return dimension
|
231
|
+
return dimension["ref"]
|
207
232
|
end
|
208
233
|
end
|
234
|
+
|
235
|
+
def style_format(style)
|
236
|
+
@shared.styles.style_format(style)
|
237
|
+
end
|
238
|
+
|
239
|
+
def base_date
|
240
|
+
@shared.base_date
|
241
|
+
end
|
242
|
+
|
243
|
+
def base_timestamp
|
244
|
+
@shared.base_timestamp
|
245
|
+
end
|
246
|
+
|
247
|
+
def shared_strings
|
248
|
+
@shared.shared_strings
|
249
|
+
end
|
209
250
|
end
|
210
251
|
end
|
211
252
|
end
|
data/lib/roo/excelx/styles.rb
CHANGED
@@ -55,9 +55,9 @@ module Roo
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def extract_num_fmts
|
58
|
-
|
59
|
-
[num_fmt['numFmtId']
|
60
|
-
end
|
58
|
+
doc.xpath('//numFmt').each_with_object({}) do |num_fmt, hash|
|
59
|
+
hash[num_fmt['numFmtId']] = num_fmt['formatCode']
|
60
|
+
end
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
data/lib/roo/excelx/workbook.rb
CHANGED
@@ -29,13 +29,17 @@ module Roo
|
|
29
29
|
|
30
30
|
# aka labels
|
31
31
|
def defined_names
|
32
|
-
|
32
|
+
doc.xpath('//definedName').each_with_object({}) do |defined_name, hash|
|
33
33
|
# "Sheet1!$C$5"
|
34
34
|
sheet, coordinates = defined_name.text.split('!$', 2)
|
35
35
|
col, row = coordinates.split('$')
|
36
36
|
name = defined_name['name']
|
37
|
-
[name
|
38
|
-
end
|
37
|
+
hash[name] = Label.new(name, sheet, row, col)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def base_timestamp
|
42
|
+
@base_timestamp ||= base_date.to_datetime.to_time.to_i
|
39
43
|
end
|
40
44
|
|
41
45
|
def base_date
|
data/lib/roo/excelx.rb
CHANGED
@@ -24,8 +24,9 @@ module Roo
|
|
24
24
|
require 'roo/excelx/sheet_doc'
|
25
25
|
require 'roo/excelx/coordinate'
|
26
26
|
require 'roo/excelx/format'
|
27
|
+
require 'roo/excelx/images'
|
27
28
|
|
28
|
-
delegate [:styles, :workbook, :shared_strings, :rels_files, :sheet_files, :comments_files] => :@shared
|
29
|
+
delegate [:styles, :workbook, :shared_strings, :rels_files, :sheet_files, :comments_files, :image_rels, :image_files] => :@shared
|
29
30
|
ExceedsMaxError = Class.new(StandardError)
|
30
31
|
|
31
32
|
# initialization and opening of a spreadsheet file
|
@@ -39,7 +40,10 @@ module Roo
|
|
39
40
|
sheet_options = {}
|
40
41
|
sheet_options[:expand_merged_ranges] = (options[:expand_merged_ranges] || false)
|
41
42
|
sheet_options[:no_hyperlinks] = (options[:no_hyperlinks] || false)
|
43
|
+
sheet_options[:empty_cell] = (options[:empty_cell] || false)
|
44
|
+
shared_options = {}
|
42
45
|
|
46
|
+
shared_options[:disable_html_wrapper] = (options[:disable_html_wrapper] || false)
|
43
47
|
unless is_stream?(filename_or_stream)
|
44
48
|
file_type_check(filename_or_stream, %w[.xlsx .xlsm], 'an Excel 2007', file_warning, packed)
|
45
49
|
basename = find_basename(filename_or_stream)
|
@@ -52,7 +56,7 @@ module Roo
|
|
52
56
|
@tmpdir = self.class.make_tempdir(self, basename, options[:tmpdir_root])
|
53
57
|
ObjectSpace.define_finalizer(self, self.class.finalize(object_id))
|
54
58
|
|
55
|
-
@shared = Shared.new(@tmpdir)
|
59
|
+
@shared = Shared.new(@tmpdir, shared_options)
|
56
60
|
@filename = local_filename(filename_or_stream, @tmpdir, packed)
|
57
61
|
process_zipfile(@filename || filename_or_stream)
|
58
62
|
|
@@ -62,10 +66,10 @@ module Roo
|
|
62
66
|
end
|
63
67
|
end.compact
|
64
68
|
@sheets = []
|
65
|
-
@sheets_by_name =
|
66
|
-
|
67
|
-
[sheet_name
|
68
|
-
end
|
69
|
+
@sheets_by_name = {}
|
70
|
+
@sheet_names.each_with_index do |sheet_name, n|
|
71
|
+
@sheets_by_name[sheet_name] = @sheets[n] = Sheet.new(sheet_name, @shared, n, sheet_options)
|
72
|
+
end
|
69
73
|
|
70
74
|
if cell_max
|
71
75
|
cell_count = ::Roo::Utils.num_cells_in_range(sheet_for(options.delete(:sheet)).dimensions)
|
@@ -94,7 +98,12 @@ module Roo
|
|
94
98
|
def sheet_for(sheet)
|
95
99
|
sheet ||= default_sheet
|
96
100
|
validate_sheet!(sheet)
|
97
|
-
@sheets_by_name[sheet]
|
101
|
+
@sheets_by_name[sheet] || @sheets[sheet]
|
102
|
+
end
|
103
|
+
|
104
|
+
def images(sheet = nil)
|
105
|
+
images_names = sheet_for(sheet).images.map(&:last)
|
106
|
+
images_names.map { |iname| image_files.find { |ifile| ifile[iname] } }
|
98
107
|
end
|
99
108
|
|
100
109
|
# Returns the content of a spreadsheet-cell.
|
@@ -325,7 +334,7 @@ module Roo
|
|
325
334
|
|
326
335
|
wb.extract(path)
|
327
336
|
workbook_doc = Roo::Utils.load_xml(path).remove_namespaces!
|
328
|
-
workbook_doc.xpath('//sheet').map { |s| s
|
337
|
+
workbook_doc.xpath('//sheet').map { |s| s['id'] }
|
329
338
|
end
|
330
339
|
|
331
340
|
# Internal
|
@@ -349,17 +358,13 @@ module Roo
|
|
349
358
|
|
350
359
|
wb_rels.extract(path)
|
351
360
|
rels_doc = Roo::Utils.load_xml(path).remove_namespaces!
|
352
|
-
worksheet_type = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'
|
353
361
|
|
354
362
|
relationships = rels_doc.xpath('//Relationship').select do |relationship|
|
355
|
-
relationship
|
363
|
+
worksheet_types.include? relationship['Type']
|
356
364
|
end
|
357
365
|
|
358
|
-
relationships.
|
359
|
-
|
360
|
-
id = attributes['Id']
|
361
|
-
hash[id.value] = attributes['Target'].value
|
362
|
-
hash
|
366
|
+
relationships.each_with_object({}) do |relationship, hash|
|
367
|
+
hash[relationship['Id']] = relationship['Target']
|
363
368
|
end
|
364
369
|
end
|
365
370
|
|
@@ -376,6 +381,15 @@ module Roo
|
|
376
381
|
end
|
377
382
|
end
|
378
383
|
|
384
|
+
def extract_images(entries, tmpdir)
|
385
|
+
img_entries = entries.select { |e| e.name[/media\/image([0-9]+)/] }
|
386
|
+
img_entries.each do |entry|
|
387
|
+
path = "#{@tmpdir}/roo#{entry.name.gsub(/xl\/|\//, "_")}"
|
388
|
+
image_files << path
|
389
|
+
entry.extract(path)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
379
393
|
# Extracts all needed files from the zip file
|
380
394
|
def process_zipfile(zipfilename_or_stream)
|
381
395
|
@sheet_files = []
|
@@ -409,6 +423,7 @@ module Roo
|
|
409
423
|
sheet_ids = extract_worksheet_ids(entries, "#{@tmpdir}/roo_workbook.xml")
|
410
424
|
sheets = extract_worksheet_rels(entries, "#{@tmpdir}/roo_workbook.xml.rels")
|
411
425
|
extract_sheets_in_order(entries, sheet_ids, sheets, @tmpdir)
|
426
|
+
extract_images(entries, @tmpdir)
|
412
427
|
|
413
428
|
entries.each do |entry|
|
414
429
|
path =
|
@@ -435,6 +450,10 @@ module Roo
|
|
435
450
|
# drawings, etc.
|
436
451
|
nr = Regexp.last_match[1].to_i
|
437
452
|
rels_files[nr - 1] = "#{@tmpdir}/roo_rels#{nr}"
|
453
|
+
when /drawing([0-9]+).xml.rels$/
|
454
|
+
# Extracting drawing relationships to make images lists for each sheet
|
455
|
+
nr = Regexp.last_match[1].to_i
|
456
|
+
image_rels[nr - 1] = "#{@tmpdir}/roo_image_rels#{nr}"
|
438
457
|
end
|
439
458
|
|
440
459
|
entry.extract(path) if path
|
@@ -442,7 +461,14 @@ module Roo
|
|
442
461
|
end
|
443
462
|
|
444
463
|
def safe_send(object, method, *args)
|
445
|
-
object.send(method, *args) if object
|
464
|
+
object.send(method, *args) if object&.respond_to?(method)
|
465
|
+
end
|
466
|
+
|
467
|
+
def worksheet_types
|
468
|
+
[
|
469
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet', # OOXML Transitional
|
470
|
+
'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet' # OOXML Strict
|
471
|
+
]
|
446
472
|
end
|
447
473
|
end
|
448
474
|
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,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'date'
|
2
4
|
require 'nokogiri'
|
3
5
|
require 'cgi'
|
@@ -11,9 +13,9 @@ module Roo
|
|
11
13
|
class OpenOffice < Roo::Base
|
12
14
|
extend Roo::Tempdir
|
13
15
|
|
14
|
-
ERROR_MISSING_CONTENT_XML = 'file missing required content.xml'
|
15
|
-
XPATH_FIND_TABLE_STYLES = "//*[local-name()='automatic-styles']"
|
16
|
-
XPATH_LOCAL_NAME_TABLE = "//*[local-name()='table']"
|
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']"
|
17
19
|
|
18
20
|
# initialization and opening of a spreadsheet file
|
19
21
|
# values for packed: :zip
|
@@ -561,7 +563,7 @@ module Roo
|
|
561
563
|
end
|
562
564
|
|
563
565
|
def read_labels
|
564
|
-
@label ||=
|
566
|
+
@label ||= doc.xpath('//table:named-range').each_with_object({}) do |ne, hash|
|
565
567
|
#-
|
566
568
|
# $Sheet1.$C$5
|
567
569
|
#+
|
@@ -569,8 +571,8 @@ module Roo
|
|
569
571
|
sheetname, coords = attribute(ne, 'cell-range-address').to_s.split('.$')
|
570
572
|
col, row = coords.split('$')
|
571
573
|
sheetname = sheetname[1..-1] if sheetname[0, 1] == '$'
|
572
|
-
[name
|
573
|
-
end
|
574
|
+
hash[name] = [sheetname, row, col]
|
575
|
+
end
|
574
576
|
end
|
575
577
|
|
576
578
|
def read_styles(style_elements)
|
data/lib/roo/spreadsheet.rb
CHANGED
@@ -24,7 +24,7 @@ module Roo
|
|
24
24
|
options[:file_warning] = :ignore
|
25
25
|
extension.tr('.', '').downcase.to_sym
|
26
26
|
else
|
27
|
-
res = ::File.extname((path =~ /\A#{::URI.
|
27
|
+
res = ::File.extname((path =~ /\A#{::URI::DEFAULT_PARSER.make_regexp}\z/) ? ::URI.parse(::URI.encode(path)).path : path)
|
28
28
|
res.tr('.', '').downcase.to_sym
|
29
29
|
end
|
30
30
|
end
|
data/lib/roo/utils.rb
CHANGED
@@ -1,35 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Roo
|
2
4
|
module Utils
|
3
5
|
extend self
|
4
6
|
|
5
7
|
LETTERS = ('A'..'Z').to_a
|
6
8
|
|
7
|
-
def
|
8
|
-
|
9
|
+
def extract_coordinate(s)
|
10
|
+
num = letter_num = 0
|
11
|
+
num_only = false
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
15
24
|
end
|
25
|
+
fail ArgumentError if letter_num == 0 || !num_only
|
26
|
+
|
27
|
+
Excelx::Coordinate.new(num, letter_num)
|
16
28
|
end
|
17
29
|
|
18
|
-
alias_method :ref_to_key, :
|
30
|
+
alias_method :ref_to_key, :extract_coordinate
|
19
31
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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]
|
28
42
|
end
|
29
43
|
|
30
44
|
# convert a number to something like 'AB' (1 => 'A', 2 => 'B', ...)
|
31
45
|
def number_to_letter(num)
|
32
|
-
result = ""
|
46
|
+
result = +""
|
33
47
|
|
34
48
|
until num.zero?
|
35
49
|
num, index = (num - 1).divmod(26)
|
@@ -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