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 +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
|