roo-andyw8 2.0.0

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