roo 2.7.0 → 2.8.3
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/.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