roo-andyw8 2.0.0

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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.simplecov +4 -0
  4. data/.travis.yml +13 -0
  5. data/CHANGELOG +438 -0
  6. data/Gemfile +24 -0
  7. data/Guardfile +24 -0
  8. data/LICENSE +22 -0
  9. data/README.md +121 -0
  10. data/Rakefile +23 -0
  11. data/examples/roo_soap_client.rb +50 -0
  12. data/examples/roo_soap_server.rb +26 -0
  13. data/examples/write_me.rb +31 -0
  14. data/lib/roo.rb +28 -0
  15. data/lib/roo/base.rb +717 -0
  16. data/lib/roo/csv.rb +110 -0
  17. data/lib/roo/excelx.rb +540 -0
  18. data/lib/roo/excelx/comments.rb +23 -0
  19. data/lib/roo/excelx/extractor.rb +20 -0
  20. data/lib/roo/excelx/relationships.rb +26 -0
  21. data/lib/roo/excelx/shared_strings.rb +40 -0
  22. data/lib/roo/excelx/sheet_doc.rb +175 -0
  23. data/lib/roo/excelx/styles.rb +62 -0
  24. data/lib/roo/excelx/workbook.rb +59 -0
  25. data/lib/roo/font.rb +17 -0
  26. data/lib/roo/libre_office.rb +5 -0
  27. data/lib/roo/link.rb +15 -0
  28. data/lib/roo/open_office.rb +652 -0
  29. data/lib/roo/spreadsheet.rb +31 -0
  30. data/lib/roo/utils.rb +81 -0
  31. data/lib/roo/version.rb +3 -0
  32. data/roo.gemspec +27 -0
  33. data/scripts/txt2html +67 -0
  34. data/spec/fixtures/vcr_cassettes/google_drive.yml +165 -0
  35. data/spec/fixtures/vcr_cassettes/google_drive_access_token.yml +73 -0
  36. data/spec/fixtures/vcr_cassettes/google_drive_set.yml +857 -0
  37. data/spec/lib/roo/base_spec.rb +4 -0
  38. data/spec/lib/roo/csv_spec.rb +48 -0
  39. data/spec/lib/roo/excelx/format_spec.rb +51 -0
  40. data/spec/lib/roo/excelx_spec.rb +363 -0
  41. data/spec/lib/roo/libreoffice_spec.rb +13 -0
  42. data/spec/lib/roo/openoffice_spec.rb +15 -0
  43. data/spec/lib/roo/spreadsheet_spec.rb +88 -0
  44. data/spec/lib/roo/utils_spec.rb +105 -0
  45. data/spec/spec_helper.rb +9 -0
  46. data/test/all_ss.rb +11 -0
  47. data/test/files/1900_base.xlsx +0 -0
  48. data/test/files/1904_base.xlsx +0 -0
  49. data/test/files/Bibelbund.csv +3741 -0
  50. data/test/files/Bibelbund.ods +0 -0
  51. data/test/files/Bibelbund.xlsx +0 -0
  52. data/test/files/Bibelbund1.ods +0 -0
  53. data/test/files/Pfand_from_windows_phone.xlsx +0 -0
  54. data/test/files/advanced_header.ods +0 -0
  55. data/test/files/bbu.ods +0 -0
  56. data/test/files/bbu.xlsx +0 -0
  57. data/test/files/bode-v1.ods.zip +0 -0
  58. data/test/files/bode-v1.xls.zip +0 -0
  59. data/test/files/boolean.csv +2 -0
  60. data/test/files/boolean.ods +0 -0
  61. data/test/files/boolean.xlsx +0 -0
  62. data/test/files/borders.ods +0 -0
  63. data/test/files/borders.xlsx +0 -0
  64. data/test/files/bug-numbered-sheet-names.xlsx +0 -0
  65. data/test/files/comments.ods +0 -0
  66. data/test/files/comments.xlsx +0 -0
  67. data/test/files/csvtypes.csv +1 -0
  68. data/test/files/datetime.ods +0 -0
  69. data/test/files/datetime.xlsx +0 -0
  70. data/test/files/dreimalvier.ods +0 -0
  71. data/test/files/emptysheets.ods +0 -0
  72. data/test/files/emptysheets.xlsx +0 -0
  73. data/test/files/encrypted-letmein.ods +0 -0
  74. data/test/files/file_item_error.xlsx +0 -0
  75. data/test/files/formula.ods +0 -0
  76. data/test/files/formula.xlsx +0 -0
  77. data/test/files/formula_string_error.xlsx +0 -0
  78. data/test/files/html-escape.ods +0 -0
  79. data/test/files/link.csv +1 -0
  80. data/test/files/link.xlsx +0 -0
  81. data/test/files/matrix.ods +0 -0
  82. data/test/files/named_cells.ods +0 -0
  83. data/test/files/named_cells.xlsx +0 -0
  84. data/test/files/no_spreadsheet_file.txt +1 -0
  85. data/test/files/numbers-export.xlsx +0 -0
  86. data/test/files/numbers1.csv +18 -0
  87. data/test/files/numbers1.ods +0 -0
  88. data/test/files/numbers1.xlsx +0 -0
  89. data/test/files/numbers1withnull.xlsx +0 -0
  90. data/test/files/numeric-link.xlsx +0 -0
  91. data/test/files/only_one_sheet.ods +0 -0
  92. data/test/files/only_one_sheet.xlsx +0 -0
  93. data/test/files/paragraph.ods +0 -0
  94. data/test/files/paragraph.xlsx +0 -0
  95. data/test/files/ric.ods +0 -0
  96. data/test/files/sheet1.xml +109 -0
  97. data/test/files/simple_spreadsheet.ods +0 -0
  98. data/test/files/simple_spreadsheet.xlsx +0 -0
  99. data/test/files/simple_spreadsheet_from_italo.ods +0 -0
  100. data/test/files/so_datetime.csv +8 -0
  101. data/test/files/style.ods +0 -0
  102. data/test/files/style.xlsx +0 -0
  103. data/test/files/time-test.csv +2 -0
  104. data/test/files/time-test.ods +0 -0
  105. data/test/files/time-test.xlsx +0 -0
  106. data/test/files/type_excel.ods +0 -0
  107. data/test/files/type_excel.xlsx +0 -0
  108. data/test/files/type_excelx.ods +0 -0
  109. data/test/files/type_openoffice.xlsx +0 -0
  110. data/test/files/whitespace.ods +0 -0
  111. data/test/files/whitespace.xlsx +0 -0
  112. data/test/test_generic_spreadsheet.rb +211 -0
  113. data/test/test_helper.rb +58 -0
  114. data/test/test_roo.rb +1977 -0
  115. metadata +318 -0
@@ -0,0 +1,110 @@
1
+ require 'csv'
2
+ require 'time'
3
+
4
+ # The CSV class can read csv files (must be separated with commas) which then
5
+ # can be handled like spreadsheets. This means you can access cells like A5
6
+ # within these files.
7
+ # The CSV class provides only string objects. If you want conversions to other
8
+ # types you have to do it yourself.
9
+ #
10
+ # You can pass options to the underlying CSV parse operation, via the
11
+ # :csv_options option.
12
+ #
13
+
14
+ class Roo::CSV < Roo::Base
15
+
16
+ attr_reader :filename
17
+
18
+ # Returns an array with the names of the sheets. In CSV class there is only
19
+ # one dummy sheet, because a csv file cannot have more than one sheet.
20
+ def sheets
21
+ ['default']
22
+ end
23
+
24
+ def cell(row, col, sheet=nil)
25
+ sheet ||= default_sheet
26
+ read_cells(sheet)
27
+ @cell[normalize(row,col)]
28
+ end
29
+
30
+ def celltype(row, col, sheet=nil)
31
+ sheet ||= default_sheet
32
+ read_cells(sheet)
33
+ @cell_type[normalize(row,col)]
34
+ end
35
+
36
+ def cell_postprocessing(row,col,value)
37
+ value
38
+ end
39
+
40
+ def csv_options
41
+ @options[:csv_options] || {}
42
+ end
43
+
44
+ private
45
+
46
+ TYPE_MAP = {
47
+ String => :string,
48
+ Float => :float,
49
+ Date => :date,
50
+ DateTime => :datetime,
51
+ }
52
+
53
+ def celltype_class(value)
54
+ TYPE_MAP[value.class]
55
+ end
56
+
57
+ def each_row(options, &block)
58
+ if uri?(filename)
59
+ make_tmpdir do |tmpdir|
60
+ tmp_filename = download_uri(filename, tmpdir)
61
+ CSV.foreach(tmp_filename, options, &block)
62
+ end
63
+ else
64
+ CSV.foreach(filename, options, &block)
65
+ end
66
+ end
67
+
68
+ def read_cells(sheet = default_sheet)
69
+ sheet ||= default_sheet
70
+ return if @cells_read[sheet]
71
+ @first_row[sheet] = 1
72
+ @last_row[sheet] = 0
73
+ @first_column[sheet] = 1
74
+ @last_column[sheet] = 1
75
+ rownum = 1
76
+ each_row csv_options do |row|
77
+ row.each_with_index do |elem,i|
78
+ @cell[[rownum,i+1]] = cell_postprocessing rownum,i+1, elem
79
+ @cell_type[[rownum,i+1]] = celltype_class @cell[[rownum,i+1]]
80
+ if i+1 > @last_column[sheet]
81
+ @last_column[sheet] += 1
82
+ end
83
+ end
84
+ rownum += 1
85
+ @last_row[sheet] += 1
86
+ end
87
+ @cells_read[sheet] = true
88
+ #-- adjust @first_row if neccessary
89
+ while !row(@first_row[sheet]).any? and @first_row[sheet] < @last_row[sheet]
90
+ @first_row[sheet] += 1
91
+ end
92
+ #-- adjust @last_row if neccessary
93
+ while !row(@last_row[sheet]).any? and @last_row[sheet] and
94
+ @last_row[sheet] > @first_row[sheet]
95
+ @last_row[sheet] -= 1
96
+ end
97
+ #-- adjust @first_column if neccessary
98
+ while !column(@first_column[sheet]).any? and
99
+ @first_column[sheet] and
100
+ @first_column[sheet] < @last_column[sheet]
101
+ @first_column[sheet] += 1
102
+ end
103
+ #-- adjust @last_column if neccessary
104
+ while !column(@last_column[sheet]).any? and
105
+ @last_column[sheet] and
106
+ @last_column[sheet] > @first_column[sheet]
107
+ @last_column[sheet] -= 1
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,540 @@
1
+ require 'date'
2
+ require 'nokogiri'
3
+ require 'roo/link'
4
+ require 'roo/utils'
5
+ require 'zip/filesystem'
6
+
7
+ class Roo::Excelx < Roo::Base
8
+ autoload :Workbook, 'roo/excelx/workbook'
9
+ autoload :SharedStrings, 'roo/excelx/shared_strings'
10
+ autoload :Styles, 'roo/excelx/styles'
11
+
12
+ autoload :Relationships, 'roo/excelx/relationships'
13
+ autoload :Comments, 'roo/excelx/comments'
14
+ autoload :SheetDoc, 'roo/excelx/sheet_doc'
15
+
16
+ module Format
17
+ EXCEPTIONAL_FORMATS = {
18
+ 'h:mm am/pm' => :date,
19
+ 'h:mm:ss am/pm' => :date,
20
+ }
21
+
22
+ STANDARD_FORMATS = {
23
+ 0 => 'General',
24
+ 1 => '0',
25
+ 2 => '0.00',
26
+ 3 => '#,##0',
27
+ 4 => '#,##0.00',
28
+ 9 => '0%',
29
+ 10 => '0.00%',
30
+ 11 => '0.00E+00',
31
+ 12 => '# ?/?',
32
+ 13 => '# ??/??',
33
+ 14 => 'mm-dd-yy',
34
+ 15 => 'd-mmm-yy',
35
+ 16 => 'd-mmm',
36
+ 17 => 'mmm-yy',
37
+ 18 => 'h:mm AM/PM',
38
+ 19 => 'h:mm:ss AM/PM',
39
+ 20 => 'h:mm',
40
+ 21 => 'h:mm:ss',
41
+ 22 => 'm/d/yy h:mm',
42
+ 37 => '#,##0 ;(#,##0)',
43
+ 38 => '#,##0 ;[Red](#,##0)',
44
+ 39 => '#,##0.00;(#,##0.00)',
45
+ 40 => '#,##0.00;[Red](#,##0.00)',
46
+ 45 => 'mm:ss',
47
+ 46 => '[h]:mm:ss',
48
+ 47 => 'mmss.0',
49
+ 48 => '##0.0E+0',
50
+ 49 => '@',
51
+ }
52
+
53
+ def to_type(format)
54
+ format = format.to_s.downcase
55
+ if type = EXCEPTIONAL_FORMATS[format]
56
+ type
57
+ elsif format.include?('#')
58
+ :float
59
+ elsif !format.match(/d+(?![\]])/).nil? || format.include?('y')
60
+ if format.include?('h') || format.include?('s')
61
+ :datetime
62
+ else
63
+ :date
64
+ end
65
+ elsif format.include?('h') || format.include?('s')
66
+ :time
67
+ elsif format.include?('%')
68
+ :percentage
69
+ else
70
+ :float
71
+ end
72
+ end
73
+
74
+ module_function :to_type
75
+ end
76
+
77
+ class Cell
78
+ attr_reader :type, :formula, :value, :excelx_type, :excelx_value, :style, :hyperlink, :coordinate
79
+
80
+ def initialize(value, type, formula, excelx_type, excelx_value, style, hyperlink, base_date, coordinate)
81
+ @type = type
82
+ @formula = formula
83
+ @base_date = base_date if [:date, :datetime].include?(@type)
84
+ @excelx_type = excelx_type
85
+ @excelx_value = excelx_value
86
+ @style = style
87
+ @value = type_cast_value(value)
88
+ @value = Roo::Link.new(hyperlink, @value.to_s) if hyperlink
89
+ @coordinate = coordinate
90
+ end
91
+
92
+ def type
93
+ if @formula
94
+ :formula
95
+ elsif @value.is_a?(Roo::Link)
96
+ :link
97
+ else
98
+ @type
99
+ end
100
+ end
101
+
102
+ class Coordinate
103
+ attr_accessor :row, :column
104
+
105
+ def initialize(row, column)
106
+ @row, @column = row, column
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def type_cast_value(value)
113
+ case @type
114
+ when :float, :percentage
115
+ value.to_f
116
+ when :date
117
+ yyyy,mm,dd = (@base_date+value.to_i).strftime("%Y-%m-%d").split('-')
118
+ Date.new(yyyy.to_i,mm.to_i,dd.to_i)
119
+ when :datetime
120
+ create_datetime_from((@base_date+value.to_f.round(6)).strftime("%Y-%m-%d %H:%M:%S.%N"))
121
+ when :time
122
+ value.to_f*(24*60*60)
123
+ when :string
124
+ value
125
+ else
126
+ value
127
+ end
128
+ end
129
+
130
+ def create_datetime_from(datetime_string)
131
+ date_part,time_part = round_time_from(datetime_string).split(' ')
132
+ yyyy,mm,dd = date_part.split('-')
133
+ hh,mi,ss = time_part.split(':')
134
+ DateTime.civil(yyyy.to_i,mm.to_i,dd.to_i,hh.to_i,mi.to_i,ss.to_i)
135
+ end
136
+
137
+ def round_time_from(datetime_string)
138
+ date_part,time_part = datetime_string.split(' ')
139
+ yyyy,mm,dd = date_part.split('-')
140
+ hh,mi,ss = time_part.split(':')
141
+ Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0).strftime("%Y-%m-%d %H:%M:%S")
142
+ end
143
+ end
144
+
145
+ class Sheet
146
+ def initialize(name, rels_path, sheet_path, comments_path, styles, shared_strings, workbook)
147
+ @name = name
148
+ @rels = Relationships.new(rels_path)
149
+ @comments = Comments.new(comments_path)
150
+ @styles = styles
151
+ @sheet = SheetDoc.new(sheet_path, @rels, @styles, shared_strings, workbook)
152
+ end
153
+
154
+ def cells
155
+ @cells ||= @sheet.cells(@rels)
156
+ end
157
+
158
+ def present_cells
159
+ @present_cells ||= cells.select {|key, cell| cell && cell.value }
160
+ end
161
+
162
+ # Yield each row as array of Excelx::Cell objects
163
+ # accepts options max_rows (int) (offset by 1 for header)
164
+ # and pad_cells (boolean)
165
+ def each_row(options = {}, &block)
166
+ row_count = 0
167
+ @sheet.each_row_streaming do |row|
168
+ break if options[:max_rows] && row_count == options[:max_rows] + 1
169
+ block.call(cells_for_row_element(row, options)) if block_given?
170
+ row_count += 1
171
+ end
172
+ end
173
+
174
+ def row(row_number)
175
+ first_column.upto(last_column).map do |col|
176
+ cells[[row_number,col]]
177
+ end.map {|cell| cell && cell.value }
178
+ end
179
+
180
+ def column(col_number)
181
+ first_row.upto(last_row).map do |row|
182
+ cells[[row,col_number]]
183
+ end.map {|cell| cell && cell.value }
184
+ end
185
+
186
+ # returns the number of the first non-empty row
187
+ def first_row
188
+ @first_row ||= present_cells.keys.map {|row, col| row }.min
189
+ end
190
+
191
+ def last_row
192
+ @last_row ||= present_cells.keys.map {|row, col| row }.max
193
+ end
194
+
195
+ # returns the number of the first non-empty column
196
+ def first_column(sheet=nil)
197
+ @first_column ||= present_cells.keys.map {|row, col| col }.min
198
+ end
199
+
200
+ # returns the number of the last non-empty column
201
+ def last_column(sheet=nil)
202
+ @last_column ||= present_cells.keys.map {|row, col| col }.max
203
+ end
204
+
205
+ def excelx_format(key)
206
+ @styles.style_format(cells[key].style).to_s
207
+ end
208
+
209
+ def hyperlinks
210
+ @hyperlinks ||= @sheet.hyperlinks(@rels)
211
+ end
212
+
213
+ def comments
214
+ @comments.comments
215
+ end
216
+
217
+ def dimensions
218
+ @sheet.dimensions
219
+ end
220
+
221
+ private
222
+
223
+ # Take an xml row and return an array of Excelx::Cell objects
224
+ # optionally pad array to header width(assumed 1st row).
225
+ # takes option pad_cells (boolean) defaults false
226
+ def cells_for_row_element(row_element, options = {})
227
+ return [] unless row_element
228
+ cell_col = 0
229
+ cells = []
230
+ @sheet.each_cell(row_element) do |cell|
231
+ cells.concat(pad_cells(cell, cell_col)) if options[:pad_cells]
232
+ cells << cell
233
+ cell_col = cell.coordinate.column
234
+ end
235
+ cells
236
+ end
237
+
238
+ def pad_cells(cell, last_column)
239
+ pad = []
240
+ (cell.coordinate.column - 1 - last_column).times { pad << nil }
241
+ pad
242
+ end
243
+ end
244
+
245
+ ExceedsMaxError = Class.new(StandardError)
246
+
247
+ # initialization and opening of a spreadsheet file
248
+ # values for packed: :zip
249
+ # optional cell_max (int) parameter for early aborting attempts to parse
250
+ # enormous documents.
251
+ def initialize(filename, options = {})
252
+ packed = options[:packed]
253
+ file_warning = options.fetch(:file_warning, :error)
254
+ cell_max = options.delete(:cell_max)
255
+
256
+ file_type_check(filename,'.xlsx','an Excel-xlsx', file_warning, packed)
257
+
258
+ @tmpdir = make_tmpdir(filename.split('/').last, options[:tmpdir_root])
259
+ @filename = local_filename(filename, @tmpdir, packed)
260
+ @comments_files = []
261
+ @rels_files = []
262
+ process_zipfile(@tmpdir, @filename)
263
+
264
+ @sheet_names = workbook.sheets.map { |sheet| sheet['name'] }
265
+ @sheets = []
266
+ @sheets_by_name = Hash[@sheet_names.map.with_index do |sheet_name, n|
267
+ @sheets[n] = Sheet.new(sheet_name, @rels_files[n], @sheet_files[n], @comments_files[n], styles, shared_strings, workbook)
268
+ [sheet_name, @sheets[n]]
269
+ end]
270
+
271
+ if cell_max
272
+ cell_count = ::Roo::Utils.num_cells_in_range(sheet_for(options.delete(:sheet)).dimensions)
273
+ raise ExceedsMaxError.new("Excel file exceeds cell maximum: #{cell_count} > #{cell_max}") if cell_count > cell_max
274
+ end
275
+
276
+ super
277
+ end
278
+
279
+ def method_missing(method,*args)
280
+ if label = workbook.defined_names[method.to_s]
281
+ sheet_for(label.sheet).cells[label.key].value
282
+ else
283
+ # call super for methods like #a1
284
+ super
285
+ end
286
+ end
287
+
288
+ def sheets
289
+ @sheet_names
290
+ end
291
+
292
+ def sheet_for(sheet)
293
+ sheet ||= default_sheet
294
+ validate_sheet!(sheet)
295
+ @sheets_by_name[sheet]
296
+ end
297
+
298
+ # Returns the content of a spreadsheet-cell.
299
+ # (1,1) is the upper left corner.
300
+ # (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
301
+ # cell at the first line and first row.
302
+ def cell(row, col, sheet=nil)
303
+ key = normalize(row,col)
304
+ cell = sheet_for(sheet).cells[key]
305
+ cell.value if cell
306
+ end
307
+
308
+ def row(rownumber,sheet=nil)
309
+ sheet_for(sheet).row(rownumber)
310
+ end
311
+
312
+ # returns all values in this column as an array
313
+ # column numbers are 1,2,3,... like in the spreadsheet
314
+ def column(column_number,sheet=nil)
315
+ if column_number.is_a?(::String)
316
+ column_number = ::Roo::Utils.letter_to_number(column_number)
317
+ end
318
+ sheet_for(sheet).column(column_number)
319
+ end
320
+
321
+ # returns the number of the first non-empty row
322
+ def first_row(sheet=nil)
323
+ sheet_for(sheet).first_row
324
+ end
325
+
326
+ # returns the number of the last non-empty row
327
+ def last_row(sheet=nil)
328
+ sheet_for(sheet).last_row
329
+ end
330
+
331
+ # returns the number of the first non-empty column
332
+ def first_column(sheet=nil)
333
+ sheet_for(sheet).first_column
334
+ end
335
+
336
+ # returns the number of the last non-empty column
337
+ def last_column(sheet=nil)
338
+ sheet_for(sheet).last_column
339
+ end
340
+
341
+ # set a cell to a certain value
342
+ # (this will not be saved back to the spreadsheet file!)
343
+ def set(row,col,value, sheet = nil) #:nodoc:
344
+ key = normalize(row,col)
345
+ cell_type = cell_type_by_value(value)
346
+ sheet_for(sheet).cells[key] = Cell.new(value, cell_type, nil, cell_type, value, nil, nil, nil, Cell::Coordinate.new(row, col))
347
+ end
348
+
349
+
350
+ # Returns the formula at (row,col).
351
+ # Returns nil if there is no formula.
352
+ # The method #formula? checks if there is a formula.
353
+ def formula(row,col,sheet=nil)
354
+ key = normalize(row,col)
355
+ sheet_for(sheet).cells[key].formula
356
+ end
357
+
358
+ # Predicate methods really should return a boolean
359
+ # value. Hopefully no one was relying on the fact that this
360
+ # previously returned either nil/formula
361
+ def formula?(*args)
362
+ !!formula(*args)
363
+ end
364
+
365
+ # returns each formula in the selected sheet as an array of tuples in following format
366
+ # [[row, col, formula], [row, col, formula],...]
367
+ def formulas(sheet=nil)
368
+ sheet_for(sheet).cells.select {|_, cell| cell.formula }.map do |(x, y), cell|
369
+ [x, y, cell.formula]
370
+ end
371
+ end
372
+
373
+ # Given a cell, return the cell's style
374
+ def font(row, col, sheet=nil)
375
+ key = normalize(row,col)
376
+ styles.definitions[sheet_for(sheet).cells[key].style]
377
+ end
378
+
379
+ # returns the type of a cell:
380
+ # * :float
381
+ # * :string,
382
+ # * :date
383
+ # * :percentage
384
+ # * :formula
385
+ # * :time
386
+ # * :datetime
387
+ def celltype(row,col,sheet=nil)
388
+ key = normalize(row, col)
389
+ sheet_for(sheet).cells[key].type
390
+ end
391
+
392
+ # returns the internal type of an excel cell
393
+ # * :numeric_or_formula
394
+ # * :string
395
+ # Note: this is only available within the Excelx class
396
+ def excelx_type(row,col,sheet=nil)
397
+ key = normalize(row,col)
398
+ sheet_for(sheet).cells[key].excelx_type
399
+ end
400
+
401
+ # returns the internal value of an excelx cell
402
+ # Note: this is only available within the Excelx class
403
+ def excelx_value(row,col,sheet=nil)
404
+ key = normalize(row,col)
405
+ sheet_for(sheet).cells[key].excelx_value
406
+ end
407
+
408
+ # returns the internal format of an excel cell
409
+ def excelx_format(row,col,sheet=nil)
410
+ key = normalize(row,col)
411
+ sheet_for(sheet).excelx_format(key)
412
+ end
413
+
414
+ def empty?(row,col,sheet=nil)
415
+ sheet = sheet_for(sheet)
416
+ key = normalize(row,col)
417
+ cell = sheet.cells[key]
418
+ !cell || !cell.value || (cell.type == :string && cell.value.empty?) \
419
+ || (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
420
+ end
421
+
422
+ # shows the internal representation of all cells
423
+ # for debugging purposes
424
+ def to_s(sheet=nil)
425
+ sheet_for(sheet).cells.inspect
426
+ end
427
+
428
+ # returns the row,col values of the labelled cell
429
+ # (nil,nil) if label is not defined
430
+ def label(name)
431
+ labels = workbook.defined_names
432
+ if labels.empty? || !labels.key?(name)
433
+ [nil,nil,nil]
434
+ else
435
+ [labels[name].row,
436
+ labels[name].col,
437
+ labels[name].sheet]
438
+ end
439
+ end
440
+
441
+ # Returns an array which all labels. Each element is an array with
442
+ # [labelname, [row,col,sheetname]]
443
+ def labels
444
+ @labels ||= workbook.defined_names.map do |name, label|
445
+ [ name,
446
+ [ label.row,
447
+ label.col,
448
+ label.sheet,
449
+ ] ]
450
+ end
451
+ end
452
+
453
+ def hyperlink?(row,col,sheet=nil)
454
+ !!hyperlink(row, col, sheet)
455
+ end
456
+
457
+ # returns the hyperlink at (row/col)
458
+ # nil if there is no hyperlink
459
+ def hyperlink(row,col,sheet=nil)
460
+ key = normalize(row,col)
461
+ sheet_for(sheet).hyperlinks[key]
462
+ end
463
+
464
+ # returns the comment at (row/col)
465
+ # nil if there is no comment
466
+ def comment(row,col,sheet=nil)
467
+ key = normalize(row,col)
468
+ sheet_for(sheet).comments[key]
469
+ end
470
+
471
+ # true, if there is a comment
472
+ def comment?(row,col,sheet=nil)
473
+ !!comment(row,col,sheet)
474
+ end
475
+
476
+ def comments(sheet=nil)
477
+ sheet_for(sheet).comments.map do |(x, y), comment|
478
+ [x, y, comment]
479
+ end
480
+ end
481
+
482
+ # Yield an array of Excelx::Cell
483
+ # Takes options for sheet, pad_cells, and max_rows
484
+ def each_row_streaming(options={})
485
+ sheet_for(options.delete(:sheet)).each_row(options) { |row| yield row }
486
+ end
487
+
488
+ private
489
+
490
+ # Extracts all needed files from the zip file
491
+ def process_zipfile(tmpdir, zipfilename)
492
+ @sheet_files = []
493
+ Zip::File.foreach(zipfilename) do |entry|
494
+ path =
495
+ case entry.name.downcase
496
+ when /workbook.xml$/
497
+ "#{tmpdir}/roo_workbook.xml"
498
+ when /sharedstrings.xml$/
499
+ "#{tmpdir}/roo_sharedStrings.xml"
500
+ when /styles.xml$/
501
+ "#{tmpdir}/roo_styles.xml"
502
+ when /sheet.xml$/
503
+ path = "#{tmpdir}/roo_sheet"
504
+ @sheet_files.unshift(path)
505
+ path
506
+ when /sheet([0-9]+).xml$/
507
+ # Numbers 3.1 exports first sheet without sheet number. Such sheets
508
+ # are always added to the beginning of the array which, naturally,
509
+ # causes other sheets to be pushed to the next index which could
510
+ # lead to sheet references getting overwritten, so we need to
511
+ # handle that case specifically.
512
+ nr = $1
513
+ sheet_files_index = nr.to_i - 1
514
+ sheet_files_index += 1 if @sheet_files[sheet_files_index]
515
+ @sheet_files[sheet_files_index] = "#{tmpdir}/roo_sheet#{nr.to_i}"
516
+ when /comments([0-9]+).xml$/
517
+ nr = $1
518
+ @comments_files[nr.to_i-1] = "#{tmpdir}/roo_comments#{nr}"
519
+ when /sheet([0-9]+).xml.rels$/
520
+ nr = $1
521
+ @rels_files[nr.to_i-1] = "#{tmpdir}/roo_rels#{nr}"
522
+ end
523
+ if path
524
+ entry.extract(path)
525
+ end
526
+ end
527
+ end
528
+
529
+ def styles
530
+ @styles ||= Styles.new(File.join(@tmpdir, 'roo_styles.xml'))
531
+ end
532
+
533
+ def shared_strings
534
+ @shared_strings ||= SharedStrings.new(File.join(@tmpdir, 'roo_sharedStrings.xml'))
535
+ end
536
+
537
+ def workbook
538
+ @workbook ||= Workbook.new(File.join(@tmpdir, "roo_workbook.xml"))
539
+ end
540
+ end