roo 2.7.0 → 2.8.3

Sign up to get free protection for your applications and to get access to all the features.
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