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.
Files changed (75) hide show
  1. checksums.yaml +5 -5
  2. data/.github/issue_template.md +16 -0
  3. data/.github/pull_request_template.md +14 -0
  4. data/.rubocop.yml +186 -0
  5. data/.travis.yml +12 -7
  6. data/CHANGELOG.md +53 -2
  7. data/LICENSE +2 -0
  8. data/README.md +29 -13
  9. data/lib/roo/base.rb +69 -61
  10. data/lib/roo/constants.rb +5 -3
  11. data/lib/roo/csv.rb +20 -12
  12. data/lib/roo/excelx/cell/base.rb +26 -12
  13. data/lib/roo/excelx/cell/boolean.rb +9 -6
  14. data/lib/roo/excelx/cell/date.rb +7 -7
  15. data/lib/roo/excelx/cell/datetime.rb +14 -18
  16. data/lib/roo/excelx/cell/empty.rb +3 -2
  17. data/lib/roo/excelx/cell/number.rb +35 -34
  18. data/lib/roo/excelx/cell/string.rb +3 -3
  19. data/lib/roo/excelx/cell/time.rb +4 -3
  20. data/lib/roo/excelx/cell.rb +10 -6
  21. data/lib/roo/excelx/comments.rb +3 -3
  22. data/lib/roo/excelx/coordinate.rb +11 -4
  23. data/lib/roo/excelx/extractor.rb +21 -3
  24. data/lib/roo/excelx/format.rb +38 -31
  25. data/lib/roo/excelx/images.rb +26 -0
  26. data/lib/roo/excelx/relationships.rb +12 -4
  27. data/lib/roo/excelx/shared.rb +10 -3
  28. data/lib/roo/excelx/shared_strings.rb +9 -15
  29. data/lib/roo/excelx/sheet.rb +49 -10
  30. data/lib/roo/excelx/sheet_doc.rb +89 -48
  31. data/lib/roo/excelx/styles.rb +3 -3
  32. data/lib/roo/excelx/workbook.rb +7 -3
  33. data/lib/roo/excelx.rb +42 -16
  34. data/lib/roo/helpers/default_attr_reader.rb +20 -0
  35. data/lib/roo/helpers/weak_instance_cache.rb +41 -0
  36. data/lib/roo/open_office.rb +8 -6
  37. data/lib/roo/spreadsheet.rb +1 -1
  38. data/lib/roo/utils.rb +70 -20
  39. data/lib/roo/version.rb +1 -1
  40. data/lib/roo.rb +4 -1
  41. data/roo.gemspec +13 -11
  42. data/spec/lib/roo/base_spec.rb +45 -3
  43. data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
  44. data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
  45. data/spec/lib/roo/excelx_spec.rb +150 -31
  46. data/spec/lib/roo/strict_spec.rb +43 -0
  47. data/spec/lib/roo/utils_spec.rb +25 -3
  48. data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
  49. data/spec/lib/roo_spec.rb +0 -0
  50. data/spec/spec_helper.rb +1 -1
  51. data/test/excelx/cell/test_attr_reader_default.rb +72 -0
  52. data/test/excelx/cell/test_base.rb +5 -0
  53. data/test/excelx/cell/test_datetime.rb +6 -6
  54. data/test/excelx/cell/test_empty.rb +11 -0
  55. data/test/excelx/cell/test_number.rb +9 -0
  56. data/test/excelx/cell/test_string.rb +20 -0
  57. data/test/excelx/cell/test_time.rb +4 -4
  58. data/test/excelx/test_coordinate.rb +51 -0
  59. data/test/formatters/test_csv.rb +19 -2
  60. data/test/formatters/test_xml.rb +13 -9
  61. data/test/helpers/test_accessing_files.rb +60 -0
  62. data/test/helpers/test_comments.rb +43 -0
  63. data/test/helpers/test_formulas.rb +9 -0
  64. data/test/helpers/test_labels.rb +103 -0
  65. data/test/helpers/test_sheets.rb +55 -0
  66. data/test/helpers/test_styles.rb +62 -0
  67. data/test/roo/test_base.rb +182 -0
  68. data/test/roo/test_csv.rb +37 -1
  69. data/test/roo/test_excelx.rb +157 -13
  70. data/test/roo/test_open_office.rb +196 -33
  71. data/test/test_helper.rb +66 -22
  72. data/test/test_roo.rb +32 -881
  73. metadata +32 -14
  74. data/.github/ISSUE_TEMPLATE +0 -10
  75. data/Gemfile_ruby2 +0 -30
@@ -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 [:styles, :workbook, :shared_strings, :base_date] => :@shared
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
- @hyperlinks ||= extract_hyperlinks(relationships)
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
- # If you're sure you're not going to need this hyperlinks you can discard it
43
- hyperlinks = unless @options[:no_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'.freeze
60
+ when 's'
57
61
  :shared
58
- when 'b'.freeze
62
+ when 'b'
59
63
  :boolean
60
- when 'str'.freeze
64
+ when 'str'
61
65
  :string
62
- when 'inlineStr'.freeze
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 = extract_coordinate(cell_xml['r'])
86
- return Excelx::Cell::Empty.new(coordinate) if cell_xml.children.empty?
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['s'].to_i
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
- cell_xml.children.each do |cell|
101
+ cell_xml_children.each do |cell|
96
102
  case cell.name
97
103
  when 'is'
98
- content_arr = cell.search('t').map(&:content)
99
- unless content_arr.empty?
100
- return Excelx::Cell.create_cell(:string, content_arr.join(''), formula, style, hyperlink, coordinate)
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
- return create_cell_from_value(value_type, cell, formula, format, style, hyperlink, base_date, coordinate)
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
- Excelx::Cell::Empty.new(coordinate)
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, base_date, coordinate)
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
- value = shared_strings.use_html?(cell.content.to_i) ? shared_strings.to_html[cell.content.to_i] : shared_strings[cell.content.to_i]
129
- Excelx::Cell.create_cell(:string, value, formula, style, hyperlink, coordinate)
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.create_cell(value_type, value, formula, style, hyperlink, coordinate)
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
- Excelx::Cell.create_cell(cell_type, cell.content, formula, excelx_type, style, hyperlink, base_date, coordinate)
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.create_cell(value_type, cell.content, formula, excelx_type, style, hyperlink, base_date, coordinate)
175
+ Excelx::Cell.cell_class(:date).new(cell.content, formula, excelx_type, style, hyperlink, base_date, coordinate)
154
176
  else
155
- Excelx::Cell.create_cell(:number, cell.content, formula, excelx_type, style, hyperlink, coordinate)
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
- Hash[hyperlinks.map do |hyperlink|
169
- if hyperlink.attribute('id') && (relationship = relationships[hyperlink.attribute('id').text])
170
- [::Roo::Utils.ref_to_key(hyperlink.attributes['ref'].to_s), relationship.attribute('Target').text]
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.compact]
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['ref'].split(/:/).map { |ref| ::Roo::Utils.ref_to_key(ref) }
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 = Hash[doc.xpath('/worksheet/sheetData/row/c').map do |cell_xml|
195
- key = ::Roo::Utils.ref_to_key(cell_xml['r'])
196
- [key, cell_from_xml(cell_xml, hyperlinks(relationships)[key])]
197
- end]
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.attributes['ref'].value
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
@@ -55,9 +55,9 @@ module Roo
55
55
  end
56
56
 
57
57
  def extract_num_fmts
58
- Hash[doc.xpath('//numFmt').map do |num_fmt|
59
- [num_fmt['numFmtId'], num_fmt['formatCode']]
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
@@ -29,13 +29,17 @@ module Roo
29
29
 
30
30
  # aka labels
31
31
  def defined_names
32
- Hash[doc.xpath('//definedName').map do |defined_name|
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, Label.new(name, sheet, row, col)]
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 = Hash[@sheet_names.map.with_index do |sheet_name, n|
66
- @sheets[n] = Sheet.new(sheet_name, @shared, n, sheet_options)
67
- [sheet_name, @sheets[n]]
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.attributes['id'].value }
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.attributes['Type'].value == worksheet_type
363
+ worksheet_types.include? relationship['Type']
356
364
  end
357
365
 
358
- relationships.inject({}) do |hash, relationship|
359
- attributes = relationship.attributes
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 && object.respond_to?(method)
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
@@ -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'.freeze
15
- XPATH_FIND_TABLE_STYLES = "//*[local-name()='automatic-styles']".freeze
16
- XPATH_LOCAL_NAME_TABLE = "//*[local-name()='table']".freeze
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 ||= Hash[doc.xpath('//table:named-range').map do |ne|
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, [sheetname, row, col]]
573
- end]
574
+ hash[name] = [sheetname, row, col]
575
+ end
574
576
  end
575
577
 
576
578
  def read_styles(style_elements)
@@ -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.regexp}\z/) ? ::URI.parse(::URI.encode(path)).path : path)
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 split_coordinate(str)
8
- @split_coordinate ||= {}
9
+ def extract_coordinate(s)
10
+ num = letter_num = 0
11
+ num_only = false
9
12
 
10
- @split_coordinate[str] ||= begin
11
- letter, number = split_coord(str)
12
- x = letter_to_number(letter)
13
- y = number
14
- [y, x]
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, :split_coordinate
30
+ alias_method :ref_to_key, :extract_coordinate
19
31
 
20
- def split_coord(s)
21
- if s =~ /([a-zA-Z]+)([0-9]+)/
22
- letter = Regexp.last_match[1]
23
- number = Regexp.last_match[2].to_i
24
- else
25
- fail ArgumentError
26
- end
27
- [letter, number]
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 = split_coordinate(cells[0])
60
- x2, y2 = split_coordinate(cells[1])
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 && Array(elements).include?(node.name)
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
@@ -1,3 +1,3 @@
1
1
  module Roo
2
- VERSION = "2.7.0"
2
+ VERSION = "2.8.3"
3
3
  end