culturecode-roo 2.0.1

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