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 +4 -4
- data/CHANGELOG.md +16 -1
- data/lib/roo/base.rb +43 -19
- data/lib/roo/excelx.rb +390 -542
- data/lib/roo/excelx/cell.rb +77 -0
- data/lib/roo/excelx/comments.rb +9 -11
- data/lib/roo/excelx/extractor.rb +12 -10
- data/lib/roo/excelx/relationships.rb +13 -14
- data/lib/roo/excelx/shared_strings.rb +19 -22
- data/lib/roo/excelx/sheet.rb +107 -0
- data/lib/roo/excelx/sheet_doc.rb +98 -100
- data/lib/roo/excelx/styles.rb +42 -40
- data/lib/roo/excelx/workbook.rb +36 -36
- data/lib/roo/open_office.rb +4 -1
- data/lib/roo/version.rb +1 -1
- data/spec/lib/roo/excelx_spec.rb +36 -0
- data/spec/lib/roo/openoffice_spec.rb +11 -0
- data/test/test_generic_spreadsheet.rb +104 -78
- data/test/test_roo.rb +22 -1
- metadata +5 -4
- data/scripts/txt2html +0 -67
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db86b4586034783940303cfd17a7ee5c60277a51
|
4
|
+
data.tar.gz: 2cf9222f2f2759f164164f745c72abe6b901df98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 442e05a8bffdc839c45a8684e8882f4327098387617b76027b6b5c0543c2a7b4be284c511c35f8333c9bc1147e31350d1f6f67174fc62e5b7fc45520ab0ad307
|
7
|
+
data.tar.gz: 4ba67602e5772cae52f41ea0357ddd6835d25c35be57797307a6194a46ad977d7922b884b4fa826c91efd8533c6f3e56995e8cb0a13146a32e4a3ee7d5e42ae9
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,19 @@
|
|
1
|
-
## [2.0.
|
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)
|
data/lib/roo/base.rb
CHANGED
@@ -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
|
347
|
-
|
348
|
-
|
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
|
-
|
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.
|
690
|
+
%("#{onecell.gsub('"', '""')}") unless onecell.empty?
|
667
691
|
when :boolean
|
668
|
-
%("#{onecell.
|
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.
|
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.
|
719
|
+
%("#{onecell.url.gsub('"', '""')}")
|
696
720
|
else
|
697
721
|
fail "unhandled celltype #{celltype(row, col, sheet)}"
|
698
722
|
end || ''
|
data/lib/roo/excelx.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
:
|
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
|
-
|
75
|
-
end
|
78
|
+
ExceedsMaxError = Class.new(StandardError)
|
76
79
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
def initialize(
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
104
|
-
|
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
|
-
|
107
|
-
|
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
|
-
|
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
|
114
|
-
|
115
|
-
|
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
|
-
|
128
|
+
# call super for methods like #a1
|
129
|
+
super
|
128
130
|
end
|
129
131
|
end
|
130
132
|
|
131
|
-
def
|
132
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
@
|
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
|
-
|
156
|
-
|
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
|
160
|
-
|
152
|
+
def row(rownumber, sheet = nil)
|
153
|
+
sheet_for(sheet).row(rownumber)
|
161
154
|
end
|
162
155
|
|
163
|
-
#
|
164
|
-
#
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
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
|
-
|
166
|
+
def first_row(sheet = nil)
|
167
|
+
sheet_for(sheet).first_row
|
190
168
|
end
|
191
169
|
|
192
|
-
|
193
|
-
|
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
|
-
|
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
|
-
|
181
|
+
def last_column(sheet = nil)
|
182
|
+
sheet_for(sheet).last_column
|
204
183
|
end
|
205
184
|
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
212
|
-
|
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
|
-
|
216
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
280
|
-
|
281
|
-
|
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
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
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
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
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
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
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
|
-
|
431
|
-
|
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
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
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
|
-
|
450
|
-
|
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
|
-
|
462
|
-
|
463
|
-
|
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
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
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
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
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
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
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
|
-
|
485
|
-
|
486
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
499
|
-
|
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
|
-
@
|
336
|
+
@cleaned[sheet] = true
|
503
337
|
end
|
504
338
|
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
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
|
-
|
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
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
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
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
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
|
-
|
577
|
-
|
578
|
-
|
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
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
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
|
-
|
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
|
-
|
628
|
-
|
629
|
-
|
474
|
+
def styles
|
475
|
+
@styles ||= Styles.new(File.join(@tmpdir, 'roo_styles.xml'))
|
476
|
+
end
|
630
477
|
|
631
|
-
|
632
|
-
|
633
|
-
|
478
|
+
def shared_strings
|
479
|
+
@shared_strings ||= SharedStrings.new(File.join(@tmpdir, 'roo_sharedStrings.xml'))
|
480
|
+
end
|
634
481
|
|
635
|
-
|
636
|
-
|
637
|
-
|
482
|
+
def workbook
|
483
|
+
@workbook ||= Workbook.new(File.join(@tmpdir, 'roo_workbook.xml'))
|
484
|
+
end
|
638
485
|
|
639
|
-
|
640
|
-
|
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
|