roo 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 67acb8a7cbbb8c526865f95e6b93f37313b6d373
4
- data.tar.gz: 48cf05f82b1a5e64c2b2549f08afbc3544e575dc
3
+ metadata.gz: db86b4586034783940303cfd17a7ee5c60277a51
4
+ data.tar.gz: 2cf9222f2f2759f164164f745c72abe6b901df98
5
5
  SHA512:
6
- metadata.gz: 0c77744d3f264952565934a3d49966bcaa80bd4f7b1a388164ded5a89873347e1f0fec00c074839a60f03982e6c37044b448568bb493a062c37c58dbf9fc5095
7
- data.tar.gz: 42b35d1b9a8b55a5d10f41803cec60da00c902853ec38a5ee44bbd5ab2736f60637ef0a91a470cdbe2503a5bb06009e3c87c9f3f571eccbc3d7de4e8caf9c1c2
6
+ metadata.gz: 442e05a8bffdc839c45a8684e8882f4327098387617b76027b6b5c0543c2a7b4be284c511c35f8333c9bc1147e31350d1f6f67174fc62e5b7fc45520ab0ad307
7
+ data.tar.gz: 4ba67602e5772cae52f41ea0357ddd6835d25c35be57797307a6194a46ad977d7922b884b4fa826c91efd8533c6f3e56995e8cb0a13146a32e4a3ee7d5e42ae9
@@ -1,4 +1,19 @@
1
- ## [2.0.0beta1] [unreleased]
1
+ ## [2.0.1] - 2015-06-01
2
+ ### Added
3
+ - Return an enumerator when calling '#each' without a block [#219](https://github.com/roo-rb/roo/pull/219)
4
+ - Added Roo::Base#close to delete any temp directories[#211](https://github.com/roo-rb/roo/pull/211)
5
+ - Offset option for excelx #each_row. [#214](https://github.com/roo-rb/roo/pull/214)
6
+ - Allow Roo::Excelx to open streams [#209](https://github.com/roo-rb/roo/pull/209)
7
+
8
+ ### Fixed
9
+ - Use gsub instead of tr for double quote escaping [#212](https://github.com/roo-rb/roo/pull/212), [#212-patch](https://github.com/roo-rb/roo/commit/fcc9a015868ebf9d42cbba5b6cfdaa58b81ecc01)
10
+ - Fixed Changelog links and release data. [#204](https://github.com/roo-rb/roo/pull/204), [#206](https://github.com/roo-rb/roo/pull/206)
11
+ - Allow Pathnames to be used when opening files. [#207](https://github.com/roo-rb/roo/pull/207)
12
+
13
+ ### Removed
14
+ - Removed the scripts folder. [#213](https://github.com/roo-rb/roo/pull/213)
15
+
16
+ ## [2.0.0] - 2015-04-24
2
17
  ### Added
3
18
  - Added optional support for hidden sheets in Excelx and LibreOffice files [#177](https://github.com/roo-rb/roo/pull/177)
4
19
  - Roo::OpenOffice can be used to open encrypted workbooks. [#157](https://github.com/roo-rb/roo/pull/157)
@@ -9,7 +9,7 @@ require 'roo/utils'
9
9
  class Roo::Base
10
10
  include Enumerable
11
11
 
12
- TEMP_PREFIX = 'roo_'
12
+ TEMP_PREFIX = 'roo_'.freeze
13
13
  MAX_ROW_COL = 999_999.freeze
14
14
  MIN_ROW_COL = 0.freeze
15
15
 
@@ -32,6 +32,15 @@ class Roo::Base
32
32
  @last_column = {}
33
33
 
34
34
  @header_line = 1
35
+ rescue => e # clean up any temp files, but only if an error was raised
36
+ close
37
+ raise e
38
+ end
39
+
40
+ def close
41
+ return nil unless @tmpdirs
42
+ @tmpdirs.each { |dir| ::FileUtils.remove_entry(dir) }
43
+ nil
35
44
  end
36
45
 
37
46
  def default_sheet
@@ -343,21 +352,25 @@ class Roo::Base
343
352
  # control characters and white spaces around columns
344
353
 
345
354
  def each(options = {})
346
- if options.empty?
347
- 1.upto(last_row) do |line|
348
- yield row(line)
355
+ if block_given?
356
+ if options.empty?
357
+ 1.upto(last_row) do |line|
358
+ yield row(line)
359
+ end
360
+ else
361
+ clean_sheet_if_need(options)
362
+ search_or_set_header(options)
363
+ headers = @headers ||
364
+ Hash[(first_column..last_column).map do |col|
365
+ [cell(@header_line, col), col]
366
+ end]
367
+
368
+ @header_line.upto(last_row) do |line|
369
+ yield(Hash[headers.map { |k, v| [k, cell(line, v)] }])
370
+ end
349
371
  end
350
372
  else
351
- clean_sheet_if_need(options)
352
- search_or_set_header(options)
353
- headers = @headers ||
354
- Hash[(first_column..last_column).map do |col|
355
- [cell(@header_line, col), col]
356
- end]
357
-
358
- @header_line.upto(last_row) do |line|
359
- yield(Hash[headers.map { |k, v| [k, cell(line, v)] }])
360
- end
373
+ to_enum(:each, options)
361
374
  end
362
375
  end
363
376
 
@@ -429,8 +442,16 @@ class Roo::Base
429
442
  "#{arr[0]},#{arr[1]}"
430
443
  end
431
444
 
445
+ def is_stream?(filename_or_stream)
446
+ filename_or_stream.respond_to?(:seek)
447
+ end
448
+
432
449
  private
433
450
 
451
+ def track_tmpdir!(tmpdir)
452
+ (@tmpdirs ||= []) << tmpdir
453
+ end
454
+
434
455
  def clean_sheet_if_need(options)
435
456
  return unless options[:clean]
436
457
  options.delete(:clean)
@@ -451,6 +472,7 @@ class Roo::Base
451
472
  end
452
473
 
453
474
  def local_filename(filename, tmpdir, packed)
475
+ return if is_stream?(filename)
454
476
  filename = download_uri(filename, tmpdir) if uri?(filename)
455
477
  filename = unzip(filename, tmpdir) if packed == :zip
456
478
  unless File.file?(filename)
@@ -516,7 +538,9 @@ class Roo::Base
516
538
  else
517
539
  TEMP_PREFIX
518
540
  end
519
- Dir.mktmpdir(prefix, root || ENV['ROO_TMP'], &block)
541
+ ::Dir.mktmpdir(prefix, root || ENV['ROO_TMP'], &block).tap do |result|
542
+ block_given? || track_tmpdir!(result)
543
+ end
520
544
  end
521
545
 
522
546
  def clean_sheet(sheet)
@@ -663,9 +687,9 @@ class Roo::Base
663
687
 
664
688
  case celltype(row, col, sheet)
665
689
  when :string
666
- %("#{onecell.tr('"', '""')}") unless onecell.empty?
690
+ %("#{onecell.gsub('"', '""')}") unless onecell.empty?
667
691
  when :boolean
668
- %("#{onecell.tr('"', '""').downcase}")
692
+ %("#{onecell.gsub('"', '""').downcase}")
669
693
  when :float, :percentage
670
694
  if onecell == onecell.to_i
671
695
  onecell.to_i.to_s
@@ -675,7 +699,7 @@ class Roo::Base
675
699
  when :formula
676
700
  case onecell
677
701
  when String
678
- %("#{onecell.tr('"', '""')}") unless onecell.empty?
702
+ %("#{onecell.gsub('"', '""')}") unless onecell.empty?
679
703
  when Float
680
704
  if onecell == onecell.to_i
681
705
  onecell.to_i.to_s
@@ -692,7 +716,7 @@ class Roo::Base
692
716
  when :time
693
717
  integer_to_timestring(onecell)
694
718
  when :link
695
- %("#{onecell.url.tr('"', '""')}")
719
+ %("#{onecell.url.gsub('"', '""')}")
696
720
  else
697
721
  fail "unhandled celltype #{celltype(row, col, sheet)}"
698
722
  end || ''
@@ -1,642 +1,490 @@
1
- require 'date'
2
1
  require 'nokogiri'
2
+ require 'zip/filesystem'
3
3
  require 'roo/link'
4
4
  require 'roo/utils'
5
- require 'zip/filesystem'
6
5
 
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
6
+ module Roo
7
+ class Excelx < Roo::Base
8
+ require 'roo/excelx/workbook'
9
+ require 'roo/excelx/shared_strings'
10
+ require 'roo/excelx/styles'
11
+ require 'roo/excelx/cell'
12
+ require 'roo/excelx/sheet'
13
+ require 'roo/excelx/relationships'
14
+ require 'roo/excelx/comments'
15
+ require 'roo/excelx/sheet_doc'
16
+
17
+ module Format
18
+ EXCEPTIONAL_FORMATS = {
19
+ 'h:mm am/pm' => :date,
20
+ 'h:mm:ss am/pm' => :date
21
+ }
22
+
23
+ STANDARD_FORMATS = {
24
+ 0 => 'General'.freeze,
25
+ 1 => '0'.freeze,
26
+ 2 => '0.00'.freeze,
27
+ 3 => '#,##0'.freeze,
28
+ 4 => '#,##0.00'.freeze,
29
+ 9 => '0%'.freeze,
30
+ 10 => '0.00%'.freeze,
31
+ 11 => '0.00E+00'.freeze,
32
+ 12 => '# ?/?'.freeze,
33
+ 13 => '# ??/??'.freeze,
34
+ 14 => 'mm-dd-yy'.freeze,
35
+ 15 => 'd-mmm-yy'.freeze,
36
+ 16 => 'd-mmm'.freeze,
37
+ 17 => 'mmm-yy'.freeze,
38
+ 18 => 'h:mm AM/PM'.freeze,
39
+ 19 => 'h:mm:ss AM/PM'.freeze,
40
+ 20 => 'h:mm'.freeze,
41
+ 21 => 'h:mm:ss'.freeze,
42
+ 22 => 'm/d/yy h:mm'.freeze,
43
+ 37 => '#,##0 ;(#,##0)'.freeze,
44
+ 38 => '#,##0 ;[Red](#,##0)'.freeze,
45
+ 39 => '#,##0.00;(#,##0.00)'.freeze,
46
+ 40 => '#,##0.00;[Red](#,##0.00)'.freeze,
47
+ 45 => 'mm:ss'.freeze,
48
+ 46 => '[h]:mm:ss'.freeze,
49
+ 47 => 'mmss.0'.freeze,
50
+ 48 => '##0.0E+0'.freeze,
51
+ 49 => '@'.freeze
52
+ }
53
+
54
+ def to_type(format)
55
+ format = format.to_s.downcase
56
+ if (type = EXCEPTIONAL_FORMATS[format])
57
+ type
58
+ elsif format.include?('#')
59
+ :float
60
+ elsif !format.match(/d+(?![\]])/).nil? || format.include?('y')
61
+ if format.include?('h') || format.include?('s')
62
+ :datetime
63
+ else
64
+ :date
65
+ end
66
+ elsif format.include?('h') || format.include?('s')
67
+ :time
68
+ elsif format.include?('%')
69
+ :percentage
62
70
  else
63
- :date
71
+ :float
64
72
  end
65
- elsif format.include?('h') || format.include?('s')
66
- :time
67
- elsif format.include?('%')
68
- :percentage
69
- else
70
- :float
71
73
  end
74
+
75
+ module_function :to_type
72
76
  end
73
77
 
74
- module_function :to_type
75
- end
78
+ ExceedsMaxError = Class.new(StandardError)
76
79
 
77
- class Cell
78
- attr_reader :type, :formula, :value, :excelx_type, :excelx_value, :style, :hyperlink, :coordinate
79
- attr_writer :value
80
-
81
- def initialize(value, type, formula, excelx_type, excelx_value, style, hyperlink, base_date, coordinate)
82
- @type = type
83
- @formula = formula
84
- @base_date = base_date if [:date, :datetime].include?(@type)
85
- @excelx_type = excelx_type
86
- @excelx_value = excelx_value
87
- @style = style
88
- @value = type_cast_value(value)
89
- @value = Roo::Link.new(hyperlink, @value.to_s) if hyperlink
90
- @coordinate = coordinate
91
- end
92
-
93
- def type
94
- if @formula
95
- :formula
96
- elsif @value.is_a?(Roo::Link)
97
- :link
98
- else
99
- @type
80
+ # initialization and opening of a spreadsheet file
81
+ # values for packed: :zip
82
+ # optional cell_max (int) parameter for early aborting attempts to parse
83
+ # enormous documents.
84
+ def initialize(filename_or_stream, options = {})
85
+ packed = options[:packed]
86
+ file_warning = options.fetch(:file_warning, :error)
87
+ cell_max = options.delete(:cell_max)
88
+ sheet_options = {}
89
+ sheet_options[:expand_merged_ranges] = (options[:expand_merged_ranges] || false)
90
+
91
+ unless is_stream?(filename_or_stream)
92
+ file_type_check(filename_or_stream, '.xlsx', 'an Excel-xlsx', file_warning, packed)
93
+ basename = File.basename(filename_or_stream)
100
94
  end
101
- end
102
95
 
103
- class Coordinate
104
- attr_accessor :row, :column
96
+ @tmpdir = make_tmpdir(basename, options[:tmpdir_root])
97
+ @filename = local_filename(filename_or_stream, @tmpdir, packed)
98
+ @comments_files = []
99
+ @rels_files = []
100
+ process_zipfile(@filename || filename_or_stream)
105
101
 
106
- def initialize(row, column)
107
- @row, @column = row, column
102
+ @sheet_names = workbook.sheets.map do |sheet|
103
+ unless options[:only_visible_sheets] && sheet['state'] == 'hidden'
104
+ sheet['name']
105
+ end
106
+ end.compact
107
+ @sheets = []
108
+ @sheets_by_name = Hash[@sheet_names.map.with_index do |sheet_name, n|
109
+ @sheets[n] = Sheet.new(sheet_name, @rels_files[n], @sheet_files[n], @comments_files[n], styles, shared_strings, workbook, sheet_options)
110
+ [sheet_name, @sheets[n]]
111
+ end]
112
+
113
+ if cell_max
114
+ cell_count = ::Roo::Utils.num_cells_in_range(sheet_for(options.delete(:sheet)).dimensions)
115
+ raise ExceedsMaxError.new("Excel file exceeds cell maximum: #{cell_count} > #{cell_max}") if cell_count > cell_max
108
116
  end
109
- end
110
117
 
111
- private
118
+ super
119
+ rescue => e # clean up any temp files, but only if an error was raised
120
+ close
121
+ raise e
122
+ end
112
123
 
113
- def type_cast_value(value)
114
- case @type
115
- when :float, :percentage
116
- value.to_f
117
- when :date
118
- yyyy,mm,dd = (@base_date+value.to_i).strftime("%Y-%m-%d").split('-')
119
- Date.new(yyyy.to_i,mm.to_i,dd.to_i)
120
- when :datetime
121
- create_datetime_from((@base_date+value.to_f.round(6)).strftime("%Y-%m-%d %H:%M:%S.%N"))
122
- when :time
123
- value.to_f*(24*60*60)
124
- when :string
125
- value
124
+ def method_missing(method, *args)
125
+ if (label = workbook.defined_names[method.to_s])
126
+ safe_send(sheet_for(label.sheet).cells[label.key], :value)
126
127
  else
127
- value
128
+ # call super for methods like #a1
129
+ super
128
130
  end
129
131
  end
130
132
 
131
- def create_datetime_from(datetime_string)
132
- date_part,time_part = round_time_from(datetime_string).split(' ')
133
- yyyy,mm,dd = date_part.split('-')
134
- hh,mi,ss = time_part.split(':')
135
- DateTime.civil(yyyy.to_i,mm.to_i,dd.to_i,hh.to_i,mi.to_i,ss.to_i)
136
- end
137
-
138
- def round_time_from(datetime_string)
139
- date_part,time_part = datetime_string.split(' ')
140
- yyyy,mm,dd = date_part.split('-')
141
- hh,mi,ss = time_part.split(':')
142
- 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")
133
+ def sheets
134
+ @sheet_names
143
135
  end
144
- end
145
136
 
146
- class Sheet
147
- def initialize(name, rels_path, sheet_path, comments_path, styles, shared_strings, workbook, options = {})
148
- @name = name
149
- @rels = Relationships.new(rels_path)
150
- @comments = Comments.new(comments_path)
151
- @styles = styles
152
- @sheet = SheetDoc.new(sheet_path, @rels, @styles, shared_strings, workbook, options)
137
+ def sheet_for(sheet)
138
+ sheet ||= default_sheet
139
+ validate_sheet!(sheet)
140
+ @sheets_by_name[sheet]
153
141
  end
154
142
 
155
- def cells
156
- @cells ||= @sheet.cells(@rels)
143
+ # Returns the content of a spreadsheet-cell.
144
+ # (1,1) is the upper left corner.
145
+ # (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
146
+ # cell at the first line and first row.
147
+ def cell(row, col, sheet = nil)
148
+ key = normalize(row, col)
149
+ safe_send(sheet_for(sheet).cells[key], :value)
157
150
  end
158
151
 
159
- def present_cells
160
- @present_cells ||= cells.select {|key, cell| cell && cell.value }
152
+ def row(rownumber, sheet = nil)
153
+ sheet_for(sheet).row(rownumber)
161
154
  end
162
155
 
163
- # Yield each row as array of Excelx::Cell objects
164
- # accepts options max_rows (int) (offset by 1 for header)
165
- # and pad_cells (boolean)
166
- def each_row(options = {}, &block)
167
- row_count = 0
168
- @sheet.each_row_streaming do |row|
169
- break if options[:max_rows] && row_count == options[:max_rows] + 1
170
- block.call(cells_for_row_element(row, options)) if block_given?
171
- row_count += 1
156
+ # returns all values in this column as an array
157
+ # column numbers are 1,2,3,... like in the spreadsheet
158
+ def column(column_number, sheet = nil)
159
+ if column_number.is_a?(::String)
160
+ column_number = ::Roo::Utils.letter_to_number(column_number)
172
161
  end
173
- end
174
-
175
- def row(row_number)
176
- first_column.upto(last_column).map do |col|
177
- cells[[row_number,col]]
178
- end.map {|cell| cell && cell.value }
179
- end
180
-
181
- def column(col_number)
182
- first_row.upto(last_row).map do |row|
183
- cells[[row,col_number]]
184
- end.map {|cell| cell && cell.value }
162
+ sheet_for(sheet).column(column_number)
185
163
  end
186
164
 
187
165
  # returns the number of the first non-empty row
188
- def first_row
189
- @first_row ||= present_cells.keys.map {|row, _| row }.min
166
+ def first_row(sheet = nil)
167
+ sheet_for(sheet).first_row
190
168
  end
191
169
 
192
- def last_row
193
- @last_row ||= present_cells.keys.map {|row, _| row }.max
170
+ # returns the number of the last non-empty row
171
+ def last_row(sheet = nil)
172
+ sheet_for(sheet).last_row
194
173
  end
195
174
 
196
175
  # returns the number of the first non-empty column
197
- def first_column
198
- @first_column ||= present_cells.keys.map {|_, col| col }.min
176
+ def first_column(sheet = nil)
177
+ sheet_for(sheet).first_column
199
178
  end
200
179
 
201
180
  # returns the number of the last non-empty column
202
- def last_column
203
- @last_column ||= present_cells.keys.map {|_, col| col }.max
181
+ def last_column(sheet = nil)
182
+ sheet_for(sheet).last_column
204
183
  end
205
184
 
206
- def excelx_format(key)
207
- cell = cells[key]
208
- @styles.style_format(cell.style).to_s if cell
185
+ # set a cell to a certain value
186
+ # (this will not be saved back to the spreadsheet file!)
187
+ def set(row, col, value, sheet = nil) #:nodoc:
188
+ key = normalize(row, col)
189
+ cell_type = cell_type_by_value(value)
190
+ sheet_for(sheet).cells[key] = Cell.new(value, cell_type, nil, cell_type, value, nil, nil, nil, Cell::Coordinate.new(row, col))
209
191
  end
210
192
 
211
- def hyperlinks
212
- @hyperlinks ||= @sheet.hyperlinks(@rels)
193
+ # Returns the formula at (row,col).
194
+ # Returns nil if there is no formula.
195
+ # The method #formula? checks if there is a formula.
196
+ def formula(row, col, sheet = nil)
197
+ key = normalize(row, col)
198
+ safe_send(sheet_for(sheet).cells[key], :formula)
213
199
  end
214
200
 
215
- def comments
216
- @comments.comments
201
+ # Predicate methods really should return a boolean
202
+ # value. Hopefully no one was relying on the fact that this
203
+ # previously returned either nil/formula
204
+ def formula?(*args)
205
+ !!formula(*args)
217
206
  end
218
207
 
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
208
+ # returns each formula in the selected sheet as an array of tuples in following format
209
+ # [[row, col, formula], [row, col, formula],...]
210
+ def formulas(sheet = nil)
211
+ sheet_for(sheet).cells.select { |_, cell| cell.formula }.map do |(x, y), cell|
212
+ [x, y, cell.formula]
236
213
  end
237
- cells
238
214
  end
239
215
 
240
- def pad_cells(cell, last_column)
241
- pad = []
242
- (cell.coordinate.column - 1 - last_column).times { pad << nil }
243
- pad
216
+ # Given a cell, return the cell's style
217
+ def font(row, col, sheet = nil)
218
+ key = normalize(row, col)
219
+ definition_index = safe_send(sheet_for(sheet).cells[key], :style)
220
+ styles.definitions[definition_index] if definition_index
244
221
  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
- sheet_options = {}
258
- sheet_options[:expand_merged_ranges] = (options[:expand_merged_ranges] || false)
259
-
260
- file_type_check(filename,'.xlsx','an Excel-xlsx', file_warning, packed)
261
-
262
- @tmpdir = make_tmpdir(filename.split('/').last, options[:tmpdir_root])
263
- @filename = local_filename(filename, @tmpdir, packed)
264
- @comments_files = []
265
- @rels_files = []
266
- process_zipfile(@tmpdir, @filename)
267
-
268
- @sheet_names = workbook.sheets.map do |sheet|
269
- unless options[:only_visible_sheets] && sheet['state'] == 'hidden'
270
- sheet['name']
271
- end
272
- end.compact
273
- @sheets = []
274
- @sheets_by_name = Hash[@sheet_names.map.with_index do |sheet_name, n|
275
- @sheets[n] = Sheet.new(sheet_name, @rels_files[n], @sheet_files[n], @comments_files[n], styles, shared_strings, workbook, sheet_options)
276
- [sheet_name, @sheets[n]]
277
- end]
278
222
 
279
- if cell_max
280
- cell_count = ::Roo::Utils.num_cells_in_range(sheet_for(options.delete(:sheet)).dimensions)
281
- raise ExceedsMaxError.new("Excel file exceeds cell maximum: #{cell_count} > #{cell_max}") if cell_count > cell_max
223
+ # returns the type of a cell:
224
+ # * :float
225
+ # * :string,
226
+ # * :date
227
+ # * :percentage
228
+ # * :formula
229
+ # * :time
230
+ # * :datetime
231
+ def celltype(row, col, sheet = nil)
232
+ key = normalize(row, col)
233
+ safe_send(sheet_for(sheet).cells[key], :type)
282
234
  end
283
235
 
284
- super
285
- end
286
-
287
- def method_missing(method,*args)
288
- if label = workbook.defined_names[method.to_s]
289
- safe_send(sheet_for(label.sheet).cells[label.key], :value)
290
- else
291
- # call super for methods like #a1
292
- super
236
+ # returns the internal type of an excel cell
237
+ # * :numeric_or_formula
238
+ # * :string
239
+ # Note: this is only available within the Excelx class
240
+ def excelx_type(row, col, sheet = nil)
241
+ key = normalize(row, col)
242
+ safe_send(sheet_for(sheet).cells[key], :excelx_type)
293
243
  end
294
- end
295
244
 
296
- def sheets
297
- @sheet_names
298
- end
299
-
300
- def sheet_for(sheet)
301
- sheet ||= default_sheet
302
- validate_sheet!(sheet)
303
- @sheets_by_name[sheet]
304
- end
305
-
306
- # Returns the content of a spreadsheet-cell.
307
- # (1,1) is the upper left corner.
308
- # (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
309
- # cell at the first line and first row.
310
- def cell(row, col, sheet=nil)
311
- key = normalize(row,col)
312
- safe_send(sheet_for(sheet).cells[key], :value)
313
- end
314
-
315
- def row(rownumber,sheet=nil)
316
- sheet_for(sheet).row(rownumber)
317
- end
318
-
319
- # returns all values in this column as an array
320
- # column numbers are 1,2,3,... like in the spreadsheet
321
- def column(column_number,sheet=nil)
322
- if column_number.is_a?(::String)
323
- column_number = ::Roo::Utils.letter_to_number(column_number)
245
+ # returns the internal value of an excelx cell
246
+ # Note: this is only available within the Excelx class
247
+ def excelx_value(row, col, sheet = nil)
248
+ key = normalize(row, col)
249
+ safe_send(sheet_for(sheet).cells[key], :excelx_value)
324
250
  end
325
- sheet_for(sheet).column(column_number)
326
- end
327
-
328
- # returns the number of the first non-empty row
329
- def first_row(sheet=nil)
330
- sheet_for(sheet).first_row
331
- end
332
-
333
- # returns the number of the last non-empty row
334
- def last_row(sheet=nil)
335
- sheet_for(sheet).last_row
336
- end
337
-
338
- # returns the number of the first non-empty column
339
- def first_column(sheet=nil)
340
- sheet_for(sheet).first_column
341
- end
342
251
 
343
- # returns the number of the last non-empty column
344
- def last_column(sheet=nil)
345
- sheet_for(sheet).last_column
346
- end
347
-
348
- # set a cell to a certain value
349
- # (this will not be saved back to the spreadsheet file!)
350
- def set(row,col,value, sheet = nil) #:nodoc:
351
- key = normalize(row,col)
352
- cell_type = cell_type_by_value(value)
353
- sheet_for(sheet).cells[key] = Cell.new(value, cell_type, nil, cell_type, value, nil, nil, nil, Cell::Coordinate.new(row, col))
354
- end
355
-
356
-
357
- # Returns the formula at (row,col).
358
- # Returns nil if there is no formula.
359
- # The method #formula? checks if there is a formula.
360
- def formula(row,col,sheet=nil)
361
- key = normalize(row,col)
362
- safe_send(sheet_for(sheet).cells[key], :formula)
363
- end
364
-
365
- # Predicate methods really should return a boolean
366
- # value. Hopefully no one was relying on the fact that this
367
- # previously returned either nil/formula
368
- def formula?(*args)
369
- !!formula(*args)
370
- end
371
-
372
- # returns each formula in the selected sheet as an array of tuples in following format
373
- # [[row, col, formula], [row, col, formula],...]
374
- def formulas(sheet=nil)
375
- sheet_for(sheet).cells.select {|_, cell| cell.formula }.map do |(x, y), cell|
376
- [x, y, cell.formula]
252
+ # returns the internal format of an excel cell
253
+ def excelx_format(row, col, sheet = nil)
254
+ key = normalize(row, col)
255
+ sheet_for(sheet).excelx_format(key)
377
256
  end
378
- end
379
-
380
- # Given a cell, return the cell's style
381
- def font(row, col, sheet=nil)
382
- key = normalize(row,col)
383
- definition_index = safe_send(sheet_for(sheet).cells[key], :style)
384
- styles.definitions[definition_index] if definition_index
385
- end
386
-
387
- # returns the type of a cell:
388
- # * :float
389
- # * :string,
390
- # * :date
391
- # * :percentage
392
- # * :formula
393
- # * :time
394
- # * :datetime
395
- def celltype(row,col,sheet=nil)
396
- key = normalize(row, col)
397
- safe_send(sheet_for(sheet).cells[key], :type)
398
- end
399
-
400
- # returns the internal type of an excel cell
401
- # * :numeric_or_formula
402
- # * :string
403
- # Note: this is only available within the Excelx class
404
- def excelx_type(row,col,sheet=nil)
405
- key = normalize(row,col)
406
- safe_send(sheet_for(sheet).cells[key], :excelx_type)
407
- end
408
257
 
409
- # returns the internal value of an excelx cell
410
- # Note: this is only available within the Excelx class
411
- def excelx_value(row,col,sheet=nil)
412
- key = normalize(row,col)
413
- safe_send(sheet_for(sheet).cells[key], :excelx_value)
414
- end
258
+ def empty?(row, col, sheet = nil)
259
+ sheet = sheet_for(sheet)
260
+ key = normalize(row, col)
261
+ cell = sheet.cells[key]
262
+ !cell || !cell.value || (cell.type == :string && cell.value.empty?) \
263
+ || (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
264
+ end
415
265
 
416
- # returns the internal format of an excel cell
417
- def excelx_format(row,col,sheet=nil)
418
- key = normalize(row,col)
419
- sheet_for(sheet).excelx_format(key)
420
- end
266
+ # shows the internal representation of all cells
267
+ # for debugging purposes
268
+ def to_s(sheet = nil)
269
+ sheet_for(sheet).cells.inspect
270
+ end
421
271
 
422
- def empty?(row,col,sheet=nil)
423
- sheet = sheet_for(sheet)
424
- key = normalize(row,col)
425
- cell = sheet.cells[key]
426
- !cell || !cell.value || (cell.type == :string && cell.value.empty?) \
427
- || (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
428
- end
272
+ # returns the row,col values of the labelled cell
273
+ # (nil,nil) if label is not defined
274
+ def label(name)
275
+ labels = workbook.defined_names
276
+ return [nil, nil, nil] if labels.empty? || !labels.key?(name)
429
277
 
430
- # shows the internal representation of all cells
431
- # for debugging purposes
432
- def to_s(sheet=nil)
433
- sheet_for(sheet).cells.inspect
434
- end
278
+ [labels[name].row, labels[name].col, labels[name].sheet]
279
+ end
435
280
 
436
- # returns the row,col values of the labelled cell
437
- # (nil,nil) if label is not defined
438
- def label(name)
439
- labels = workbook.defined_names
440
- if labels.empty? || !labels.key?(name)
441
- [nil,nil,nil]
442
- else
443
- [labels[name].row,
444
- labels[name].col,
445
- labels[name].sheet]
281
+ # Returns an array which all labels. Each element is an array with
282
+ # [labelname, [row,col,sheetname]]
283
+ def labels
284
+ @labels ||= workbook.defined_names.map do |name, label|
285
+ [
286
+ name,
287
+ [label.row, label.col, label.sheet]
288
+ ]
289
+ end
446
290
  end
447
- end
448
291
 
449
- # Returns an array which all labels. Each element is an array with
450
- # [labelname, [row,col,sheetname]]
451
- def labels
452
- @labels ||= workbook.defined_names.map do |name, label|
453
- [ name,
454
- [ label.row,
455
- label.col,
456
- label.sheet,
457
- ] ]
292
+ def hyperlink?(row, col, sheet = nil)
293
+ !!hyperlink(row, col, sheet)
458
294
  end
459
- end
460
295
 
461
- def hyperlink?(row,col,sheet=nil)
462
- !!hyperlink(row, col, sheet)
463
- end
296
+ # returns the hyperlink at (row/col)
297
+ # nil if there is no hyperlink
298
+ def hyperlink(row, col, sheet = nil)
299
+ key = normalize(row, col)
300
+ sheet_for(sheet).hyperlinks[key]
301
+ end
464
302
 
465
- # returns the hyperlink at (row/col)
466
- # nil if there is no hyperlink
467
- def hyperlink(row,col,sheet=nil)
468
- key = normalize(row,col)
469
- sheet_for(sheet).hyperlinks[key]
470
- end
303
+ # returns the comment at (row/col)
304
+ # nil if there is no comment
305
+ def comment(row, col, sheet = nil)
306
+ key = normalize(row, col)
307
+ sheet_for(sheet).comments[key]
308
+ end
471
309
 
472
- # returns the comment at (row/col)
473
- # nil if there is no comment
474
- def comment(row,col,sheet=nil)
475
- key = normalize(row,col)
476
- sheet_for(sheet).comments[key]
477
- end
310
+ # true, if there is a comment
311
+ def comment?(row, col, sheet = nil)
312
+ !!comment(row, col, sheet)
313
+ end
478
314
 
479
- # true, if there is a comment
480
- def comment?(row,col,sheet=nil)
481
- !!comment(row,col,sheet)
482
- end
315
+ def comments(sheet = nil)
316
+ sheet_for(sheet).comments.map do |(x, y), comment|
317
+ [x, y, comment]
318
+ end
319
+ end
483
320
 
484
- def comments(sheet=nil)
485
- sheet_for(sheet).comments.map do |(x, y), comment|
486
- [x, y, comment]
321
+ # Yield an array of Excelx::Cell
322
+ # Takes options for sheet, pad_cells, and max_rows
323
+ def each_row_streaming(options = {})
324
+ sheet_for(options.delete(:sheet)).each_row(options) { |row| yield row }
487
325
  end
488
- end
489
326
 
490
- # Yield an array of Excelx::Cell
491
- # Takes options for sheet, pad_cells, and max_rows
492
- def each_row_streaming(options={})
493
- sheet_for(options.delete(:sheet)).each_row(options) { |row| yield row }
494
- end
327
+ private
495
328
 
496
- private
329
+ def clean_sheet(sheet)
330
+ @sheets_by_name[sheet].cells.each_pair do |coord, value|
331
+ next unless value.value.is_a?(::String)
497
332
 
498
- def clean_sheet(sheet)
499
- @sheets_by_name[sheet].cells.each_pair do |coord, value|
500
- next unless value.value.is_a?(::String)
333
+ @sheets_by_name[sheet].cells[coord].value = sanitize_value(value.value)
334
+ end
501
335
 
502
- @sheets_by_name[sheet].cells[coord].value = sanitize_value(value.value)
336
+ @cleaned[sheet] = true
503
337
  end
504
338
 
505
- @cleaned[sheet] = true
506
- end
507
-
508
- # Internal: extracts the worksheet_ids from the workbook.xml file. xlsx
509
- # documents require a workbook.xml file, so a if the file is missing
510
- # it is not a valid xlsx file. In these cases, an ArgumentError is
511
- # raised.
512
- #
513
- # wb - a Zip::Entry for the workbook.xml file.
514
- # path - A String for Zip::Entry's destination path.
515
- #
516
- # Examples
517
- #
518
- # extract_worksheet_ids(<Zip::Entry>, 'tmpdir/roo_workbook.xml')
519
- # # => ["rId1", "rId2", "rId3"]
520
- #
521
- # Returns an Array of Strings.
522
- def extract_worksheet_ids(entries, path)
339
+ # Internal: extracts the worksheet_ids from the workbook.xml file. xlsx
340
+ # documents require a workbook.xml file, so a if the file is missing
341
+ # it is not a valid xlsx file. In these cases, an ArgumentError is
342
+ # raised.
343
+ #
344
+ # wb - a Zip::Entry for the workbook.xml file.
345
+ # path - A String for Zip::Entry's destination path.
346
+ #
347
+ # Examples
348
+ #
349
+ # extract_worksheet_ids(<Zip::Entry>, 'tmpdir/roo_workbook.xml')
350
+ # # => ["rId1", "rId2", "rId3"]
351
+ #
352
+ # Returns an Array of Strings.
353
+ def extract_worksheet_ids(entries, path)
523
354
  wb = entries.find { |e| e.name[/workbook.xml$/] }
524
355
  fail ArgumentError 'missing required workbook file' if wb.nil?
525
356
 
526
357
  wb.extract(path)
527
358
  workbook_doc = Roo::Utils.load_xml(path).remove_namespaces!
528
- workbook_doc.xpath('//sheet').map{ |s| s.attributes['id'].value }
529
- end
359
+ workbook_doc.xpath('//sheet').map { |s| s.attributes['id'].value }
360
+ end
361
+
362
+ # Internal
363
+ #
364
+ # wb_rels - A Zip::Entry for the workbook.xml.rels file.
365
+ # path - A String for the Zip::Entry's destination path.
366
+ #
367
+ # Examples
368
+ #
369
+ # extract_worksheets(<Zip::Entry>, 'tmpdir/roo_workbook.xml.rels')
370
+ # # => {
371
+ # "rId1"=>"worksheets/sheet1.xml",
372
+ # "rId2"=>"worksheets/sheet2.xml",
373
+ # "rId3"=>"worksheets/sheet3.xml"
374
+ # }
375
+ #
376
+ # Returns a Hash.
377
+ def extract_worksheet_rels(entries, path)
378
+ wb_rels = entries.find { |e| e.name[/workbook.xml.rels$/] }
379
+ fail ArgumentError 'missing required workbook file' if wb_rels.nil?
380
+
381
+ wb_rels.extract(path)
382
+ rels_doc = Roo::Utils.load_xml(path).remove_namespaces!
383
+ worksheet_type = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'
530
384
 
531
- # Internal
532
- #
533
- # wb_rels - A Zip::Entry for the workbook.xml.rels file.
534
- # path - A String for the Zip::Entry's destination path.
535
- #
536
- # Examples
537
- #
538
- # extract_worksheets(<Zip::Entry>, 'tmpdir/roo_workbook.xml.rels')
539
- # # => {
540
- # "rId1"=>"worksheets/sheet1.xml",
541
- # "rId2"=>"worksheets/sheet2.xml",
542
- # "rId3"=>"worksheets/sheet3.xml"
543
- # }
544
- #
545
- # Returns a Hash.
546
- def extract_worksheet_rels(entries, path)
547
- wb_rels = entries.find { |e| e.name[/workbook.xml.rels$/] }
548
- fail ArgumentError 'missing required workbook file' if wb_rels.nil?
549
-
550
- wb_rels.extract(path)
551
- rels_doc = Roo::Utils.load_xml(path).remove_namespaces!
552
- worksheet_type ='http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'
553
-
554
- relationships = rels_doc.xpath('//Relationship').select do |relationship|
555
- relationship.attributes['Type'].value == worksheet_type
556
- end
557
-
558
- relationships.inject({}) do |hash, relationship|
559
- attributes = relationship.attributes
560
- id = attributes['Id'];
561
- hash[id.value] = attributes['Target'].value
562
- hash
385
+ relationships = rels_doc.xpath('//Relationship').select do |relationship|
386
+ relationship.attributes['Type'].value == worksheet_type
387
+ end
388
+
389
+ relationships.inject({}) do |hash, relationship|
390
+ attributes = relationship.attributes
391
+ id = attributes['Id']
392
+ hash[id.value] = attributes['Target'].value
393
+ hash
394
+ end
563
395
  end
564
- end
565
396
 
566
- def extract_sheets_in_order(entries, sheet_ids, sheets, tmpdir)
567
- sheet_ids.each_with_index do |id, i|
568
- name = sheets[id]
569
- entry = entries.find { |entry| entry.name =~ /#{name}$/ }
570
- path = "#{tmpdir}/roo_sheet#{i + 1}"
571
- @sheet_files << path
572
- entry.extract(path)
397
+ def extract_sheets_in_order(entries, sheet_ids, sheets, tmpdir)
398
+ sheet_ids.each_with_index do |id, i|
399
+ name = sheets[id]
400
+ entry = entries.find { |e| e.name =~ /#{name}$/ }
401
+ path = "#{tmpdir}/roo_sheet#{i + 1}"
402
+ @sheet_files << path
403
+ entry.extract(path)
404
+ end
573
405
  end
574
- end
575
406
 
576
- # Extracts all needed files from the zip file
577
- def process_zipfile(tmpdir, zipfilename)
578
- @sheet_files = []
579
- entries = Zip::File.open(zipfilename).to_a.sort_by(&:name)
407
+ # Extracts all needed files from the zip file
408
+ def process_zipfile(zipfilename_or_stream)
409
+ @sheet_files = []
580
410
 
581
- # NOTE: When Google or Numbers 3.1 exports to xlsx, the worksheet filenames
582
- # are not in order. With Numbers 3.1, the first sheet is always
583
- # sheet.xml, not sheet1.xml. With Google, the order of the worksheets is
584
- # independent of a worksheet's filename (i.e. sheet6.xml can be the
585
- # first worksheet).
586
- #
587
- # workbook.xml lists the correct order of worksheets and
588
- # workbook.xml.rels lists the filenames for those worksheets.
589
- #
590
- # workbook.xml:
591
- # <sheet state="visible" name="IS" sheetId="1" r:id="rId3"/>
592
- # <sheet state="visible" name="BS" sheetId="2" r:id="rId4"/>
593
- # workbook.xml.rel:
594
- # <Relationship Id="rId4" Target="worksheets/sheet5.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>
595
- # <Relationship Id="rId3" Target="worksheets/sheet4.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>
596
- sheet_ids = extract_worksheet_ids(entries, "#{tmpdir}/roo_workbook.xml")
597
- sheets = extract_worksheet_rels(entries, "#{tmpdir}/roo_workbook.xml.rels")
598
- extract_sheets_in_order(entries, sheet_ids, sheets, tmpdir)
599
-
600
- entries.each do |entry|
601
- path =
602
- case entry.name.downcase
603
- when /sharedstrings.xml$/
604
- "#{tmpdir}/roo_sharedStrings.xml"
605
- when /styles.xml$/
606
- "#{tmpdir}/roo_styles.xml"
607
- when /comments([0-9]+).xml$/
608
- # FIXME: Most of the time, The order of the comment files are the same
609
- # the sheet order, i.e. sheet1.xml's comments are in comments1.xml.
610
- # In some situations, this isn't true. The true location of a
611
- # sheet's comment file is in the sheet1.xml.rels file. SEE
612
- # ECMA-376 12.3.3 in "Ecma Office Open XML Part 1".
613
- nr = Regexp.last_match[1].to_i
614
- @comments_files[nr - 1] = "#{tmpdir}/roo_comments#{nr}"
615
- when /sheet([0-9]+).xml.rels$/
616
- # FIXME: Roo seems to use sheet[\d].xml.rels for hyperlinks only, but
617
- # it also stores the location for sharedStrings, comments,
618
- # drawings, etc.
619
- nr = Regexp.last_match[1].to_i
620
- @rels_files[nr - 1] = "#{tmpdir}/roo_rels#{nr}"
411
+ unless is_stream?(zipfilename_or_stream)
412
+ process_zipfile_entries Zip::File.open(zipfilename_or_stream).to_a.sort_by(&:name)
413
+ else
414
+ stream = Zip::InputStream.open zipfilename_or_stream
415
+ begin
416
+ entries = []
417
+ while (entry = stream.get_next_entry)
418
+ entries << entry
419
+ end
420
+ process_zipfile_entries entries
421
+ ensure
422
+ stream.close
423
+ end
621
424
  end
425
+ end
622
426
 
623
- entry.extract(path) if path
427
+ def process_zipfile_entries(entries)
428
+ # NOTE: When Google or Numbers 3.1 exports to xlsx, the worksheet filenames
429
+ # are not in order. With Numbers 3.1, the first sheet is always
430
+ # sheet.xml, not sheet1.xml. With Google, the order of the worksheets is
431
+ # independent of a worksheet's filename (i.e. sheet6.xml can be the
432
+ # first worksheet).
433
+ #
434
+ # workbook.xml lists the correct order of worksheets and
435
+ # workbook.xml.rels lists the filenames for those worksheets.
436
+ #
437
+ # workbook.xml:
438
+ # <sheet state="visible" name="IS" sheetId="1" r:id="rId3"/>
439
+ # <sheet state="visible" name="BS" sheetId="2" r:id="rId4"/>
440
+ # workbook.xml.rel:
441
+ # <Relationship Id="rId4" Target="worksheets/sheet5.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>
442
+ # <Relationship Id="rId3" Target="worksheets/sheet4.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>
443
+ sheet_ids = extract_worksheet_ids(entries, "#{@tmpdir}/roo_workbook.xml")
444
+ sheets = extract_worksheet_rels(entries, "#{@tmpdir}/roo_workbook.xml.rels")
445
+ extract_sheets_in_order(entries, sheet_ids, sheets, @tmpdir)
446
+
447
+ entries.each do |entry|
448
+ path =
449
+ case entry.name.downcase
450
+ when /sharedstrings.xml$/
451
+ "#{@tmpdir}/roo_sharedStrings.xml"
452
+ when /styles.xml$/
453
+ "#{@tmpdir}/roo_styles.xml"
454
+ when /comments([0-9]+).xml$/
455
+ # FIXME: Most of the time, The order of the comment files are the same
456
+ # the sheet order, i.e. sheet1.xml's comments are in comments1.xml.
457
+ # In some situations, this isn't true. The true location of a
458
+ # sheet's comment file is in the sheet1.xml.rels file. SEE
459
+ # ECMA-376 12.3.3 in "Ecma Office Open XML Part 1".
460
+ nr = Regexp.last_match[1].to_i
461
+ @comments_files[nr - 1] = "#{@tmpdir}/roo_comments#{nr}"
462
+ when /sheet([0-9]+).xml.rels$/
463
+ # FIXME: Roo seems to use sheet[\d].xml.rels for hyperlinks only, but
464
+ # it also stores the location for sharedStrings, comments,
465
+ # drawings, etc.
466
+ nr = Regexp.last_match[1].to_i
467
+ @rels_files[nr - 1] = "#{@tmpdir}/roo_rels#{nr}"
468
+ end
469
+
470
+ entry.extract(path) if path
471
+ end
624
472
  end
625
- end
626
473
 
627
- def styles
628
- @styles ||= Styles.new(File.join(@tmpdir, 'roo_styles.xml'))
629
- end
474
+ def styles
475
+ @styles ||= Styles.new(File.join(@tmpdir, 'roo_styles.xml'))
476
+ end
630
477
 
631
- def shared_strings
632
- @shared_strings ||= SharedStrings.new(File.join(@tmpdir, 'roo_sharedStrings.xml'))
633
- end
478
+ def shared_strings
479
+ @shared_strings ||= SharedStrings.new(File.join(@tmpdir, 'roo_sharedStrings.xml'))
480
+ end
634
481
 
635
- def workbook
636
- @workbook ||= Workbook.new(File.join(@tmpdir, "roo_workbook.xml"))
637
- end
482
+ def workbook
483
+ @workbook ||= Workbook.new(File.join(@tmpdir, 'roo_workbook.xml'))
484
+ end
638
485
 
639
- def safe_send(object, method, *args)
640
- object.send(method, *args) if object && object.respond_to?(method)
486
+ def safe_send(object, method, *args)
487
+ object.send(method, *args) if object && object.respond_to?(method)
488
+ end
641
489
  end
642
490
  end