culturecode-roo 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.
- data/.gitignore +7 -0
- data/.simplecov +4 -0
- data/.travis.yml +13 -0
- data/CHANGELOG +438 -0
- data/Gemfile +24 -0
- data/Guardfile +24 -0
- data/LICENSE +22 -0
- data/README.md +121 -0
- data/Rakefile +23 -0
- data/examples/roo_soap_client.rb +50 -0
- data/examples/roo_soap_server.rb +26 -0
- data/examples/write_me.rb +31 -0
- data/lib/roo.rb +28 -0
- data/lib/roo/base.rb +717 -0
- data/lib/roo/csv.rb +110 -0
- data/lib/roo/excelx.rb +542 -0
- data/lib/roo/excelx/comments.rb +23 -0
- data/lib/roo/excelx/extractor.rb +20 -0
- data/lib/roo/excelx/relationships.rb +26 -0
- data/lib/roo/excelx/shared_strings.rb +40 -0
- data/lib/roo/excelx/sheet_doc.rb +175 -0
- data/lib/roo/excelx/styles.rb +62 -0
- data/lib/roo/excelx/workbook.rb +59 -0
- 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/open_office.rb +652 -0
- data/lib/roo/spreadsheet.rb +31 -0
- data/lib/roo/utils.rb +81 -0
- data/lib/roo/version.rb +3 -0
- data/roo.gemspec +27 -0
- data/scripts/txt2html +67 -0
- data/spec/fixtures/vcr_cassettes/google_drive.yml +165 -0
- data/spec/fixtures/vcr_cassettes/google_drive_access_token.yml +73 -0
- data/spec/fixtures/vcr_cassettes/google_drive_set.yml +857 -0
- data/spec/lib/roo/base_spec.rb +4 -0
- data/spec/lib/roo/csv_spec.rb +48 -0
- data/spec/lib/roo/excelx/format_spec.rb +51 -0
- data/spec/lib/roo/excelx_spec.rb +363 -0
- data/spec/lib/roo/libreoffice_spec.rb +13 -0
- data/spec/lib/roo/openoffice_spec.rb +15 -0
- data/spec/lib/roo/spreadsheet_spec.rb +88 -0
- data/spec/lib/roo/utils_spec.rb +105 -0
- data/spec/spec_helper.rb +9 -0
- data/test/all_ss.rb +11 -0
- data/test/files/1900_base.xlsx +0 -0
- data/test/files/1904_base.xlsx +0 -0
- data/test/files/Bibelbund.csv +3741 -0
- data/test/files/Bibelbund.ods +0 -0
- data/test/files/Bibelbund.xlsx +0 -0
- data/test/files/Bibelbund1.ods +0 -0
- data/test/files/Pfand_from_windows_phone.xlsx +0 -0
- data/test/files/advanced_header.ods +0 -0
- data/test/files/bbu.ods +0 -0
- data/test/files/bbu.xlsx +0 -0
- data/test/files/bode-v1.ods.zip +0 -0
- data/test/files/bode-v1.xls.zip +0 -0
- data/test/files/boolean.csv +2 -0
- data/test/files/boolean.ods +0 -0
- data/test/files/boolean.xlsx +0 -0
- data/test/files/borders.ods +0 -0
- data/test/files/borders.xlsx +0 -0
- data/test/files/bug-numbered-sheet-names.xlsx +0 -0
- data/test/files/comments.ods +0 -0
- data/test/files/comments.xlsx +0 -0
- data/test/files/csvtypes.csv +1 -0
- data/test/files/datetime.ods +0 -0
- data/test/files/datetime.xlsx +0 -0
- data/test/files/dreimalvier.ods +0 -0
- data/test/files/emptysheets.ods +0 -0
- data/test/files/emptysheets.xlsx +0 -0
- data/test/files/encrypted-letmein.ods +0 -0
- data/test/files/file_item_error.xlsx +0 -0
- data/test/files/formula.ods +0 -0
- data/test/files/formula.xlsx +0 -0
- data/test/files/formula_string_error.xlsx +0 -0
- data/test/files/html-escape.ods +0 -0
- data/test/files/link.csv +1 -0
- data/test/files/link.xlsx +0 -0
- data/test/files/matrix.ods +0 -0
- data/test/files/named_cells.ods +0 -0
- data/test/files/named_cells.xlsx +0 -0
- data/test/files/no_spreadsheet_file.txt +1 -0
- data/test/files/numbers-export.xlsx +0 -0
- data/test/files/numbers1.csv +18 -0
- data/test/files/numbers1.ods +0 -0
- data/test/files/numbers1.xlsx +0 -0
- data/test/files/numbers1withnull.xlsx +0 -0
- data/test/files/numeric-link.xlsx +0 -0
- data/test/files/only_one_sheet.ods +0 -0
- data/test/files/only_one_sheet.xlsx +0 -0
- data/test/files/paragraph.ods +0 -0
- data/test/files/paragraph.xlsx +0 -0
- data/test/files/ric.ods +0 -0
- data/test/files/sheet1.xml +109 -0
- data/test/files/simple_spreadsheet.ods +0 -0
- data/test/files/simple_spreadsheet.xlsx +0 -0
- data/test/files/simple_spreadsheet_from_italo.ods +0 -0
- data/test/files/so_datetime.csv +8 -0
- data/test/files/style.ods +0 -0
- data/test/files/style.xlsx +0 -0
- data/test/files/time-test.csv +2 -0
- data/test/files/time-test.ods +0 -0
- data/test/files/time-test.xlsx +0 -0
- 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_openoffice.xlsx +0 -0
- data/test/files/whitespace.ods +0 -0
- data/test/files/whitespace.xlsx +0 -0
- data/test/test_generic_spreadsheet.rb +211 -0
- data/test/test_helper.rb +58 -0
- data/test/test_roo.rb +1977 -0
- metadata +329 -0
data/lib/roo/csv.rb
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
require 'time'
|
|
3
|
+
|
|
4
|
+
# The CSV class can read csv files (must be separated with commas) which then
|
|
5
|
+
# can be handled like spreadsheets. This means you can access cells like A5
|
|
6
|
+
# within these files.
|
|
7
|
+
# The CSV class provides only string objects. If you want conversions to other
|
|
8
|
+
# types you have to do it yourself.
|
|
9
|
+
#
|
|
10
|
+
# You can pass options to the underlying CSV parse operation, via the
|
|
11
|
+
# :csv_options option.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
class Roo::CSV < Roo::Base
|
|
15
|
+
|
|
16
|
+
attr_reader :filename
|
|
17
|
+
|
|
18
|
+
# Returns an array with the names of the sheets. In CSV class there is only
|
|
19
|
+
# one dummy sheet, because a csv file cannot have more than one sheet.
|
|
20
|
+
def sheets
|
|
21
|
+
['default']
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cell(row, col, sheet=nil)
|
|
25
|
+
sheet ||= default_sheet
|
|
26
|
+
read_cells(sheet)
|
|
27
|
+
@cell[normalize(row,col)]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def celltype(row, col, sheet=nil)
|
|
31
|
+
sheet ||= default_sheet
|
|
32
|
+
read_cells(sheet)
|
|
33
|
+
@cell_type[normalize(row,col)]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def cell_postprocessing(row,col,value)
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def csv_options
|
|
41
|
+
@options[:csv_options] || {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
TYPE_MAP = {
|
|
47
|
+
String => :string,
|
|
48
|
+
Float => :float,
|
|
49
|
+
Date => :date,
|
|
50
|
+
DateTime => :datetime,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def celltype_class(value)
|
|
54
|
+
TYPE_MAP[value.class]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def each_row(options, &block)
|
|
58
|
+
if uri?(filename)
|
|
59
|
+
make_tmpdir do |tmpdir|
|
|
60
|
+
tmp_filename = download_uri(filename, tmpdir)
|
|
61
|
+
CSV.foreach(tmp_filename, options, &block)
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
CSV.foreach(filename, options, &block)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read_cells(sheet = default_sheet)
|
|
69
|
+
sheet ||= default_sheet
|
|
70
|
+
return if @cells_read[sheet]
|
|
71
|
+
@first_row[sheet] = 1
|
|
72
|
+
@last_row[sheet] = 0
|
|
73
|
+
@first_column[sheet] = 1
|
|
74
|
+
@last_column[sheet] = 1
|
|
75
|
+
rownum = 1
|
|
76
|
+
each_row csv_options do |row|
|
|
77
|
+
row.each_with_index do |elem,i|
|
|
78
|
+
@cell[[rownum,i+1]] = cell_postprocessing rownum,i+1, elem
|
|
79
|
+
@cell_type[[rownum,i+1]] = celltype_class @cell[[rownum,i+1]]
|
|
80
|
+
if i+1 > @last_column[sheet]
|
|
81
|
+
@last_column[sheet] += 1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
rownum += 1
|
|
85
|
+
@last_row[sheet] += 1
|
|
86
|
+
end
|
|
87
|
+
@cells_read[sheet] = true
|
|
88
|
+
#-- adjust @first_row if neccessary
|
|
89
|
+
while !row(@first_row[sheet]).any? and @first_row[sheet] < @last_row[sheet]
|
|
90
|
+
@first_row[sheet] += 1
|
|
91
|
+
end
|
|
92
|
+
#-- adjust @last_row if neccessary
|
|
93
|
+
while !row(@last_row[sheet]).any? and @last_row[sheet] and
|
|
94
|
+
@last_row[sheet] > @first_row[sheet]
|
|
95
|
+
@last_row[sheet] -= 1
|
|
96
|
+
end
|
|
97
|
+
#-- adjust @first_column if neccessary
|
|
98
|
+
while !column(@first_column[sheet]).any? and
|
|
99
|
+
@first_column[sheet] and
|
|
100
|
+
@first_column[sheet] < @last_column[sheet]
|
|
101
|
+
@first_column[sheet] += 1
|
|
102
|
+
end
|
|
103
|
+
#-- adjust @last_column if neccessary
|
|
104
|
+
while !column(@last_column[sheet]).any? and
|
|
105
|
+
@last_column[sheet] and
|
|
106
|
+
@last_column[sheet] > @first_column[sheet]
|
|
107
|
+
@last_column[sheet] -= 1
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/roo/excelx.rb
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
require 'nokogiri'
|
|
3
|
+
require 'roo/link'
|
|
4
|
+
require 'roo/utils'
|
|
5
|
+
require 'zip/filesystem'
|
|
6
|
+
|
|
7
|
+
class Roo::Excelx < Roo::Base
|
|
8
|
+
autoload :Workbook, 'roo/excelx/workbook'
|
|
9
|
+
autoload :SharedStrings, 'roo/excelx/shared_strings'
|
|
10
|
+
autoload :Styles, 'roo/excelx/styles'
|
|
11
|
+
|
|
12
|
+
autoload :Relationships, 'roo/excelx/relationships'
|
|
13
|
+
autoload :Comments, 'roo/excelx/comments'
|
|
14
|
+
autoload :SheetDoc, 'roo/excelx/sheet_doc'
|
|
15
|
+
|
|
16
|
+
module Format
|
|
17
|
+
EXCEPTIONAL_FORMATS = {
|
|
18
|
+
'h:mm am/pm' => :date,
|
|
19
|
+
'h:mm:ss am/pm' => :date,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
STANDARD_FORMATS = {
|
|
23
|
+
0 => 'General',
|
|
24
|
+
1 => '0',
|
|
25
|
+
2 => '0.00',
|
|
26
|
+
3 => '#,##0',
|
|
27
|
+
4 => '#,##0.00',
|
|
28
|
+
9 => '0%',
|
|
29
|
+
10 => '0.00%',
|
|
30
|
+
11 => '0.00E+00',
|
|
31
|
+
12 => '# ?/?',
|
|
32
|
+
13 => '# ??/??',
|
|
33
|
+
14 => 'mm-dd-yy',
|
|
34
|
+
15 => 'd-mmm-yy',
|
|
35
|
+
16 => 'd-mmm',
|
|
36
|
+
17 => 'mmm-yy',
|
|
37
|
+
18 => 'h:mm AM/PM',
|
|
38
|
+
19 => 'h:mm:ss AM/PM',
|
|
39
|
+
20 => 'h:mm',
|
|
40
|
+
21 => 'h:mm:ss',
|
|
41
|
+
22 => 'm/d/yy h:mm',
|
|
42
|
+
37 => '#,##0 ;(#,##0)',
|
|
43
|
+
38 => '#,##0 ;[Red](#,##0)',
|
|
44
|
+
39 => '#,##0.00;(#,##0.00)',
|
|
45
|
+
40 => '#,##0.00;[Red](#,##0.00)',
|
|
46
|
+
45 => 'mm:ss',
|
|
47
|
+
46 => '[h]:mm:ss',
|
|
48
|
+
47 => 'mmss.0',
|
|
49
|
+
48 => '##0.0E+0',
|
|
50
|
+
49 => '@',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def to_type(format)
|
|
54
|
+
format = format.to_s.downcase
|
|
55
|
+
if format == 'general'
|
|
56
|
+
:string
|
|
57
|
+
elsif type = EXCEPTIONAL_FORMATS[format]
|
|
58
|
+
type
|
|
59
|
+
elsif format.include?('#')
|
|
60
|
+
:float
|
|
61
|
+
elsif !format.match(/d+(?![\]])/).nil? || format.include?('y')
|
|
62
|
+
if format.include?('h') || format.include?('s')
|
|
63
|
+
:datetime
|
|
64
|
+
else
|
|
65
|
+
:date
|
|
66
|
+
end
|
|
67
|
+
elsif format.include?('h') || format.include?('s')
|
|
68
|
+
:time
|
|
69
|
+
elsif format.include?('%')
|
|
70
|
+
:percentage
|
|
71
|
+
else
|
|
72
|
+
:float
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
module_function :to_type
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class Cell
|
|
80
|
+
attr_reader :type, :formula, :value, :excelx_type, :excelx_value, :style, :hyperlink, :coordinate
|
|
81
|
+
|
|
82
|
+
def initialize(value, type, formula, excelx_type, excelx_value, style, hyperlink, base_date, coordinate)
|
|
83
|
+
@type = type
|
|
84
|
+
@formula = formula
|
|
85
|
+
@base_date = base_date if [:date, :datetime].include?(@type)
|
|
86
|
+
@excelx_type = excelx_type
|
|
87
|
+
@excelx_value = excelx_value
|
|
88
|
+
@style = style
|
|
89
|
+
@value = type_cast_value(value)
|
|
90
|
+
@value = Roo::Link.new(hyperlink, @value.to_s) if hyperlink
|
|
91
|
+
@coordinate = coordinate
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def type
|
|
95
|
+
if @formula
|
|
96
|
+
:formula
|
|
97
|
+
elsif @value.is_a?(Roo::Link)
|
|
98
|
+
:link
|
|
99
|
+
else
|
|
100
|
+
@type
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class Coordinate
|
|
105
|
+
attr_accessor :row, :column
|
|
106
|
+
|
|
107
|
+
def initialize(row, column)
|
|
108
|
+
@row, @column = row, column
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def type_cast_value(value)
|
|
115
|
+
case @type
|
|
116
|
+
when :float, :percentage
|
|
117
|
+
value.to_f
|
|
118
|
+
when :date
|
|
119
|
+
yyyy,mm,dd = (@base_date+value.to_i).strftime("%Y-%m-%d").split('-')
|
|
120
|
+
Date.new(yyyy.to_i,mm.to_i,dd.to_i)
|
|
121
|
+
when :datetime
|
|
122
|
+
create_datetime_from((@base_date+value.to_f.round(6)).strftime("%Y-%m-%d %H:%M:%S.%N"))
|
|
123
|
+
when :time
|
|
124
|
+
value.to_f*(24*60*60)
|
|
125
|
+
when :string
|
|
126
|
+
value
|
|
127
|
+
else
|
|
128
|
+
value
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def create_datetime_from(datetime_string)
|
|
133
|
+
date_part,time_part = round_time_from(datetime_string).split(' ')
|
|
134
|
+
yyyy,mm,dd = date_part.split('-')
|
|
135
|
+
hh,mi,ss = time_part.split(':')
|
|
136
|
+
DateTime.civil(yyyy.to_i,mm.to_i,dd.to_i,hh.to_i,mi.to_i,ss.to_i)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def round_time_from(datetime_string)
|
|
140
|
+
date_part,time_part = datetime_string.split(' ')
|
|
141
|
+
yyyy,mm,dd = date_part.split('-')
|
|
142
|
+
hh,mi,ss = time_part.split(':')
|
|
143
|
+
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")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
class Sheet
|
|
148
|
+
def initialize(name, rels_path, sheet_path, comments_path, styles, shared_strings, workbook)
|
|
149
|
+
@name = name
|
|
150
|
+
@rels = Relationships.new(rels_path)
|
|
151
|
+
@comments = Comments.new(comments_path)
|
|
152
|
+
@styles = styles
|
|
153
|
+
@sheet = SheetDoc.new(sheet_path, @rels, @styles, shared_strings, workbook)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def cells
|
|
157
|
+
@cells ||= @sheet.cells(@rels)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def present_cells
|
|
161
|
+
@present_cells ||= cells.select {|key, cell| cell && cell.value }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Yield each row as array of Excelx::Cell objects
|
|
165
|
+
# accepts options max_rows (int) (offset by 1 for header)
|
|
166
|
+
# and pad_cells (boolean)
|
|
167
|
+
def each_row(options = {}, &block)
|
|
168
|
+
row_count = 0
|
|
169
|
+
@sheet.each_row_streaming do |row|
|
|
170
|
+
break if options[:max_rows] && row_count == options[:max_rows] + 1
|
|
171
|
+
block.call(cells_for_row_element(row, options)) if block_given?
|
|
172
|
+
row_count += 1
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def row(row_number)
|
|
177
|
+
first_column.upto(last_column).map do |col|
|
|
178
|
+
cells[[row_number,col]]
|
|
179
|
+
end.map {|cell| cell && cell.value }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def column(col_number)
|
|
183
|
+
first_row.upto(last_row).map do |row|
|
|
184
|
+
cells[[row,col_number]]
|
|
185
|
+
end.map {|cell| cell && cell.value }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# returns the number of the first non-empty row
|
|
189
|
+
def first_row
|
|
190
|
+
@first_row ||= present_cells.keys.map {|row, col| row }.min
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def last_row
|
|
194
|
+
@last_row ||= present_cells.keys.map {|row, col| row }.max
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# returns the number of the first non-empty column
|
|
198
|
+
def first_column(sheet=nil)
|
|
199
|
+
@first_column ||= present_cells.keys.map {|row, col| col }.min
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# returns the number of the last non-empty column
|
|
203
|
+
def last_column(sheet=nil)
|
|
204
|
+
@last_column ||= present_cells.keys.map {|row, col| col }.max
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def excelx_format(key)
|
|
208
|
+
@styles.style_format(cells[key].style).to_s
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def hyperlinks
|
|
212
|
+
@hyperlinks ||= @sheet.hyperlinks(@rels)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def comments
|
|
216
|
+
@comments.comments
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def dimensions
|
|
220
|
+
@sheet.dimensions
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
# Take an xml row and return an array of Excelx::Cell objects
|
|
226
|
+
# optionally pad array to header width(assumed 1st row).
|
|
227
|
+
# takes option pad_cells (boolean) defaults false
|
|
228
|
+
def cells_for_row_element(row_element, options = {})
|
|
229
|
+
return [] unless row_element
|
|
230
|
+
cell_col = 0
|
|
231
|
+
cells = []
|
|
232
|
+
@sheet.each_cell(row_element) do |cell|
|
|
233
|
+
cells.concat(pad_cells(cell, cell_col)) if options[:pad_cells]
|
|
234
|
+
cells << cell
|
|
235
|
+
cell_col = cell.coordinate.column
|
|
236
|
+
end
|
|
237
|
+
cells
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def pad_cells(cell, last_column)
|
|
241
|
+
pad = []
|
|
242
|
+
(cell.coordinate.column - 1 - last_column).times { pad << nil }
|
|
243
|
+
pad
|
|
244
|
+
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
|
+
|
|
258
|
+
file_type_check(filename,'.xlsx','an Excel-xlsx', file_warning, packed)
|
|
259
|
+
|
|
260
|
+
@tmpdir = make_tmpdir(filename.split('/').last, options[:tmpdir_root])
|
|
261
|
+
@filename = local_filename(filename, @tmpdir, packed)
|
|
262
|
+
@comments_files = []
|
|
263
|
+
@rels_files = []
|
|
264
|
+
process_zipfile(@tmpdir, @filename)
|
|
265
|
+
|
|
266
|
+
@sheet_names = workbook.sheets.map { |sheet| sheet['name'] }
|
|
267
|
+
@sheets = []
|
|
268
|
+
@sheets_by_name = Hash[@sheet_names.map.with_index do |sheet_name, n|
|
|
269
|
+
@sheets[n] = Sheet.new(sheet_name, @rels_files[n], @sheet_files[n], @comments_files[n], styles, shared_strings, workbook)
|
|
270
|
+
[sheet_name, @sheets[n]]
|
|
271
|
+
end]
|
|
272
|
+
|
|
273
|
+
if cell_max
|
|
274
|
+
cell_count = ::Roo::Utils.num_cells_in_range(sheet_for(options.delete(:sheet)).dimensions)
|
|
275
|
+
raise ExceedsMaxError.new("Excel file exceeds cell maximum: #{cell_count} > #{cell_max}") if cell_count > cell_max
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
super
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def method_missing(method,*args)
|
|
282
|
+
if label = workbook.defined_names[method.to_s]
|
|
283
|
+
sheet_for(label.sheet).cells[label.key].value
|
|
284
|
+
else
|
|
285
|
+
# call super for methods like #a1
|
|
286
|
+
super
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def sheets
|
|
291
|
+
@sheet_names
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def sheet_for(sheet)
|
|
295
|
+
sheet ||= default_sheet
|
|
296
|
+
validate_sheet!(sheet)
|
|
297
|
+
@sheets_by_name[sheet]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Returns the content of a spreadsheet-cell.
|
|
301
|
+
# (1,1) is the upper left corner.
|
|
302
|
+
# (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
|
|
303
|
+
# cell at the first line and first row.
|
|
304
|
+
def cell(row, col, sheet=nil)
|
|
305
|
+
key = normalize(row,col)
|
|
306
|
+
cell = sheet_for(sheet).cells[key]
|
|
307
|
+
cell.value if cell
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def row(rownumber,sheet=nil)
|
|
311
|
+
sheet_for(sheet).row(rownumber)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# returns all values in this column as an array
|
|
315
|
+
# column numbers are 1,2,3,... like in the spreadsheet
|
|
316
|
+
def column(column_number,sheet=nil)
|
|
317
|
+
if column_number.is_a?(::String)
|
|
318
|
+
column_number = ::Roo::Utils.letter_to_number(column_number)
|
|
319
|
+
end
|
|
320
|
+
sheet_for(sheet).column(column_number)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# returns the number of the first non-empty row
|
|
324
|
+
def first_row(sheet=nil)
|
|
325
|
+
sheet_for(sheet).first_row
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# returns the number of the last non-empty row
|
|
329
|
+
def last_row(sheet=nil)
|
|
330
|
+
sheet_for(sheet).last_row
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# returns the number of the first non-empty column
|
|
334
|
+
def first_column(sheet=nil)
|
|
335
|
+
sheet_for(sheet).first_column
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# returns the number of the last non-empty column
|
|
339
|
+
def last_column(sheet=nil)
|
|
340
|
+
sheet_for(sheet).last_column
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# set a cell to a certain value
|
|
344
|
+
# (this will not be saved back to the spreadsheet file!)
|
|
345
|
+
def set(row,col,value, sheet = nil) #:nodoc:
|
|
346
|
+
key = normalize(row,col)
|
|
347
|
+
cell_type = cell_type_by_value(value)
|
|
348
|
+
sheet_for(sheet).cells[key] = Cell.new(value, cell_type, nil, cell_type, value, nil, nil, nil, Cell::Coordinate.new(row, col))
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# Returns the formula at (row,col).
|
|
353
|
+
# Returns nil if there is no formula.
|
|
354
|
+
# The method #formula? checks if there is a formula.
|
|
355
|
+
def formula(row,col,sheet=nil)
|
|
356
|
+
key = normalize(row,col)
|
|
357
|
+
sheet_for(sheet).cells[key].formula
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Predicate methods really should return a boolean
|
|
361
|
+
# value. Hopefully no one was relying on the fact that this
|
|
362
|
+
# previously returned either nil/formula
|
|
363
|
+
def formula?(*args)
|
|
364
|
+
!!formula(*args)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# returns each formula in the selected sheet as an array of tuples in following format
|
|
368
|
+
# [[row, col, formula], [row, col, formula],...]
|
|
369
|
+
def formulas(sheet=nil)
|
|
370
|
+
sheet_for(sheet).cells.select {|_, cell| cell.formula }.map do |(x, y), cell|
|
|
371
|
+
[x, y, cell.formula]
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Given a cell, return the cell's style
|
|
376
|
+
def font(row, col, sheet=nil)
|
|
377
|
+
key = normalize(row,col)
|
|
378
|
+
styles.definitions[sheet_for(sheet).cells[key].style]
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# returns the type of a cell:
|
|
382
|
+
# * :float
|
|
383
|
+
# * :string,
|
|
384
|
+
# * :date
|
|
385
|
+
# * :percentage
|
|
386
|
+
# * :formula
|
|
387
|
+
# * :time
|
|
388
|
+
# * :datetime
|
|
389
|
+
def celltype(row,col,sheet=nil)
|
|
390
|
+
key = normalize(row, col)
|
|
391
|
+
sheet_for(sheet).cells[key].type
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# returns the internal type of an excel cell
|
|
395
|
+
# * :numeric_or_formula
|
|
396
|
+
# * :string
|
|
397
|
+
# Note: this is only available within the Excelx class
|
|
398
|
+
def excelx_type(row,col,sheet=nil)
|
|
399
|
+
key = normalize(row,col)
|
|
400
|
+
sheet_for(sheet).cells[key].excelx_type
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# returns the internal value of an excelx cell
|
|
404
|
+
# Note: this is only available within the Excelx class
|
|
405
|
+
def excelx_value(row,col,sheet=nil)
|
|
406
|
+
key = normalize(row,col)
|
|
407
|
+
sheet_for(sheet).cells[key].excelx_value
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# returns the internal format of an excel cell
|
|
411
|
+
def excelx_format(row,col,sheet=nil)
|
|
412
|
+
key = normalize(row,col)
|
|
413
|
+
sheet_for(sheet).excelx_format(key)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def empty?(row,col,sheet=nil)
|
|
417
|
+
sheet = sheet_for(sheet)
|
|
418
|
+
key = normalize(row,col)
|
|
419
|
+
cell = sheet.cells[key]
|
|
420
|
+
!cell || !cell.value || (cell.type == :string && cell.value.empty?) \
|
|
421
|
+
|| (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# shows the internal representation of all cells
|
|
425
|
+
# for debugging purposes
|
|
426
|
+
def to_s(sheet=nil)
|
|
427
|
+
sheet_for(sheet).cells.inspect
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# returns the row,col values of the labelled cell
|
|
431
|
+
# (nil,nil) if label is not defined
|
|
432
|
+
def label(name)
|
|
433
|
+
labels = workbook.defined_names
|
|
434
|
+
if labels.empty? || !labels.key?(name)
|
|
435
|
+
[nil,nil,nil]
|
|
436
|
+
else
|
|
437
|
+
[labels[name].row,
|
|
438
|
+
labels[name].col,
|
|
439
|
+
labels[name].sheet]
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Returns an array which all labels. Each element is an array with
|
|
444
|
+
# [labelname, [row,col,sheetname]]
|
|
445
|
+
def labels
|
|
446
|
+
@labels ||= workbook.defined_names.map do |name, label|
|
|
447
|
+
[ name,
|
|
448
|
+
[ label.row,
|
|
449
|
+
label.col,
|
|
450
|
+
label.sheet,
|
|
451
|
+
] ]
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def hyperlink?(row,col,sheet=nil)
|
|
456
|
+
!!hyperlink(row, col, sheet)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# returns the hyperlink at (row/col)
|
|
460
|
+
# nil if there is no hyperlink
|
|
461
|
+
def hyperlink(row,col,sheet=nil)
|
|
462
|
+
key = normalize(row,col)
|
|
463
|
+
sheet_for(sheet).hyperlinks[key]
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# returns the comment at (row/col)
|
|
467
|
+
# nil if there is no comment
|
|
468
|
+
def comment(row,col,sheet=nil)
|
|
469
|
+
key = normalize(row,col)
|
|
470
|
+
sheet_for(sheet).comments[key]
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# true, if there is a comment
|
|
474
|
+
def comment?(row,col,sheet=nil)
|
|
475
|
+
!!comment(row,col,sheet)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def comments(sheet=nil)
|
|
479
|
+
sheet_for(sheet).comments.map do |(x, y), comment|
|
|
480
|
+
[x, y, comment]
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Yield an array of Excelx::Cell
|
|
485
|
+
# Takes options for sheet, pad_cells, and max_rows
|
|
486
|
+
def each_row_streaming(options={})
|
|
487
|
+
sheet_for(options.delete(:sheet)).each_row(options) { |row| yield row }
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
private
|
|
491
|
+
|
|
492
|
+
# Extracts all needed files from the zip file
|
|
493
|
+
def process_zipfile(tmpdir, zipfilename)
|
|
494
|
+
@sheet_files = []
|
|
495
|
+
Zip::File.foreach(zipfilename) do |entry|
|
|
496
|
+
path =
|
|
497
|
+
case entry.name.downcase
|
|
498
|
+
when /workbook.xml$/
|
|
499
|
+
"#{tmpdir}/roo_workbook.xml"
|
|
500
|
+
when /sharedstrings.xml$/
|
|
501
|
+
"#{tmpdir}/roo_sharedStrings.xml"
|
|
502
|
+
when /styles.xml$/
|
|
503
|
+
"#{tmpdir}/roo_styles.xml"
|
|
504
|
+
when /sheet.xml$/
|
|
505
|
+
path = "#{tmpdir}/roo_sheet"
|
|
506
|
+
@sheet_files.unshift(path)
|
|
507
|
+
path
|
|
508
|
+
when /sheet([0-9]+).xml$/
|
|
509
|
+
# Numbers 3.1 exports first sheet without sheet number. Such sheets
|
|
510
|
+
# are always added to the beginning of the array which, naturally,
|
|
511
|
+
# causes other sheets to be pushed to the next index which could
|
|
512
|
+
# lead to sheet references getting overwritten, so we need to
|
|
513
|
+
# handle that case specifically.
|
|
514
|
+
nr = $1
|
|
515
|
+
sheet_files_index = nr.to_i - 1
|
|
516
|
+
sheet_files_index += 1 if @sheet_files[sheet_files_index]
|
|
517
|
+
@sheet_files[sheet_files_index] = "#{tmpdir}/roo_sheet#{nr.to_i}"
|
|
518
|
+
when /comments([0-9]+).xml$/
|
|
519
|
+
nr = $1
|
|
520
|
+
@comments_files[nr.to_i-1] = "#{tmpdir}/roo_comments#{nr}"
|
|
521
|
+
when /sheet([0-9]+).xml.rels$/
|
|
522
|
+
nr = $1
|
|
523
|
+
@rels_files[nr.to_i-1] = "#{tmpdir}/roo_rels#{nr}"
|
|
524
|
+
end
|
|
525
|
+
if path
|
|
526
|
+
entry.extract(path)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def styles
|
|
532
|
+
@styles ||= Styles.new(File.join(@tmpdir, 'roo_styles.xml'))
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def shared_strings
|
|
536
|
+
@shared_strings ||= SharedStrings.new(File.join(@tmpdir, 'roo_sharedStrings.xml'))
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def workbook
|
|
540
|
+
@workbook ||= Workbook.new(File.join(@tmpdir, "roo_workbook.xml"))
|
|
541
|
+
end
|
|
542
|
+
end
|