roo 2.0.0 → 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.
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