roo 2.6.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 (86) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +17 -0
  3. data/.github/issue_template.md +16 -0
  4. data/.github/pull_request_template.md +14 -0
  5. data/.rubocop.yml +186 -0
  6. data/.travis.yml +14 -11
  7. data/CHANGELOG.md +64 -2
  8. data/Gemfile +2 -4
  9. data/LICENSE +2 -0
  10. data/README.md +36 -10
  11. data/lib/roo/base.rb +82 -225
  12. data/lib/roo/constants.rb +5 -3
  13. data/lib/roo/csv.rb +100 -97
  14. data/lib/roo/excelx/cell/base.rb +26 -12
  15. data/lib/roo/excelx/cell/boolean.rb +9 -6
  16. data/lib/roo/excelx/cell/date.rb +7 -7
  17. data/lib/roo/excelx/cell/datetime.rb +50 -44
  18. data/lib/roo/excelx/cell/empty.rb +3 -2
  19. data/lib/roo/excelx/cell/number.rb +44 -47
  20. data/lib/roo/excelx/cell/string.rb +3 -3
  21. data/lib/roo/excelx/cell/time.rb +17 -16
  22. data/lib/roo/excelx/cell.rb +10 -6
  23. data/lib/roo/excelx/comments.rb +3 -3
  24. data/lib/roo/excelx/coordinate.rb +11 -4
  25. data/lib/roo/excelx/extractor.rb +21 -3
  26. data/lib/roo/excelx/format.rb +38 -31
  27. data/lib/roo/excelx/images.rb +26 -0
  28. data/lib/roo/excelx/relationships.rb +12 -4
  29. data/lib/roo/excelx/shared.rb +10 -3
  30. data/lib/roo/excelx/shared_strings.rb +9 -15
  31. data/lib/roo/excelx/sheet.rb +49 -10
  32. data/lib/roo/excelx/sheet_doc.rb +89 -48
  33. data/lib/roo/excelx/styles.rb +3 -3
  34. data/lib/roo/excelx/workbook.rb +7 -3
  35. data/lib/roo/excelx.rb +50 -19
  36. data/lib/roo/formatters/base.rb +15 -0
  37. data/lib/roo/formatters/csv.rb +84 -0
  38. data/lib/roo/formatters/matrix.rb +23 -0
  39. data/lib/roo/formatters/xml.rb +31 -0
  40. data/lib/roo/formatters/yaml.rb +40 -0
  41. data/lib/roo/helpers/default_attr_reader.rb +20 -0
  42. data/lib/roo/helpers/weak_instance_cache.rb +41 -0
  43. data/lib/roo/open_office.rb +17 -9
  44. data/lib/roo/spreadsheet.rb +1 -1
  45. data/lib/roo/tempdir.rb +5 -10
  46. data/lib/roo/utils.rb +70 -20
  47. data/lib/roo/version.rb +1 -1
  48. data/lib/roo.rb +4 -1
  49. data/roo.gemspec +14 -11
  50. data/spec/lib/roo/base_spec.rb +45 -3
  51. data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
  52. data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
  53. data/spec/lib/roo/excelx_spec.rb +150 -31
  54. data/spec/lib/roo/strict_spec.rb +43 -0
  55. data/spec/lib/roo/utils_spec.rb +25 -3
  56. data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
  57. data/spec/lib/roo_spec.rb +0 -0
  58. data/spec/spec_helper.rb +2 -6
  59. data/test/excelx/cell/test_attr_reader_default.rb +72 -0
  60. data/test/excelx/cell/test_base.rb +5 -0
  61. data/test/excelx/cell/test_datetime.rb +6 -6
  62. data/test/excelx/cell/test_empty.rb +11 -0
  63. data/test/excelx/cell/test_number.rb +9 -0
  64. data/test/excelx/cell/test_string.rb +20 -0
  65. data/test/excelx/cell/test_time.rb +5 -5
  66. data/test/excelx/test_coordinate.rb +51 -0
  67. data/test/formatters/test_csv.rb +136 -0
  68. data/test/formatters/test_matrix.rb +76 -0
  69. data/test/formatters/test_xml.rb +78 -0
  70. data/test/formatters/test_yaml.rb +20 -0
  71. data/test/helpers/test_accessing_files.rb +60 -0
  72. data/test/helpers/test_comments.rb +43 -0
  73. data/test/helpers/test_formulas.rb +9 -0
  74. data/test/helpers/test_labels.rb +103 -0
  75. data/test/helpers/test_sheets.rb +55 -0
  76. data/test/helpers/test_styles.rb +62 -0
  77. data/test/roo/test_base.rb +182 -0
  78. data/test/roo/test_csv.rb +88 -0
  79. data/test/roo/test_excelx.rb +330 -0
  80. data/test/roo/test_libre_office.rb +9 -0
  81. data/test/roo/test_open_office.rb +289 -0
  82. data/test/test_helper.rb +129 -14
  83. data/test/test_roo.rb +32 -1787
  84. metadata +81 -29
  85. data/.github/ISSUE_TEMPLATE +0 -10
  86. data/Gemfile_ruby2 +0 -29
@@ -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
@@ -4,12 +4,11 @@ require 'roo/link'
4
4
  require 'roo/tempdir'
5
5
  require 'roo/utils'
6
6
  require 'forwardable'
7
+ require 'set'
7
8
 
8
9
  module Roo
9
10
  class Excelx < Roo::Base
10
11
  extend Roo::Tempdir
11
-
12
- require 'set'
13
12
  extend Forwardable
14
13
 
15
14
  ERROR_VALUES = %w(#N/A #REF! #NAME? #DIV/0! #NULL! #VALUE! #NUM!).to_set
@@ -25,8 +24,9 @@ module Roo
25
24
  require 'roo/excelx/sheet_doc'
26
25
  require 'roo/excelx/coordinate'
27
26
  require 'roo/excelx/format'
27
+ require 'roo/excelx/images'
28
28
 
29
- 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
30
30
  ExceedsMaxError = Class.new(StandardError)
31
31
 
32
32
  # initialization and opening of a spreadsheet file
@@ -40,14 +40,23 @@ module Roo
40
40
  sheet_options = {}
41
41
  sheet_options[:expand_merged_ranges] = (options[:expand_merged_ranges] || false)
42
42
  sheet_options[:no_hyperlinks] = (options[:no_hyperlinks] || false)
43
+ sheet_options[:empty_cell] = (options[:empty_cell] || false)
44
+ shared_options = {}
43
45
 
46
+ shared_options[:disable_html_wrapper] = (options[:disable_html_wrapper] || false)
44
47
  unless is_stream?(filename_or_stream)
45
48
  file_type_check(filename_or_stream, %w[.xlsx .xlsm], 'an Excel 2007', file_warning, packed)
46
49
  basename = find_basename(filename_or_stream)
47
50
  end
48
51
 
52
+ # NOTE: Create temp directory and allow Ruby to cleanup the temp directory
53
+ # when the object is garbage collected. Initially, the finalizer was
54
+ # created in the Roo::Tempdir module, but that led to a segfault
55
+ # when testing in Ruby 2.4.0.
49
56
  @tmpdir = self.class.make_tempdir(self, basename, options[:tmpdir_root])
50
- @shared = Shared.new(@tmpdir)
57
+ ObjectSpace.define_finalizer(self, self.class.finalize(object_id))
58
+
59
+ @shared = Shared.new(@tmpdir, shared_options)
51
60
  @filename = local_filename(filename_or_stream, @tmpdir, packed)
52
61
  process_zipfile(@filename || filename_or_stream)
53
62
 
@@ -57,10 +66,10 @@ module Roo
57
66
  end
58
67
  end.compact
59
68
  @sheets = []
60
- @sheets_by_name = Hash[@sheet_names.map.with_index do |sheet_name, n|
61
- @sheets[n] = Sheet.new(sheet_name, @shared, n, sheet_options)
62
- [sheet_name, @sheets[n]]
63
- 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
64
73
 
65
74
  if cell_max
66
75
  cell_count = ::Roo::Utils.num_cells_in_range(sheet_for(options.delete(:sheet)).dimensions)
@@ -89,7 +98,12 @@ module Roo
89
98
  def sheet_for(sheet)
90
99
  sheet ||= default_sheet
91
100
  validate_sheet!(sheet)
92
- @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] } }
93
107
  end
94
108
 
95
109
  # Returns the content of a spreadsheet-cell.
@@ -218,7 +232,7 @@ module Roo
218
232
  sheet = sheet_for(sheet)
219
233
  key = normalize(row, col)
220
234
  cell = sheet.cells[key]
221
- !cell || cell.empty? || (cell.type == :string && cell.value.empty?) ||
235
+ !cell || cell.empty? ||
222
236
  (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
223
237
  end
224
238
 
@@ -320,7 +334,7 @@ module Roo
320
334
 
321
335
  wb.extract(path)
322
336
  workbook_doc = Roo::Utils.load_xml(path).remove_namespaces!
323
- workbook_doc.xpath('//sheet').map { |s| s.attributes['id'].value }
337
+ workbook_doc.xpath('//sheet').map { |s| s['id'] }
324
338
  end
325
339
 
326
340
  # Internal
@@ -344,17 +358,13 @@ module Roo
344
358
 
345
359
  wb_rels.extract(path)
346
360
  rels_doc = Roo::Utils.load_xml(path).remove_namespaces!
347
- worksheet_type = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'
348
361
 
349
362
  relationships = rels_doc.xpath('//Relationship').select do |relationship|
350
- relationship.attributes['Type'].value == worksheet_type
363
+ worksheet_types.include? relationship['Type']
351
364
  end
352
365
 
353
- relationships.inject({}) do |hash, relationship|
354
- attributes = relationship.attributes
355
- id = attributes['Id']
356
- hash[id.value] = attributes['Target'].value
357
- hash
366
+ relationships.each_with_object({}) do |relationship, hash|
367
+ hash[relationship['Id']] = relationship['Target']
358
368
  end
359
369
  end
360
370
 
@@ -371,6 +381,15 @@ module Roo
371
381
  end
372
382
  end
373
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
+
374
393
  # Extracts all needed files from the zip file
375
394
  def process_zipfile(zipfilename_or_stream)
376
395
  @sheet_files = []
@@ -404,6 +423,7 @@ module Roo
404
423
  sheet_ids = extract_worksheet_ids(entries, "#{@tmpdir}/roo_workbook.xml")
405
424
  sheets = extract_worksheet_rels(entries, "#{@tmpdir}/roo_workbook.xml.rels")
406
425
  extract_sheets_in_order(entries, sheet_ids, sheets, @tmpdir)
426
+ extract_images(entries, @tmpdir)
407
427
 
408
428
  entries.each do |entry|
409
429
  path =
@@ -430,6 +450,10 @@ module Roo
430
450
  # drawings, etc.
431
451
  nr = Regexp.last_match[1].to_i
432
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}"
433
457
  end
434
458
 
435
459
  entry.extract(path) if path
@@ -437,7 +461,14 @@ module Roo
437
461
  end
438
462
 
439
463
  def safe_send(object, method, *args)
440
- 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
+ ]
441
472
  end
442
473
  end
443
474
  end
@@ -0,0 +1,15 @@
1
+ module Roo
2
+ module Formatters
3
+ module Base
4
+ # converts an integer value to a time string like '02:05:06'
5
+ def integer_to_timestring(content)
6
+ h = (content / 3600.0).floor
7
+ content -= h * 3600
8
+ m = (content / 60.0).floor
9
+ content -= m * 60
10
+ s = content
11
+ Kernel.format("%02d:%02d:%02d", h, m, s)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,84 @@
1
+ module Roo
2
+ module Formatters
3
+ module CSV
4
+ def to_csv(filename = nil, separator = ",", sheet = default_sheet)
5
+ if filename
6
+ File.open(filename, "w") do |file|
7
+ write_csv_content(file, sheet, separator)
8
+ end
9
+ true
10
+ else
11
+ sio = ::StringIO.new
12
+ write_csv_content(sio, sheet, separator)
13
+ sio.rewind
14
+ sio.read
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Write all cells to the csv file. File can be a filename or nil. If the
21
+ # file argument is nil the output goes to STDOUT
22
+ def write_csv_content(file = nil, sheet = nil, separator = ",")
23
+ file ||= STDOUT
24
+ return unless first_row(sheet) # The sheet is empty
25
+
26
+ 1.upto(last_row(sheet)) do |row|
27
+ 1.upto(last_column(sheet)) do |col|
28
+ # TODO: use CSV.generate_line
29
+ file.print(separator) if col > 1
30
+ file.print cell_to_csv(row, col, sheet)
31
+ end
32
+ file.print("\n")
33
+ end
34
+ end
35
+
36
+ # The content of a cell in the csv output
37
+ def cell_to_csv(row, col, sheet)
38
+ return "" if empty?(row, col, sheet)
39
+
40
+ onecell = cell(row, col, sheet)
41
+
42
+ case celltype(row, col, sheet)
43
+ when :string
44
+ %("#{onecell.gsub('"', '""')}") unless onecell.empty?
45
+ when :boolean
46
+ # TODO: this only works for excelx
47
+ onecell = self.sheet_for(sheet).cells[[row, col]].formatted_value
48
+ %("#{onecell.gsub('"', '""').downcase}")
49
+ when :float, :percentage
50
+ if onecell == onecell.to_i
51
+ onecell.to_i.to_s
52
+ else
53
+ onecell.to_s
54
+ end
55
+ when :formula
56
+ case onecell
57
+ when String
58
+ %("#{onecell.gsub('"', '""')}") unless onecell.empty?
59
+ when Integer
60
+ onecell.to_s
61
+ when Float
62
+ if onecell == onecell.to_i
63
+ onecell.to_i.to_s
64
+ else
65
+ onecell.to_s
66
+ end
67
+ when Date, DateTime, TrueClass, FalseClass
68
+ onecell.to_s
69
+ else
70
+ fail "unhandled onecell-class #{onecell.class}"
71
+ end
72
+ when :date, :datetime
73
+ onecell.to_s
74
+ when :time
75
+ integer_to_timestring(onecell)
76
+ when :link
77
+ %("#{onecell.url.gsub('"', '""')}")
78
+ else
79
+ fail "unhandled celltype #{celltype(row, col, sheet)}"
80
+ end || ""
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ module Roo
2
+ module Formatters
3
+ module Matrix
4
+ # returns a matrix object from the whole sheet or a rectangular area of a sheet
5
+ def to_matrix(from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet)
6
+ require 'matrix'
7
+
8
+ return ::Matrix.empty unless first_row
9
+
10
+ from_row ||= first_row(sheet)
11
+ to_row ||= last_row(sheet)
12
+ from_column ||= first_column(sheet)
13
+ to_column ||= last_column(sheet)
14
+
15
+ ::Matrix.rows(from_row.upto(to_row).map do |row|
16
+ from_column.upto(to_column).map do |col|
17
+ cell(row, col, sheet)
18
+ end
19
+ end)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # returns an XML representation of all sheets of a spreadsheet file
2
+ module Roo
3
+ module Formatters
4
+ module XML
5
+ def to_xml
6
+ Nokogiri::XML::Builder.new do |xml|
7
+ xml.spreadsheet do
8
+ sheets.each do |sheet|
9
+ self.default_sheet = sheet
10
+ xml.sheet(name: sheet) do |x|
11
+ if first_row && last_row && first_column && last_column
12
+ # sonst gibt es Fehler bei leeren Blaettern
13
+ first_row.upto(last_row) do |row|
14
+ first_column.upto(last_column) do |col|
15
+ next if empty?(row, col)
16
+
17
+ x.cell(cell(row, col),
18
+ row: row,
19
+ column: col,
20
+ type: celltype(row, col))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end.to_xml
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ module Roo
2
+ module Formatters
3
+ module YAML
4
+ # returns a rectangular area (default: all cells) as yaml-output
5
+ # you can add additional attributes with the prefix parameter like:
6
+ # oo.to_yaml({"file"=>"flightdata_2007-06-26", "sheet" => "1"})
7
+ def to_yaml(prefix = {}, from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet)
8
+ # return an empty string if there is no first_row, i.e. the sheet is empty
9
+ return "" unless first_row
10
+
11
+ from_row ||= first_row(sheet)
12
+ to_row ||= last_row(sheet)
13
+ from_column ||= first_column(sheet)
14
+ to_column ||= last_column(sheet)
15
+
16
+ result = "--- \n"
17
+ from_row.upto(to_row) do |row|
18
+ from_column.upto(to_column) do |col|
19
+ next if empty?(row, col, sheet)
20
+
21
+ result << "cell_#{row}_#{col}: \n"
22
+ prefix.each do|k, v|
23
+ result << " #{k}: #{v} \n"
24
+ end
25
+ result << " row: #{row} \n"
26
+ result << " col: #{col} \n"
27
+ result << " celltype: #{celltype(row, col, sheet)} \n"
28
+ value = cell(row, col, sheet)
29
+ if celltype(row, col, sheet) == :time
30
+ value = integer_to_timestring(value)
31
+ end
32
+ result << " value: #{value} \n"
33
+ end
34
+ end
35
+
36
+ result
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ module Helpers
5
+ module DefaultAttrReader
6
+ def attr_reader_with_default(attr_hash)
7
+ attr_hash.each do |attr_name, default_value|
8
+ instance_variable = :"@#{attr_name}"
9
+ define_method attr_name do
10
+ if instance_variable_defined? instance_variable
11
+ instance_variable_get instance_variable
12
+ else
13
+ default_value
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end