roo-andyw8 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- 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 +540 -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 +318 -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,540 @@
|
|
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 type = EXCEPTIONAL_FORMATS[format]
|
56
|
+
type
|
57
|
+
elsif format.include?('#')
|
58
|
+
:float
|
59
|
+
elsif !format.match(/d+(?![\]])/).nil? || format.include?('y')
|
60
|
+
if format.include?('h') || format.include?('s')
|
61
|
+
:datetime
|
62
|
+
else
|
63
|
+
:date
|
64
|
+
end
|
65
|
+
elsif format.include?('h') || format.include?('s')
|
66
|
+
:time
|
67
|
+
elsif format.include?('%')
|
68
|
+
:percentage
|
69
|
+
else
|
70
|
+
:float
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module_function :to_type
|
75
|
+
end
|
76
|
+
|
77
|
+
class Cell
|
78
|
+
attr_reader :type, :formula, :value, :excelx_type, :excelx_value, :style, :hyperlink, :coordinate
|
79
|
+
|
80
|
+
def initialize(value, type, formula, excelx_type, excelx_value, style, hyperlink, base_date, coordinate)
|
81
|
+
@type = type
|
82
|
+
@formula = formula
|
83
|
+
@base_date = base_date if [:date, :datetime].include?(@type)
|
84
|
+
@excelx_type = excelx_type
|
85
|
+
@excelx_value = excelx_value
|
86
|
+
@style = style
|
87
|
+
@value = type_cast_value(value)
|
88
|
+
@value = Roo::Link.new(hyperlink, @value.to_s) if hyperlink
|
89
|
+
@coordinate = coordinate
|
90
|
+
end
|
91
|
+
|
92
|
+
def type
|
93
|
+
if @formula
|
94
|
+
:formula
|
95
|
+
elsif @value.is_a?(Roo::Link)
|
96
|
+
:link
|
97
|
+
else
|
98
|
+
@type
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Coordinate
|
103
|
+
attr_accessor :row, :column
|
104
|
+
|
105
|
+
def initialize(row, column)
|
106
|
+
@row, @column = row, column
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def type_cast_value(value)
|
113
|
+
case @type
|
114
|
+
when :float, :percentage
|
115
|
+
value.to_f
|
116
|
+
when :date
|
117
|
+
yyyy,mm,dd = (@base_date+value.to_i).strftime("%Y-%m-%d").split('-')
|
118
|
+
Date.new(yyyy.to_i,mm.to_i,dd.to_i)
|
119
|
+
when :datetime
|
120
|
+
create_datetime_from((@base_date+value.to_f.round(6)).strftime("%Y-%m-%d %H:%M:%S.%N"))
|
121
|
+
when :time
|
122
|
+
value.to_f*(24*60*60)
|
123
|
+
when :string
|
124
|
+
value
|
125
|
+
else
|
126
|
+
value
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def create_datetime_from(datetime_string)
|
131
|
+
date_part,time_part = round_time_from(datetime_string).split(' ')
|
132
|
+
yyyy,mm,dd = date_part.split('-')
|
133
|
+
hh,mi,ss = time_part.split(':')
|
134
|
+
DateTime.civil(yyyy.to_i,mm.to_i,dd.to_i,hh.to_i,mi.to_i,ss.to_i)
|
135
|
+
end
|
136
|
+
|
137
|
+
def round_time_from(datetime_string)
|
138
|
+
date_part,time_part = datetime_string.split(' ')
|
139
|
+
yyyy,mm,dd = date_part.split('-')
|
140
|
+
hh,mi,ss = time_part.split(':')
|
141
|
+
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")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class Sheet
|
146
|
+
def initialize(name, rels_path, sheet_path, comments_path, styles, shared_strings, workbook)
|
147
|
+
@name = name
|
148
|
+
@rels = Relationships.new(rels_path)
|
149
|
+
@comments = Comments.new(comments_path)
|
150
|
+
@styles = styles
|
151
|
+
@sheet = SheetDoc.new(sheet_path, @rels, @styles, shared_strings, workbook)
|
152
|
+
end
|
153
|
+
|
154
|
+
def cells
|
155
|
+
@cells ||= @sheet.cells(@rels)
|
156
|
+
end
|
157
|
+
|
158
|
+
def present_cells
|
159
|
+
@present_cells ||= cells.select {|key, cell| cell && cell.value }
|
160
|
+
end
|
161
|
+
|
162
|
+
# Yield each row as array of Excelx::Cell objects
|
163
|
+
# accepts options max_rows (int) (offset by 1 for header)
|
164
|
+
# and pad_cells (boolean)
|
165
|
+
def each_row(options = {}, &block)
|
166
|
+
row_count = 0
|
167
|
+
@sheet.each_row_streaming do |row|
|
168
|
+
break if options[:max_rows] && row_count == options[:max_rows] + 1
|
169
|
+
block.call(cells_for_row_element(row, options)) if block_given?
|
170
|
+
row_count += 1
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def row(row_number)
|
175
|
+
first_column.upto(last_column).map do |col|
|
176
|
+
cells[[row_number,col]]
|
177
|
+
end.map {|cell| cell && cell.value }
|
178
|
+
end
|
179
|
+
|
180
|
+
def column(col_number)
|
181
|
+
first_row.upto(last_row).map do |row|
|
182
|
+
cells[[row,col_number]]
|
183
|
+
end.map {|cell| cell && cell.value }
|
184
|
+
end
|
185
|
+
|
186
|
+
# returns the number of the first non-empty row
|
187
|
+
def first_row
|
188
|
+
@first_row ||= present_cells.keys.map {|row, col| row }.min
|
189
|
+
end
|
190
|
+
|
191
|
+
def last_row
|
192
|
+
@last_row ||= present_cells.keys.map {|row, col| row }.max
|
193
|
+
end
|
194
|
+
|
195
|
+
# returns the number of the first non-empty column
|
196
|
+
def first_column(sheet=nil)
|
197
|
+
@first_column ||= present_cells.keys.map {|row, col| col }.min
|
198
|
+
end
|
199
|
+
|
200
|
+
# returns the number of the last non-empty column
|
201
|
+
def last_column(sheet=nil)
|
202
|
+
@last_column ||= present_cells.keys.map {|row, col| col }.max
|
203
|
+
end
|
204
|
+
|
205
|
+
def excelx_format(key)
|
206
|
+
@styles.style_format(cells[key].style).to_s
|
207
|
+
end
|
208
|
+
|
209
|
+
def hyperlinks
|
210
|
+
@hyperlinks ||= @sheet.hyperlinks(@rels)
|
211
|
+
end
|
212
|
+
|
213
|
+
def comments
|
214
|
+
@comments.comments
|
215
|
+
end
|
216
|
+
|
217
|
+
def dimensions
|
218
|
+
@sheet.dimensions
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
# Take an xml row and return an array of Excelx::Cell objects
|
224
|
+
# optionally pad array to header width(assumed 1st row).
|
225
|
+
# takes option pad_cells (boolean) defaults false
|
226
|
+
def cells_for_row_element(row_element, options = {})
|
227
|
+
return [] unless row_element
|
228
|
+
cell_col = 0
|
229
|
+
cells = []
|
230
|
+
@sheet.each_cell(row_element) do |cell|
|
231
|
+
cells.concat(pad_cells(cell, cell_col)) if options[:pad_cells]
|
232
|
+
cells << cell
|
233
|
+
cell_col = cell.coordinate.column
|
234
|
+
end
|
235
|
+
cells
|
236
|
+
end
|
237
|
+
|
238
|
+
def pad_cells(cell, last_column)
|
239
|
+
pad = []
|
240
|
+
(cell.coordinate.column - 1 - last_column).times { pad << nil }
|
241
|
+
pad
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
ExceedsMaxError = Class.new(StandardError)
|
246
|
+
|
247
|
+
# initialization and opening of a spreadsheet file
|
248
|
+
# values for packed: :zip
|
249
|
+
# optional cell_max (int) parameter for early aborting attempts to parse
|
250
|
+
# enormous documents.
|
251
|
+
def initialize(filename, options = {})
|
252
|
+
packed = options[:packed]
|
253
|
+
file_warning = options.fetch(:file_warning, :error)
|
254
|
+
cell_max = options.delete(:cell_max)
|
255
|
+
|
256
|
+
file_type_check(filename,'.xlsx','an Excel-xlsx', file_warning, packed)
|
257
|
+
|
258
|
+
@tmpdir = make_tmpdir(filename.split('/').last, options[:tmpdir_root])
|
259
|
+
@filename = local_filename(filename, @tmpdir, packed)
|
260
|
+
@comments_files = []
|
261
|
+
@rels_files = []
|
262
|
+
process_zipfile(@tmpdir, @filename)
|
263
|
+
|
264
|
+
@sheet_names = workbook.sheets.map { |sheet| sheet['name'] }
|
265
|
+
@sheets = []
|
266
|
+
@sheets_by_name = Hash[@sheet_names.map.with_index do |sheet_name, n|
|
267
|
+
@sheets[n] = Sheet.new(sheet_name, @rels_files[n], @sheet_files[n], @comments_files[n], styles, shared_strings, workbook)
|
268
|
+
[sheet_name, @sheets[n]]
|
269
|
+
end]
|
270
|
+
|
271
|
+
if cell_max
|
272
|
+
cell_count = ::Roo::Utils.num_cells_in_range(sheet_for(options.delete(:sheet)).dimensions)
|
273
|
+
raise ExceedsMaxError.new("Excel file exceeds cell maximum: #{cell_count} > #{cell_max}") if cell_count > cell_max
|
274
|
+
end
|
275
|
+
|
276
|
+
super
|
277
|
+
end
|
278
|
+
|
279
|
+
def method_missing(method,*args)
|
280
|
+
if label = workbook.defined_names[method.to_s]
|
281
|
+
sheet_for(label.sheet).cells[label.key].value
|
282
|
+
else
|
283
|
+
# call super for methods like #a1
|
284
|
+
super
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def sheets
|
289
|
+
@sheet_names
|
290
|
+
end
|
291
|
+
|
292
|
+
def sheet_for(sheet)
|
293
|
+
sheet ||= default_sheet
|
294
|
+
validate_sheet!(sheet)
|
295
|
+
@sheets_by_name[sheet]
|
296
|
+
end
|
297
|
+
|
298
|
+
# Returns the content of a spreadsheet-cell.
|
299
|
+
# (1,1) is the upper left corner.
|
300
|
+
# (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
|
301
|
+
# cell at the first line and first row.
|
302
|
+
def cell(row, col, sheet=nil)
|
303
|
+
key = normalize(row,col)
|
304
|
+
cell = sheet_for(sheet).cells[key]
|
305
|
+
cell.value if cell
|
306
|
+
end
|
307
|
+
|
308
|
+
def row(rownumber,sheet=nil)
|
309
|
+
sheet_for(sheet).row(rownumber)
|
310
|
+
end
|
311
|
+
|
312
|
+
# returns all values in this column as an array
|
313
|
+
# column numbers are 1,2,3,... like in the spreadsheet
|
314
|
+
def column(column_number,sheet=nil)
|
315
|
+
if column_number.is_a?(::String)
|
316
|
+
column_number = ::Roo::Utils.letter_to_number(column_number)
|
317
|
+
end
|
318
|
+
sheet_for(sheet).column(column_number)
|
319
|
+
end
|
320
|
+
|
321
|
+
# returns the number of the first non-empty row
|
322
|
+
def first_row(sheet=nil)
|
323
|
+
sheet_for(sheet).first_row
|
324
|
+
end
|
325
|
+
|
326
|
+
# returns the number of the last non-empty row
|
327
|
+
def last_row(sheet=nil)
|
328
|
+
sheet_for(sheet).last_row
|
329
|
+
end
|
330
|
+
|
331
|
+
# returns the number of the first non-empty column
|
332
|
+
def first_column(sheet=nil)
|
333
|
+
sheet_for(sheet).first_column
|
334
|
+
end
|
335
|
+
|
336
|
+
# returns the number of the last non-empty column
|
337
|
+
def last_column(sheet=nil)
|
338
|
+
sheet_for(sheet).last_column
|
339
|
+
end
|
340
|
+
|
341
|
+
# set a cell to a certain value
|
342
|
+
# (this will not be saved back to the spreadsheet file!)
|
343
|
+
def set(row,col,value, sheet = nil) #:nodoc:
|
344
|
+
key = normalize(row,col)
|
345
|
+
cell_type = cell_type_by_value(value)
|
346
|
+
sheet_for(sheet).cells[key] = Cell.new(value, cell_type, nil, cell_type, value, nil, nil, nil, Cell::Coordinate.new(row, col))
|
347
|
+
end
|
348
|
+
|
349
|
+
|
350
|
+
# Returns the formula at (row,col).
|
351
|
+
# Returns nil if there is no formula.
|
352
|
+
# The method #formula? checks if there is a formula.
|
353
|
+
def formula(row,col,sheet=nil)
|
354
|
+
key = normalize(row,col)
|
355
|
+
sheet_for(sheet).cells[key].formula
|
356
|
+
end
|
357
|
+
|
358
|
+
# Predicate methods really should return a boolean
|
359
|
+
# value. Hopefully no one was relying on the fact that this
|
360
|
+
# previously returned either nil/formula
|
361
|
+
def formula?(*args)
|
362
|
+
!!formula(*args)
|
363
|
+
end
|
364
|
+
|
365
|
+
# returns each formula in the selected sheet as an array of tuples in following format
|
366
|
+
# [[row, col, formula], [row, col, formula],...]
|
367
|
+
def formulas(sheet=nil)
|
368
|
+
sheet_for(sheet).cells.select {|_, cell| cell.formula }.map do |(x, y), cell|
|
369
|
+
[x, y, cell.formula]
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# Given a cell, return the cell's style
|
374
|
+
def font(row, col, sheet=nil)
|
375
|
+
key = normalize(row,col)
|
376
|
+
styles.definitions[sheet_for(sheet).cells[key].style]
|
377
|
+
end
|
378
|
+
|
379
|
+
# returns the type of a cell:
|
380
|
+
# * :float
|
381
|
+
# * :string,
|
382
|
+
# * :date
|
383
|
+
# * :percentage
|
384
|
+
# * :formula
|
385
|
+
# * :time
|
386
|
+
# * :datetime
|
387
|
+
def celltype(row,col,sheet=nil)
|
388
|
+
key = normalize(row, col)
|
389
|
+
sheet_for(sheet).cells[key].type
|
390
|
+
end
|
391
|
+
|
392
|
+
# returns the internal type of an excel cell
|
393
|
+
# * :numeric_or_formula
|
394
|
+
# * :string
|
395
|
+
# Note: this is only available within the Excelx class
|
396
|
+
def excelx_type(row,col,sheet=nil)
|
397
|
+
key = normalize(row,col)
|
398
|
+
sheet_for(sheet).cells[key].excelx_type
|
399
|
+
end
|
400
|
+
|
401
|
+
# returns the internal value of an excelx cell
|
402
|
+
# Note: this is only available within the Excelx class
|
403
|
+
def excelx_value(row,col,sheet=nil)
|
404
|
+
key = normalize(row,col)
|
405
|
+
sheet_for(sheet).cells[key].excelx_value
|
406
|
+
end
|
407
|
+
|
408
|
+
# returns the internal format of an excel cell
|
409
|
+
def excelx_format(row,col,sheet=nil)
|
410
|
+
key = normalize(row,col)
|
411
|
+
sheet_for(sheet).excelx_format(key)
|
412
|
+
end
|
413
|
+
|
414
|
+
def empty?(row,col,sheet=nil)
|
415
|
+
sheet = sheet_for(sheet)
|
416
|
+
key = normalize(row,col)
|
417
|
+
cell = sheet.cells[key]
|
418
|
+
!cell || !cell.value || (cell.type == :string && cell.value.empty?) \
|
419
|
+
|| (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
|
420
|
+
end
|
421
|
+
|
422
|
+
# shows the internal representation of all cells
|
423
|
+
# for debugging purposes
|
424
|
+
def to_s(sheet=nil)
|
425
|
+
sheet_for(sheet).cells.inspect
|
426
|
+
end
|
427
|
+
|
428
|
+
# returns the row,col values of the labelled cell
|
429
|
+
# (nil,nil) if label is not defined
|
430
|
+
def label(name)
|
431
|
+
labels = workbook.defined_names
|
432
|
+
if labels.empty? || !labels.key?(name)
|
433
|
+
[nil,nil,nil]
|
434
|
+
else
|
435
|
+
[labels[name].row,
|
436
|
+
labels[name].col,
|
437
|
+
labels[name].sheet]
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# Returns an array which all labels. Each element is an array with
|
442
|
+
# [labelname, [row,col,sheetname]]
|
443
|
+
def labels
|
444
|
+
@labels ||= workbook.defined_names.map do |name, label|
|
445
|
+
[ name,
|
446
|
+
[ label.row,
|
447
|
+
label.col,
|
448
|
+
label.sheet,
|
449
|
+
] ]
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def hyperlink?(row,col,sheet=nil)
|
454
|
+
!!hyperlink(row, col, sheet)
|
455
|
+
end
|
456
|
+
|
457
|
+
# returns the hyperlink at (row/col)
|
458
|
+
# nil if there is no hyperlink
|
459
|
+
def hyperlink(row,col,sheet=nil)
|
460
|
+
key = normalize(row,col)
|
461
|
+
sheet_for(sheet).hyperlinks[key]
|
462
|
+
end
|
463
|
+
|
464
|
+
# returns the comment at (row/col)
|
465
|
+
# nil if there is no comment
|
466
|
+
def comment(row,col,sheet=nil)
|
467
|
+
key = normalize(row,col)
|
468
|
+
sheet_for(sheet).comments[key]
|
469
|
+
end
|
470
|
+
|
471
|
+
# true, if there is a comment
|
472
|
+
def comment?(row,col,sheet=nil)
|
473
|
+
!!comment(row,col,sheet)
|
474
|
+
end
|
475
|
+
|
476
|
+
def comments(sheet=nil)
|
477
|
+
sheet_for(sheet).comments.map do |(x, y), comment|
|
478
|
+
[x, y, comment]
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# Yield an array of Excelx::Cell
|
483
|
+
# Takes options for sheet, pad_cells, and max_rows
|
484
|
+
def each_row_streaming(options={})
|
485
|
+
sheet_for(options.delete(:sheet)).each_row(options) { |row| yield row }
|
486
|
+
end
|
487
|
+
|
488
|
+
private
|
489
|
+
|
490
|
+
# Extracts all needed files from the zip file
|
491
|
+
def process_zipfile(tmpdir, zipfilename)
|
492
|
+
@sheet_files = []
|
493
|
+
Zip::File.foreach(zipfilename) do |entry|
|
494
|
+
path =
|
495
|
+
case entry.name.downcase
|
496
|
+
when /workbook.xml$/
|
497
|
+
"#{tmpdir}/roo_workbook.xml"
|
498
|
+
when /sharedstrings.xml$/
|
499
|
+
"#{tmpdir}/roo_sharedStrings.xml"
|
500
|
+
when /styles.xml$/
|
501
|
+
"#{tmpdir}/roo_styles.xml"
|
502
|
+
when /sheet.xml$/
|
503
|
+
path = "#{tmpdir}/roo_sheet"
|
504
|
+
@sheet_files.unshift(path)
|
505
|
+
path
|
506
|
+
when /sheet([0-9]+).xml$/
|
507
|
+
# Numbers 3.1 exports first sheet without sheet number. Such sheets
|
508
|
+
# are always added to the beginning of the array which, naturally,
|
509
|
+
# causes other sheets to be pushed to the next index which could
|
510
|
+
# lead to sheet references getting overwritten, so we need to
|
511
|
+
# handle that case specifically.
|
512
|
+
nr = $1
|
513
|
+
sheet_files_index = nr.to_i - 1
|
514
|
+
sheet_files_index += 1 if @sheet_files[sheet_files_index]
|
515
|
+
@sheet_files[sheet_files_index] = "#{tmpdir}/roo_sheet#{nr.to_i}"
|
516
|
+
when /comments([0-9]+).xml$/
|
517
|
+
nr = $1
|
518
|
+
@comments_files[nr.to_i-1] = "#{tmpdir}/roo_comments#{nr}"
|
519
|
+
when /sheet([0-9]+).xml.rels$/
|
520
|
+
nr = $1
|
521
|
+
@rels_files[nr.to_i-1] = "#{tmpdir}/roo_rels#{nr}"
|
522
|
+
end
|
523
|
+
if path
|
524
|
+
entry.extract(path)
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
def styles
|
530
|
+
@styles ||= Styles.new(File.join(@tmpdir, 'roo_styles.xml'))
|
531
|
+
end
|
532
|
+
|
533
|
+
def shared_strings
|
534
|
+
@shared_strings ||= SharedStrings.new(File.join(@tmpdir, 'roo_sharedStrings.xml'))
|
535
|
+
end
|
536
|
+
|
537
|
+
def workbook
|
538
|
+
@workbook ||= Workbook.new(File.join(@tmpdir, "roo_workbook.xml"))
|
539
|
+
end
|
540
|
+
end
|