roo 1.13.2 → 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/.gitignore +7 -0
- data/.simplecov +4 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +515 -0
- data/Gemfile +16 -10
- data/Guardfile +24 -0
- data/LICENSE +3 -1
- data/README.md +254 -0
- data/Rakefile +23 -23
- data/examples/roo_soap_client.rb +28 -31
- data/examples/roo_soap_server.rb +4 -6
- data/examples/write_me.rb +9 -10
- data/lib/roo/base.rb +334 -395
- data/lib/roo/csv.rb +120 -113
- data/lib/roo/excelx/cell.rb +77 -0
- data/lib/roo/excelx/comments.rb +22 -0
- data/lib/roo/excelx/extractor.rb +22 -0
- data/lib/roo/excelx/relationships.rb +25 -0
- data/lib/roo/excelx/shared_strings.rb +37 -0
- data/lib/roo/excelx/sheet.rb +107 -0
- data/lib/roo/excelx/sheet_doc.rb +200 -0
- data/lib/roo/excelx/styles.rb +64 -0
- data/lib/roo/excelx/workbook.rb +59 -0
- data/lib/roo/excelx.rb +413 -597
- data/lib/roo/font.rb +17 -0
- data/lib/roo/libre_office.rb +5 -0
- data/lib/roo/link.rb +15 -0
- data/lib/roo/{openoffice.rb → open_office.rb} +681 -496
- data/lib/roo/spreadsheet.rb +20 -23
- data/lib/roo/utils.rb +78 -0
- data/lib/roo/version.rb +3 -0
- data/lib/roo.rb +18 -24
- data/roo.gemspec +20 -204
- data/spec/lib/roo/base_spec.rb +1 -4
- data/spec/lib/roo/csv_spec.rb +21 -13
- data/spec/lib/roo/excelx/format_spec.rb +7 -6
- data/spec/lib/roo/excelx_spec.rb +424 -11
- data/spec/lib/roo/libreoffice_spec.rb +16 -6
- data/spec/lib/roo/openoffice_spec.rb +13 -8
- data/spec/lib/roo/spreadsheet_spec.rb +40 -12
- data/spec/lib/roo/utils_spec.rb +106 -0
- data/spec/spec_helper.rb +2 -1
- data/test/test_generic_spreadsheet.rb +117 -139
- data/test/test_helper.rb +9 -56
- data/test/test_roo.rb +274 -478
- metadata +65 -303
- data/CHANGELOG +0 -417
- data/Gemfile.lock +0 -78
- data/README.markdown +0 -126
- data/VERSION +0 -1
- data/lib/roo/excel.rb +0 -355
- data/lib/roo/excel2003xml.rb +0 -300
- data/lib/roo/google.rb +0 -292
- data/lib/roo/roo_rails_helper.rb +0 -83
- data/lib/roo/worksheet.rb +0 -18
- data/scripts/txt2html +0 -67
- data/spec/lib/roo/excel2003xml_spec.rb +0 -15
- data/spec/lib/roo/excel_spec.rb +0 -17
- data/spec/lib/roo/google_spec.rb +0 -64
- data/test/files/1900_base.xls +0 -0
- data/test/files/1900_base.xlsx +0 -0
- data/test/files/1904_base.xls +0 -0
- data/test/files/1904_base.xlsx +0 -0
- data/test/files/Bibelbund.csv +0 -3741
- data/test/files/Bibelbund.ods +0 -0
- data/test/files/Bibelbund.xls +0 -0
- data/test/files/Bibelbund.xlsx +0 -0
- data/test/files/Bibelbund.xml +0 -62518
- data/test/files/Bibelbund1.ods +0 -0
- data/test/files/Pfand_from_windows_phone.xlsx +0 -0
- data/test/files/bad_excel_date.xls +0 -0
- data/test/files/bbu.ods +0 -0
- data/test/files/bbu.xls +0 -0
- data/test/files/bbu.xlsx +0 -0
- data/test/files/bbu.xml +0 -152
- data/test/files/bode-v1.ods.zip +0 -0
- data/test/files/bode-v1.xls.zip +0 -0
- data/test/files/boolean.csv +0 -2
- data/test/files/boolean.ods +0 -0
- data/test/files/boolean.xls +0 -0
- data/test/files/boolean.xlsx +0 -0
- data/test/files/boolean.xml +0 -112
- data/test/files/borders.ods +0 -0
- data/test/files/borders.xls +0 -0
- data/test/files/borders.xlsx +0 -0
- data/test/files/borders.xml +0 -144
- data/test/files/bug-numbered-sheet-names.xlsx +0 -0
- data/test/files/bug-row-column-fixnum-float.xls +0 -0
- data/test/files/bug-row-column-fixnum-float.xml +0 -127
- data/test/files/comments.ods +0 -0
- data/test/files/comments.xls +0 -0
- data/test/files/comments.xlsx +0 -0
- data/test/files/csvtypes.csv +0 -1
- data/test/files/datetime.ods +0 -0
- data/test/files/datetime.xls +0 -0
- data/test/files/datetime.xlsx +0 -0
- data/test/files/datetime.xml +0 -142
- data/test/files/datetime_floatconv.xls +0 -0
- data/test/files/datetime_floatconv.xml +0 -148
- data/test/files/dreimalvier.ods +0 -0
- data/test/files/emptysheets.ods +0 -0
- data/test/files/emptysheets.xls +0 -0
- data/test/files/emptysheets.xlsx +0 -0
- data/test/files/emptysheets.xml +0 -105
- data/test/files/excel2003.xml +0 -21140
- data/test/files/false_encoding.xls +0 -0
- data/test/files/false_encoding.xml +0 -132
- data/test/files/file_item_error.xlsx +0 -0
- data/test/files/formula.ods +0 -0
- data/test/files/formula.xls +0 -0
- data/test/files/formula.xlsx +0 -0
- data/test/files/formula.xml +0 -134
- data/test/files/formula_parse_error.xls +0 -0
- data/test/files/formula_parse_error.xml +0 -1833
- data/test/files/formula_string_error.xlsx +0 -0
- data/test/files/html-escape.ods +0 -0
- data/test/files/link.xls +0 -0
- data/test/files/link.xlsx +0 -0
- data/test/files/matrix.ods +0 -0
- data/test/files/matrix.xls +0 -0
- data/test/files/named_cells.ods +0 -0
- data/test/files/named_cells.xls +0 -0
- data/test/files/named_cells.xlsx +0 -0
- data/test/files/no_spreadsheet_file.txt +0 -1
- data/test/files/numbers1.csv +0 -18
- data/test/files/numbers1.ods +0 -0
- data/test/files/numbers1.xls +0 -0
- data/test/files/numbers1.xlsx +0 -0
- data/test/files/numbers1.xml +0 -312
- data/test/files/numeric-link.xlsx +0 -0
- data/test/files/only_one_sheet.ods +0 -0
- data/test/files/only_one_sheet.xls +0 -0
- data/test/files/only_one_sheet.xlsx +0 -0
- data/test/files/only_one_sheet.xml +0 -67
- data/test/files/paragraph.ods +0 -0
- data/test/files/paragraph.xls +0 -0
- data/test/files/paragraph.xlsx +0 -0
- data/test/files/paragraph.xml +0 -127
- data/test/files/prova.xls +0 -0
- data/test/files/ric.ods +0 -0
- data/test/files/simple_spreadsheet.ods +0 -0
- data/test/files/simple_spreadsheet.xls +0 -0
- data/test/files/simple_spreadsheet.xlsx +0 -0
- data/test/files/simple_spreadsheet.xml +0 -225
- data/test/files/simple_spreadsheet_from_italo.ods +0 -0
- data/test/files/simple_spreadsheet_from_italo.xls +0 -0
- data/test/files/simple_spreadsheet_from_italo.xml +0 -242
- data/test/files/so_datetime.csv +0 -7
- data/test/files/style.ods +0 -0
- data/test/files/style.xls +0 -0
- data/test/files/style.xlsx +0 -0
- data/test/files/style.xml +0 -154
- data/test/files/time-test.csv +0 -2
- data/test/files/time-test.ods +0 -0
- data/test/files/time-test.xls +0 -0
- data/test/files/time-test.xlsx +0 -0
- data/test/files/time-test.xml +0 -131
- data/test/files/type_excel.ods +0 -0
- data/test/files/type_excel.xlsx +0 -0
- data/test/files/type_excelx.ods +0 -0
- data/test/files/type_excelx.xls +0 -0
- data/test/files/type_openoffice.xls +0 -0
- data/test/files/type_openoffice.xlsx +0 -0
- data/test/files/whitespace.ods +0 -0
- data/test/files/whitespace.xls +0 -0
- data/test/files/whitespace.xlsx +0 -0
- data/test/files/whitespace.xml +0 -184
- data/test/rm_sub_test.rb +0 -12
- data/test/rm_test.rb +0 -7
- data/website/index.html +0 -385
- data/website/index.txt +0 -423
- data/website/javascripts/rounded_corners_lite.inc.js +0 -285
- data/website/stylesheets/screen.css +0 -130
- data/website/template.rhtml +0 -48
data/lib/roo/excelx.rb
CHANGED
|
@@ -1,674 +1,490 @@
|
|
|
1
|
-
require 'date'
|
|
2
1
|
require 'nokogiri'
|
|
3
|
-
require '
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
2
|
+
require 'zip/filesystem'
|
|
3
|
+
require 'roo/link'
|
|
4
|
+
require 'roo/utils'
|
|
5
|
+
|
|
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
|
|
52
70
|
else
|
|
53
|
-
:
|
|
71
|
+
:float
|
|
54
72
|
end
|
|
55
|
-
elsif format.include?('h') || format.include?('s')
|
|
56
|
-
:time
|
|
57
|
-
elsif format.include?('%')
|
|
58
|
-
:percentage
|
|
59
|
-
else
|
|
60
|
-
:float
|
|
61
73
|
end
|
|
74
|
+
|
|
75
|
+
module_function :to_type
|
|
62
76
|
end
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
end
|
|
78
|
+
ExceedsMaxError = Class.new(StandardError)
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 = {})
|
|
71
85
|
packed = options[:packed]
|
|
72
|
-
file_warning = options
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
make_tmpdir do |tmpdir|
|
|
81
|
-
filename = download_uri(filename, tmpdir) if uri?(filename)
|
|
82
|
-
filename = unzip(filename, tmpdir) if packed == :zip
|
|
83
|
-
@filename = filename
|
|
84
|
-
unless File.file?(@filename)
|
|
85
|
-
raise IOError, "file #{@filename} does not exist"
|
|
86
|
-
end
|
|
87
|
-
@comments_files = Array.new
|
|
88
|
-
@rels_files = Array.new
|
|
89
|
-
extract_content(tmpdir, @filename)
|
|
90
|
-
@workbook_doc = load_xml(File.join(tmpdir, "roo_workbook.xml"))
|
|
91
|
-
@shared_table = []
|
|
92
|
-
if File.exist?(File.join(tmpdir, 'roo_sharedStrings.xml'))
|
|
93
|
-
@sharedstring_doc = load_xml(File.join(tmpdir, 'roo_sharedStrings.xml'))
|
|
94
|
-
read_shared_strings(@sharedstring_doc)
|
|
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)
|
|
95
94
|
end
|
|
96
|
-
|
|
97
|
-
@
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
|
|
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)
|
|
101
|
+
|
|
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
|
|
101
116
|
end
|
|
102
|
-
@sheet_doc = load_xmls(@sheet_files)
|
|
103
|
-
@comments_doc = load_xmls(@comments_files)
|
|
104
|
-
@rels_doc = load_xmls(@rels_files)
|
|
105
|
-
end
|
|
106
|
-
super(filename, options)
|
|
107
|
-
@formula = Hash.new
|
|
108
|
-
@excelx_type = Hash.new
|
|
109
|
-
@excelx_value = Hash.new
|
|
110
|
-
@s_attribute = Hash.new # TODO: ggf. wieder entfernen nur lokal benoetigt
|
|
111
|
-
@comment = Hash.new
|
|
112
|
-
@comments_read = Hash.new
|
|
113
|
-
@hyperlink = Hash.new
|
|
114
|
-
@hyperlinks_read = Hash.new
|
|
115
|
-
end
|
|
116
117
|
|
|
117
|
-
def method_missing(m,*args)
|
|
118
|
-
# is method name a label name
|
|
119
|
-
read_labels
|
|
120
|
-
if @label.has_key?(m.to_s)
|
|
121
|
-
sheet ||= @default_sheet
|
|
122
|
-
read_cells(sheet)
|
|
123
|
-
row,col = label(m.to_s)
|
|
124
|
-
cell(row,col)
|
|
125
|
-
else
|
|
126
|
-
# call super for methods like #a1
|
|
127
118
|
super
|
|
119
|
+
rescue => e # clean up any temp files, but only if an error was raised
|
|
120
|
+
close
|
|
121
|
+
raise e
|
|
128
122
|
end
|
|
129
|
-
end
|
|
130
123
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if celltype(row,col,sheet) == :date
|
|
140
|
-
yyyy,mm,dd = @cell[sheet][[row,col]].split('-')
|
|
141
|
-
return Date.new(yyyy.to_i,mm.to_i,dd.to_i)
|
|
142
|
-
elsif celltype(row,col,sheet) == :datetime
|
|
143
|
-
date_part,time_part = @cell[sheet][[row,col]].split(' ')
|
|
144
|
-
yyyy,mm,dd = date_part.split('-')
|
|
145
|
-
hh,mi,ss = time_part.split(':')
|
|
146
|
-
return DateTime.civil(yyyy.to_i,mm.to_i,dd.to_i,hh.to_i,mi.to_i,ss.to_i)
|
|
147
|
-
end
|
|
148
|
-
@cell[sheet][[row,col]]
|
|
149
|
-
end
|
|
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)
|
|
127
|
+
else
|
|
128
|
+
# call super for methods like #a1
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
end
|
|
150
132
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
sheet
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
sheet
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
133
|
+
def sheets
|
|
134
|
+
@sheet_names
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def sheet_for(sheet)
|
|
138
|
+
sheet ||= default_sheet
|
|
139
|
+
validate_sheet!(sheet)
|
|
140
|
+
@sheets_by_name[sheet]
|
|
141
|
+
end
|
|
142
|
+
|
|
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)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def row(rownumber, sheet = nil)
|
|
153
|
+
sheet_for(sheet).row(rownumber)
|
|
154
|
+
end
|
|
155
|
+
|
|
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)
|
|
170
161
|
end
|
|
171
|
-
|
|
172
|
-
[]
|
|
162
|
+
sheet_for(sheet).column(column_number)
|
|
173
163
|
end
|
|
174
|
-
end
|
|
175
164
|
|
|
176
|
-
|
|
177
|
-
|
|
165
|
+
# returns the number of the first non-empty row
|
|
166
|
+
def first_row(sheet = nil)
|
|
167
|
+
sheet_for(sheet).first_row
|
|
168
|
+
end
|
|
178
169
|
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
# returns the number of the last non-empty row
|
|
171
|
+
def last_row(sheet = nil)
|
|
172
|
+
sheet_for(sheet).last_row
|
|
181
173
|
end
|
|
182
174
|
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
# returns the number of the first non-empty column
|
|
176
|
+
def first_column(sheet = nil)
|
|
177
|
+
sheet_for(sheet).first_column
|
|
185
178
|
end
|
|
186
179
|
|
|
187
|
-
|
|
188
|
-
|
|
180
|
+
# returns the number of the last non-empty column
|
|
181
|
+
def last_column(sheet = nil)
|
|
182
|
+
sheet_for(sheet).last_column
|
|
189
183
|
end
|
|
190
|
-
end
|
|
191
184
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
sheet
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
s_attribute = s_attribute.to_i
|
|
200
|
-
@style_definitions[s_attribute]
|
|
201
|
-
end
|
|
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))
|
|
191
|
+
end
|
|
202
192
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# * :time
|
|
210
|
-
# * :datetime
|
|
211
|
-
def celltype(row,col,sheet=nil)
|
|
212
|
-
sheet ||= @default_sheet
|
|
213
|
-
read_cells(sheet)
|
|
214
|
-
row,col = normalize(row,col)
|
|
215
|
-
if @formula[sheet][[row,col]]
|
|
216
|
-
return :formula
|
|
217
|
-
else
|
|
218
|
-
@cell_type[sheet][[row,col]]
|
|
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)
|
|
219
199
|
end
|
|
220
|
-
end
|
|
221
200
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
read_cells(sheet)
|
|
229
|
-
row,col = normalize(row,col)
|
|
230
|
-
return @excelx_type[sheet][[row,col]]
|
|
231
|
-
end
|
|
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)
|
|
206
|
+
end
|
|
232
207
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
end
|
|
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]
|
|
213
|
+
end
|
|
214
|
+
end
|
|
241
215
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
attribute2format(s).to_s
|
|
249
|
-
end
|
|
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
|
|
221
|
+
end
|
|
250
222
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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)
|
|
255
234
|
end
|
|
256
|
-
end
|
|
257
235
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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)
|
|
243
|
+
end
|
|
265
244
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return nil,nil,nil
|
|
272
|
-
else
|
|
273
|
-
return @label[labelname][1].to_i,
|
|
274
|
-
Roo::Base.letter_to_number(@label[labelname][2]),
|
|
275
|
-
@label[labelname][0]
|
|
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)
|
|
276
250
|
end
|
|
277
|
-
end
|
|
278
251
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
# read_cells(sheet)
|
|
284
|
-
read_labels
|
|
285
|
-
@label.map do |label|
|
|
286
|
-
[ label[0], # name
|
|
287
|
-
[ label[1][1].to_i, # row
|
|
288
|
-
Roo::Base.letter_to_number(label[1][2]), # column
|
|
289
|
-
label[1][0], # sheet
|
|
290
|
-
] ]
|
|
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)
|
|
291
256
|
end
|
|
292
|
-
end
|
|
293
257
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
297
265
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
row,col = normalize(row,col)
|
|
304
|
-
return nil unless @hyperlink[sheet]
|
|
305
|
-
@hyperlink[sheet][[row,col]]
|
|
306
|
-
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
|
|
307
271
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
read_comments(sheet) unless @comments_read[sheet]
|
|
314
|
-
row,col = normalize(row,col)
|
|
315
|
-
return nil unless @comment[sheet]
|
|
316
|
-
@comment[sheet][[row,col]]
|
|
317
|
-
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)
|
|
318
277
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
sheet ||= @default_sheet
|
|
322
|
-
# read_cells(sheet)
|
|
323
|
-
read_comments(sheet) unless @comments_read[sheet]
|
|
324
|
-
row,col = normalize(row,col)
|
|
325
|
-
comment(row,col) != nil
|
|
326
|
-
end
|
|
278
|
+
[labels[name].row, labels[name].col, labels[name].sheet]
|
|
279
|
+
end
|
|
327
280
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
]
|
|
336
289
|
end
|
|
337
|
-
else
|
|
338
|
-
[]
|
|
339
290
|
end
|
|
340
|
-
end
|
|
341
291
|
|
|
342
|
-
|
|
292
|
+
def hyperlink?(row, col, sheet = nil)
|
|
293
|
+
!!hyperlink(row, col, sheet)
|
|
294
|
+
end
|
|
343
295
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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]
|
|
347
301
|
end
|
|
348
|
-
end
|
|
349
302
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
@cell_type[sheet] ||= {}
|
|
357
|
-
@cell_type[sheet][key] = value_type
|
|
358
|
-
@formula[sheet] ||= {}
|
|
359
|
-
@formula[sheet][key] = formula if formula
|
|
360
|
-
@cell[sheet] ||= {}
|
|
361
|
-
@cell[sheet][key] =
|
|
362
|
-
case @cell_type[sheet][key]
|
|
363
|
-
when :float
|
|
364
|
-
v.to_f
|
|
365
|
-
when :string
|
|
366
|
-
v
|
|
367
|
-
when :date
|
|
368
|
-
(base_date+v.to_i).strftime("%Y-%m-%d")
|
|
369
|
-
when :datetime
|
|
370
|
-
(base_date+v.to_f).strftime("%Y-%m-%d %H:%M:%S")
|
|
371
|
-
when :percentage
|
|
372
|
-
v.to_f
|
|
373
|
-
when :time
|
|
374
|
-
v.to_f*(24*60*60)
|
|
375
|
-
else
|
|
376
|
-
v
|
|
377
|
-
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
|
|
378
309
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
@excelx_value[sheet][key] = excelx_value
|
|
384
|
-
@s_attribute[sheet] ||= {}
|
|
385
|
-
@s_attribute[sheet][key] = s_attribute
|
|
386
|
-
end
|
|
310
|
+
# true, if there is a comment
|
|
311
|
+
def comment?(row, col, sheet = nil)
|
|
312
|
+
!!comment(row, col, sheet)
|
|
313
|
+
end
|
|
387
314
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
validate_sheet!(sheet)
|
|
392
|
-
return if @cells_read[sheet]
|
|
393
|
-
|
|
394
|
-
@sheet_doc[sheets.index(sheet)].xpath("/xmlns:worksheet/xmlns:sheetData/xmlns:row/xmlns:c").each do |c|
|
|
395
|
-
s_attribute = c['s'].to_i # should be here
|
|
396
|
-
# c: <c r="A5" s="2">
|
|
397
|
-
# <v>22606</v>
|
|
398
|
-
# </c>, format: , tmp_type: float
|
|
399
|
-
value_type =
|
|
400
|
-
case c['t']
|
|
401
|
-
when 's'
|
|
402
|
-
:shared
|
|
403
|
-
when 'b'
|
|
404
|
-
:boolean
|
|
405
|
-
# 2011-02-25 BEGIN
|
|
406
|
-
when 'str'
|
|
407
|
-
:string
|
|
408
|
-
# 2011-02-25 END
|
|
409
|
-
# 2011-09-15 BEGIN
|
|
410
|
-
when 'inlineStr'
|
|
411
|
-
:inlinestr
|
|
412
|
-
# 2011-09-15 END
|
|
413
|
-
else
|
|
414
|
-
format = attribute2format(s_attribute)
|
|
415
|
-
Format.to_type(format)
|
|
416
|
-
end
|
|
417
|
-
formula = nil
|
|
418
|
-
c.children.each do |cell|
|
|
419
|
-
case cell.name
|
|
420
|
-
when 'is'
|
|
421
|
-
cell.children.each do |is|
|
|
422
|
-
if is.name == 't'
|
|
423
|
-
inlinestr_content = is.content
|
|
424
|
-
value_type = :string
|
|
425
|
-
v = inlinestr_content
|
|
426
|
-
excelx_type = :string
|
|
427
|
-
y, x = Roo::Base.split_coordinate(c['r'])
|
|
428
|
-
excelx_value = inlinestr_content #cell.content
|
|
429
|
-
set_cell_values(sheet,x,y,0,v,value_type,formula,excelx_type,excelx_value,s_attribute)
|
|
430
|
-
end
|
|
431
|
-
end
|
|
432
|
-
when 'f'
|
|
433
|
-
formula = cell.content
|
|
434
|
-
when 'v'
|
|
435
|
-
if [:time, :datetime].include?(value_type) && cell.content.to_f >= 1.0
|
|
436
|
-
value_type =
|
|
437
|
-
if (cell.content.to_f - cell.content.to_f.floor).abs > 0.000001
|
|
438
|
-
:datetime
|
|
439
|
-
else
|
|
440
|
-
:date
|
|
441
|
-
end
|
|
442
|
-
end
|
|
443
|
-
excelx_type = [:numeric_or_formula,format.to_s]
|
|
444
|
-
excelx_value = cell.content
|
|
445
|
-
v =
|
|
446
|
-
case value_type
|
|
447
|
-
when :shared
|
|
448
|
-
value_type = :string
|
|
449
|
-
excelx_type = :string
|
|
450
|
-
@shared_table[cell.content.to_i]
|
|
451
|
-
when :boolean
|
|
452
|
-
(cell.content.to_i == 1 ? 'TRUE' : 'FALSE')
|
|
453
|
-
when :date
|
|
454
|
-
cell.content
|
|
455
|
-
when :time
|
|
456
|
-
cell.content
|
|
457
|
-
when :datetime
|
|
458
|
-
cell.content
|
|
459
|
-
when :formula
|
|
460
|
-
cell.content.to_f #TODO: !!!!
|
|
461
|
-
when :string
|
|
462
|
-
excelx_type = :string
|
|
463
|
-
cell.content
|
|
464
|
-
else
|
|
465
|
-
value_type = :float
|
|
466
|
-
cell.content
|
|
467
|
-
end
|
|
468
|
-
y, x = Roo::Base.split_coordinate(c['r'])
|
|
469
|
-
set_cell_values(sheet,x,y,0,v,value_type,formula,excelx_type,excelx_value,s_attribute)
|
|
470
|
-
end
|
|
315
|
+
def comments(sheet = nil)
|
|
316
|
+
sheet_for(sheet).comments.map do |(x, y), comment|
|
|
317
|
+
[x, y, comment]
|
|
471
318
|
end
|
|
472
319
|
end
|
|
473
|
-
@cells_read[sheet] = true
|
|
474
|
-
# begin comments
|
|
475
|
-
=begin
|
|
476
|
-
Datei xl/comments1.xml
|
|
477
|
-
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
|
478
|
-
<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
479
|
-
<authors>
|
|
480
|
-
<author />
|
|
481
|
-
</authors>
|
|
482
|
-
<commentList>
|
|
483
|
-
<comment ref="B4" authorId="0">
|
|
484
|
-
<text>
|
|
485
|
-
<r>
|
|
486
|
-
<rPr>
|
|
487
|
-
<sz val="10" />
|
|
488
|
-
<rFont val="Arial" />
|
|
489
|
-
<family val="2" />
|
|
490
|
-
</rPr>
|
|
491
|
-
<t>Kommentar fuer B4</t>
|
|
492
|
-
</r>
|
|
493
|
-
</text>
|
|
494
|
-
</comment>
|
|
495
|
-
<comment ref="B5" authorId="0">
|
|
496
|
-
<text>
|
|
497
|
-
<r>
|
|
498
|
-
<rPr>
|
|
499
|
-
<sz val="10" />
|
|
500
|
-
<rFont val="Arial" />
|
|
501
|
-
<family val="2" />
|
|
502
|
-
</rPr>
|
|
503
|
-
<t>Kommentar fuer B5</t>
|
|
504
|
-
</r>
|
|
505
|
-
</text>
|
|
506
|
-
</comment>
|
|
507
|
-
</commentList>
|
|
508
|
-
</comments>
|
|
509
|
-
=end
|
|
510
|
-
=begin
|
|
511
|
-
if @comments_doc[self.sheets.index(sheet)]
|
|
512
|
-
read_comments(sheet)
|
|
513
|
-
end
|
|
514
|
-
=end
|
|
515
|
-
#end comments
|
|
516
|
-
end
|
|
517
320
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
n = self.sheets.index(sheet)
|
|
523
|
-
return unless @comments_doc[n] #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
524
|
-
@comments_doc[n].xpath("//xmlns:comments/xmlns:commentList/xmlns:comment").each do |comment|
|
|
525
|
-
ref = comment.attributes['ref'].to_s
|
|
526
|
-
row,col = Roo::Base.split_coordinate(ref)
|
|
527
|
-
comment.xpath('./xmlns:text/xmlns:r/xmlns:t').each do |text|
|
|
528
|
-
@comment[sheet] ||= {}
|
|
529
|
-
@comment[sheet][[row,col]] = text.text
|
|
530
|
-
end
|
|
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 }
|
|
531
325
|
end
|
|
532
|
-
@comments_read[sheet] = true
|
|
533
|
-
end
|
|
534
326
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
sheet
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
[r.attribute('Id').text, r]
|
|
543
|
-
end]
|
|
544
|
-
@sheet_doc[n].xpath("/xmlns:worksheet/xmlns:hyperlinks/xmlns:hyperlink").each do |h|
|
|
545
|
-
if rel_element = rels[h.attribute('id').text]
|
|
546
|
-
row,col = Roo::Base.split_coordinate(h.attributes['ref'].to_s)
|
|
547
|
-
@hyperlink[sheet] ||= {}
|
|
548
|
-
@hyperlink[sheet][[row,col]] = rel_element.attribute('Target').text
|
|
549
|
-
end
|
|
327
|
+
private
|
|
328
|
+
|
|
329
|
+
def clean_sheet(sheet)
|
|
330
|
+
@sheets_by_name[sheet].cells.each_pair do |coord, value|
|
|
331
|
+
next unless value.value.is_a?(::String)
|
|
332
|
+
|
|
333
|
+
@sheets_by_name[sheet].cells[coord].value = sanitize_value(value.value)
|
|
550
334
|
end
|
|
551
|
-
end
|
|
552
|
-
@hyperlinks_read[sheet] = true
|
|
553
|
-
end
|
|
554
335
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
# "Sheet1!$C$5"
|
|
558
|
-
sheet, coordinates = defined_name.text.split('!$', 2)
|
|
559
|
-
col,row = coordinates.split('$')
|
|
560
|
-
[defined_name['name'], [sheet,row,col]]
|
|
561
|
-
end]
|
|
562
|
-
end
|
|
336
|
+
@cleaned[sheet] = true
|
|
337
|
+
end
|
|
563
338
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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)
|
|
354
|
+
wb = entries.find { |e| e.name[/workbook.xml$/] }
|
|
355
|
+
fail ArgumentError 'missing required workbook file' if wb.nil?
|
|
356
|
+
|
|
357
|
+
wb.extract(path)
|
|
358
|
+
workbook_doc = Roo::Utils.load_xml(path).remove_namespaces!
|
|
359
|
+
workbook_doc.xpath('//sheet').map { |s| s.attributes['id'].value }
|
|
360
|
+
end
|
|
570
361
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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'
|
|
384
|
+
|
|
385
|
+
relationships = rels_doc.xpath('//Relationship').select do |relationship|
|
|
386
|
+
relationship.attributes['Type'].value == worksheet_type
|
|
387
|
+
end
|
|
594
388
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
395
|
+
end
|
|
600
396
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
605
405
|
end
|
|
606
|
-
end
|
|
607
406
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
407
|
+
# Extracts all needed files from the zip file
|
|
408
|
+
def process_zipfile(zipfilename_or_stream)
|
|
409
|
+
@sheet_files = []
|
|
410
|
+
|
|
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
|
|
618
419
|
end
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
420
|
+
process_zipfile_entries entries
|
|
421
|
+
ensure
|
|
422
|
+
stream.close
|
|
622
423
|
end
|
|
623
424
|
end
|
|
624
|
-
@shared_table << shared_table_entry
|
|
625
425
|
end
|
|
626
|
-
end
|
|
627
426
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
640
471
|
end
|
|
641
472
|
end
|
|
642
473
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
@cellXfs << xf['numFmtId']
|
|
646
|
-
@style_definitions << fonts[xf['fontId'].to_i]
|
|
647
|
-
end
|
|
474
|
+
def styles
|
|
475
|
+
@styles ||= Styles.new(File.join(@tmpdir, 'roo_styles.xml'))
|
|
648
476
|
end
|
|
649
|
-
end
|
|
650
477
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
@numFmts[id] || Format::STANDARD_FORMATS[id.to_i]
|
|
655
|
-
end
|
|
478
|
+
def shared_strings
|
|
479
|
+
@shared_strings ||= SharedStrings.new(File.join(@tmpdir, 'roo_sharedStrings.xml'))
|
|
480
|
+
end
|
|
656
481
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
482
|
+
def workbook
|
|
483
|
+
@workbook ||= Workbook.new(File.join(@tmpdir, 'roo_workbook.xml'))
|
|
484
|
+
end
|
|
660
485
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
# http://msdn.microsoft.com/en-us/library/ff530155(v=office.12).aspx
|
|
664
|
-
def read_base_date
|
|
665
|
-
base_date = Date.new(1899,12,30)
|
|
666
|
-
@workbook_doc.xpath("//xmlns:workbookPr").map do |workbookPr|
|
|
667
|
-
if workbookPr["date1904"] && workbookPr["date1904"] =~ /true|1/i
|
|
668
|
-
base_date = Date.new(1904,01,01)
|
|
669
|
-
end
|
|
486
|
+
def safe_send(object, method, *args)
|
|
487
|
+
object.send(method, *args) if object && object.respond_to?(method)
|
|
670
488
|
end
|
|
671
|
-
base_date
|
|
672
489
|
end
|
|
673
|
-
|
|
674
|
-
end # class
|
|
490
|
+
end
|