roo 2.6.0 → 2.8.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.codeclimate.yml +17 -0
- data/.github/issue_template.md +16 -0
- data/.github/pull_request_template.md +14 -0
- data/.rubocop.yml +186 -0
- data/.travis.yml +14 -11
- data/CHANGELOG.md +64 -2
- data/Gemfile +2 -4
- data/LICENSE +2 -0
- data/README.md +36 -10
- data/lib/roo/base.rb +82 -225
- data/lib/roo/constants.rb +5 -3
- data/lib/roo/csv.rb +100 -97
- data/lib/roo/excelx/cell/base.rb +26 -12
- data/lib/roo/excelx/cell/boolean.rb +9 -6
- data/lib/roo/excelx/cell/date.rb +7 -7
- data/lib/roo/excelx/cell/datetime.rb +50 -44
- data/lib/roo/excelx/cell/empty.rb +3 -2
- data/lib/roo/excelx/cell/number.rb +44 -47
- data/lib/roo/excelx/cell/string.rb +3 -3
- data/lib/roo/excelx/cell/time.rb +17 -16
- data/lib/roo/excelx/cell.rb +10 -6
- data/lib/roo/excelx/comments.rb +3 -3
- data/lib/roo/excelx/coordinate.rb +11 -4
- data/lib/roo/excelx/extractor.rb +21 -3
- data/lib/roo/excelx/format.rb +38 -31
- data/lib/roo/excelx/images.rb +26 -0
- data/lib/roo/excelx/relationships.rb +12 -4
- data/lib/roo/excelx/shared.rb +10 -3
- data/lib/roo/excelx/shared_strings.rb +9 -15
- data/lib/roo/excelx/sheet.rb +49 -10
- data/lib/roo/excelx/sheet_doc.rb +89 -48
- data/lib/roo/excelx/styles.rb +3 -3
- data/lib/roo/excelx/workbook.rb +7 -3
- data/lib/roo/excelx.rb +50 -19
- data/lib/roo/formatters/base.rb +15 -0
- data/lib/roo/formatters/csv.rb +84 -0
- data/lib/roo/formatters/matrix.rb +23 -0
- data/lib/roo/formatters/xml.rb +31 -0
- data/lib/roo/formatters/yaml.rb +40 -0
- data/lib/roo/helpers/default_attr_reader.rb +20 -0
- data/lib/roo/helpers/weak_instance_cache.rb +41 -0
- data/lib/roo/open_office.rb +17 -9
- data/lib/roo/spreadsheet.rb +1 -1
- data/lib/roo/tempdir.rb +5 -10
- data/lib/roo/utils.rb +70 -20
- data/lib/roo/version.rb +1 -1
- data/lib/roo.rb +4 -1
- data/roo.gemspec +14 -11
- data/spec/lib/roo/base_spec.rb +45 -3
- data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
- data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
- data/spec/lib/roo/excelx_spec.rb +150 -31
- data/spec/lib/roo/strict_spec.rb +43 -0
- data/spec/lib/roo/utils_spec.rb +25 -3
- data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
- data/spec/lib/roo_spec.rb +0 -0
- data/spec/spec_helper.rb +2 -6
- data/test/excelx/cell/test_attr_reader_default.rb +72 -0
- data/test/excelx/cell/test_base.rb +5 -0
- data/test/excelx/cell/test_datetime.rb +6 -6
- data/test/excelx/cell/test_empty.rb +11 -0
- data/test/excelx/cell/test_number.rb +9 -0
- data/test/excelx/cell/test_string.rb +20 -0
- data/test/excelx/cell/test_time.rb +5 -5
- data/test/excelx/test_coordinate.rb +51 -0
- data/test/formatters/test_csv.rb +136 -0
- data/test/formatters/test_matrix.rb +76 -0
- data/test/formatters/test_xml.rb +78 -0
- data/test/formatters/test_yaml.rb +20 -0
- data/test/helpers/test_accessing_files.rb +60 -0
- data/test/helpers/test_comments.rb +43 -0
- data/test/helpers/test_formulas.rb +9 -0
- data/test/helpers/test_labels.rb +103 -0
- data/test/helpers/test_sheets.rb +55 -0
- data/test/helpers/test_styles.rb +62 -0
- data/test/roo/test_base.rb +182 -0
- data/test/roo/test_csv.rb +88 -0
- data/test/roo/test_excelx.rb +330 -0
- data/test/roo/test_libre_office.rb +9 -0
- data/test/roo/test_open_office.rb +289 -0
- data/test/test_helper.rb +129 -14
- data/test/test_roo.rb +32 -1787
- metadata +81 -29
- data/.github/ISSUE_TEMPLATE +0 -10
- data/Gemfile_ruby2 +0 -29
data/lib/roo/excelx/sheet_doc.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
require 'roo/excelx/extractor'
|
3
5
|
|
@@ -5,7 +7,7 @@ module Roo
|
|
5
7
|
class Excelx
|
6
8
|
class SheetDoc < Excelx::Extractor
|
7
9
|
extend Forwardable
|
8
|
-
delegate [:
|
10
|
+
delegate [:workbook] => :@shared
|
9
11
|
|
10
12
|
def initialize(path, relationships, shared, options = {})
|
11
13
|
super(path)
|
@@ -19,7 +21,12 @@ module Roo
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def hyperlinks(relationships)
|
22
|
-
|
24
|
+
# If you're sure you're not going to need this hyperlinks you can discard it
|
25
|
+
@hyperlinks ||= if @options[:no_hyperlinks] || !relationships.include_type?("hyperlink")
|
26
|
+
{}
|
27
|
+
else
|
28
|
+
extract_hyperlinks(relationships)
|
29
|
+
end
|
23
30
|
end
|
24
31
|
|
25
32
|
# Get the dimensions for the sheet.
|
@@ -39,13 +46,10 @@ module Roo
|
|
39
46
|
def each_cell(row_xml)
|
40
47
|
return [] unless row_xml
|
41
48
|
row_xml.children.each do |cell_element|
|
42
|
-
|
43
|
-
hyperlinks =
|
44
|
-
key = ::Roo::Utils.ref_to_key(cell_element['r'])
|
45
|
-
hyperlinks(@relationships)[key]
|
46
|
-
end
|
49
|
+
coordinate = ::Roo::Utils.extract_coordinate(cell_element["r"])
|
50
|
+
hyperlinks = hyperlinks(@relationships)[coordinate]
|
47
51
|
|
48
|
-
yield cell_from_xml(cell_element, hyperlinks)
|
52
|
+
yield cell_from_xml(cell_element, hyperlinks, coordinate)
|
49
53
|
end
|
50
54
|
end
|
51
55
|
|
@@ -53,13 +57,13 @@ module Roo
|
|
53
57
|
|
54
58
|
def cell_value_type(type, format)
|
55
59
|
case type
|
56
|
-
when 's'
|
60
|
+
when 's'
|
57
61
|
:shared
|
58
|
-
when 'b'
|
62
|
+
when 'b'
|
59
63
|
:boolean
|
60
|
-
when 'str'
|
64
|
+
when 'str'
|
61
65
|
:string
|
62
|
-
when 'inlineStr'
|
66
|
+
when 'inlineStr'
|
63
67
|
:inlinestr
|
64
68
|
else
|
65
69
|
Excelx::Format.to_type(format)
|
@@ -74,42 +78,58 @@ module Roo
|
|
74
78
|
# </c>
|
75
79
|
# hyperlink - a String for the hyperlink for the cell or nil when no
|
76
80
|
# hyperlink is present.
|
81
|
+
# coordinate - a Roo::Excelx::Coordinate for the coordinate for the cell
|
82
|
+
# or nil to extract coordinate from cell_xml.
|
83
|
+
# empty_cell - an Optional Boolean value.
|
77
84
|
#
|
78
85
|
# Examples
|
79
86
|
#
|
80
|
-
# cells_from_xml(<Nokogiri::XML::Element>, nil)
|
87
|
+
# cells_from_xml(<Nokogiri::XML::Element>, nil, nil)
|
81
88
|
# # => <Excelx::Cell::String>
|
82
89
|
#
|
83
90
|
# Returns a type of <Excelx::Cell>.
|
84
|
-
def cell_from_xml(cell_xml, hyperlink)
|
85
|
-
coordinate
|
86
|
-
|
91
|
+
def cell_from_xml(cell_xml, hyperlink, coordinate, empty_cell=true)
|
92
|
+
coordinate ||= ::Roo::Utils.extract_coordinate(cell_xml["r"])
|
93
|
+
cell_xml_children = cell_xml.children
|
94
|
+
return create_empty_cell(coordinate, empty_cell) if cell_xml_children.empty?
|
87
95
|
|
88
96
|
# NOTE: This is error prone, to_i will silently turn a nil into a 0.
|
89
97
|
# This works by coincidence because Format[0] is General.
|
90
|
-
style = cell_xml[
|
91
|
-
format = styles.style_format(style)
|
92
|
-
value_type = cell_value_type(cell_xml['t'], format)
|
98
|
+
style = cell_xml["s"].to_i
|
93
99
|
formula = nil
|
94
100
|
|
95
|
-
|
101
|
+
cell_xml_children.each do |cell|
|
96
102
|
case cell.name
|
97
103
|
when 'is'
|
98
|
-
|
99
|
-
|
100
|
-
|
104
|
+
content = +""
|
105
|
+
cell.children.each do |inline_str|
|
106
|
+
if inline_str.name == 't'
|
107
|
+
content << inline_str.content
|
108
|
+
end
|
109
|
+
end
|
110
|
+
unless content.empty?
|
111
|
+
return Excelx::Cell.cell_class(:string).new(content, formula, style, hyperlink, coordinate)
|
101
112
|
end
|
102
113
|
when 'f'
|
103
114
|
formula = cell.content
|
104
115
|
when 'v'
|
105
|
-
|
116
|
+
format = style_format(style)
|
117
|
+
value_type = cell_value_type(cell_xml["t"], format)
|
118
|
+
|
119
|
+
return create_cell_from_value(value_type, cell, formula, format, style, hyperlink, coordinate)
|
106
120
|
end
|
107
121
|
end
|
108
122
|
|
109
|
-
|
123
|
+
create_empty_cell(coordinate, empty_cell)
|
124
|
+
end
|
125
|
+
|
126
|
+
def create_empty_cell(coordinate, empty_cell)
|
127
|
+
if empty_cell
|
128
|
+
Excelx::Cell::Empty.new(coordinate)
|
129
|
+
end
|
110
130
|
end
|
111
131
|
|
112
|
-
def create_cell_from_value(value_type, cell, formula, format, style, hyperlink,
|
132
|
+
def create_cell_from_value(value_type, cell, formula, format, style, hyperlink, coordinate)
|
113
133
|
# NOTE: format.to_s can replace excelx_type as an argument for
|
114
134
|
# Cell::Time, Cell::DateTime, Cell::Date or Cell::Number, but
|
115
135
|
# it will break some brittle tests.
|
@@ -125,11 +145,12 @@ module Roo
|
|
125
145
|
# 3. formula
|
126
146
|
case value_type
|
127
147
|
when :shared
|
128
|
-
|
129
|
-
|
148
|
+
cell_content = cell.content.to_i
|
149
|
+
value = shared_strings.use_html?(cell_content) ? shared_strings.to_html[cell_content] : shared_strings[cell_content]
|
150
|
+
Excelx::Cell.cell_class(:string).new(value, formula, style, hyperlink, coordinate)
|
130
151
|
when :boolean, :string
|
131
152
|
value = cell.content
|
132
|
-
Excelx::Cell.
|
153
|
+
Excelx::Cell.cell_class(value_type).new(value, formula, style, hyperlink, coordinate)
|
133
154
|
when :time, :datetime
|
134
155
|
cell_content = cell.content.to_f
|
135
156
|
# NOTE: A date will be a whole number. A time will have be > 1. And
|
@@ -148,35 +169,35 @@ module Roo
|
|
148
169
|
else
|
149
170
|
:date
|
150
171
|
end
|
151
|
-
|
172
|
+
base_value = cell_type == :date ? base_date : base_timestamp
|
173
|
+
Excelx::Cell.cell_class(cell_type).new(cell_content, formula, excelx_type, style, hyperlink, base_value, coordinate)
|
152
174
|
when :date
|
153
|
-
Excelx::Cell.
|
175
|
+
Excelx::Cell.cell_class(:date).new(cell.content, formula, excelx_type, style, hyperlink, base_date, coordinate)
|
154
176
|
else
|
155
|
-
Excelx::Cell.
|
177
|
+
Excelx::Cell.cell_class(:number).new(cell.content, formula, excelx_type, style, hyperlink, coordinate)
|
156
178
|
end
|
157
179
|
end
|
158
180
|
|
159
|
-
def extract_coordinate(coordinate)
|
160
|
-
row, column = ::Roo::Utils.split_coordinate(coordinate)
|
161
|
-
|
162
|
-
Excelx::Coordinate.new(row, column)
|
163
|
-
end
|
164
|
-
|
165
181
|
def extract_hyperlinks(relationships)
|
166
182
|
return {} unless (hyperlinks = doc.xpath('/worksheet/hyperlinks/hyperlink'))
|
167
183
|
|
168
|
-
|
169
|
-
if
|
170
|
-
|
184
|
+
hyperlinks.each_with_object({}) do |hyperlink, hash|
|
185
|
+
if relationship = relationships[hyperlink['id']]
|
186
|
+
target_link = relationship['Target']
|
187
|
+
target_link += "##{hyperlink['location']}" if hyperlink['location']
|
188
|
+
|
189
|
+
Roo::Utils.coordinates_in_range(hyperlink["ref"].to_s) do |coord|
|
190
|
+
hash[coord] = target_link
|
191
|
+
end
|
171
192
|
end
|
172
|
-
end
|
193
|
+
end
|
173
194
|
end
|
174
195
|
|
175
196
|
def expand_merged_ranges(cells)
|
176
197
|
# Extract merged ranges from xml
|
177
198
|
merges = {}
|
178
199
|
doc.xpath('/worksheet/mergeCells/mergeCell').each do |mergecell_xml|
|
179
|
-
tl, br = mergecell_xml[
|
200
|
+
tl, br = mergecell_xml["ref"].split(/:/).map { |ref| ::Roo::Utils.ref_to_key(ref) }
|
180
201
|
for row in tl[0]..br[0] do
|
181
202
|
for col in tl[1]..br[1] do
|
182
203
|
next if row == tl[0] && col == tl[1]
|
@@ -191,10 +212,14 @@ module Roo
|
|
191
212
|
end
|
192
213
|
|
193
214
|
def extract_cells(relationships)
|
194
|
-
extracted_cells =
|
195
|
-
|
196
|
-
|
197
|
-
|
215
|
+
extracted_cells = {}
|
216
|
+
empty_cell = @options[:empty_cell]
|
217
|
+
|
218
|
+
doc.xpath('/worksheet/sheetData/row/c').each do |cell_xml|
|
219
|
+
coordinate = ::Roo::Utils.extract_coordinate(cell_xml["r"])
|
220
|
+
cell = cell_from_xml(cell_xml, hyperlinks(relationships)[coordinate], coordinate, empty_cell)
|
221
|
+
extracted_cells[coordinate] = cell if cell
|
222
|
+
end
|
198
223
|
|
199
224
|
expand_merged_ranges(extracted_cells) if @options[:expand_merged_ranges]
|
200
225
|
|
@@ -203,9 +228,25 @@ module Roo
|
|
203
228
|
|
204
229
|
def extract_dimensions
|
205
230
|
Roo::Utils.each_element(@path, 'dimension') do |dimension|
|
206
|
-
return dimension
|
231
|
+
return dimension["ref"]
|
207
232
|
end
|
208
233
|
end
|
234
|
+
|
235
|
+
def style_format(style)
|
236
|
+
@shared.styles.style_format(style)
|
237
|
+
end
|
238
|
+
|
239
|
+
def base_date
|
240
|
+
@shared.base_date
|
241
|
+
end
|
242
|
+
|
243
|
+
def base_timestamp
|
244
|
+
@shared.base_timestamp
|
245
|
+
end
|
246
|
+
|
247
|
+
def shared_strings
|
248
|
+
@shared.shared_strings
|
249
|
+
end
|
209
250
|
end
|
210
251
|
end
|
211
252
|
end
|
data/lib/roo/excelx/styles.rb
CHANGED
@@ -55,9 +55,9 @@ module Roo
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def extract_num_fmts
|
58
|
-
|
59
|
-
[num_fmt['numFmtId']
|
60
|
-
end
|
58
|
+
doc.xpath('//numFmt').each_with_object({}) do |num_fmt, hash|
|
59
|
+
hash[num_fmt['numFmtId']] = num_fmt['formatCode']
|
60
|
+
end
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
data/lib/roo/excelx/workbook.rb
CHANGED
@@ -29,13 +29,17 @@ module Roo
|
|
29
29
|
|
30
30
|
# aka labels
|
31
31
|
def defined_names
|
32
|
-
|
32
|
+
doc.xpath('//definedName').each_with_object({}) do |defined_name, hash|
|
33
33
|
# "Sheet1!$C$5"
|
34
34
|
sheet, coordinates = defined_name.text.split('!$', 2)
|
35
35
|
col, row = coordinates.split('$')
|
36
36
|
name = defined_name['name']
|
37
|
-
[name
|
38
|
-
end
|
37
|
+
hash[name] = Label.new(name, sheet, row, col)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def base_timestamp
|
42
|
+
@base_timestamp ||= base_date.to_datetime.to_time.to_i
|
39
43
|
end
|
40
44
|
|
41
45
|
def base_date
|
data/lib/roo/excelx.rb
CHANGED
@@ -4,12 +4,11 @@ require 'roo/link'
|
|
4
4
|
require 'roo/tempdir'
|
5
5
|
require 'roo/utils'
|
6
6
|
require 'forwardable'
|
7
|
+
require 'set'
|
7
8
|
|
8
9
|
module Roo
|
9
10
|
class Excelx < Roo::Base
|
10
11
|
extend Roo::Tempdir
|
11
|
-
|
12
|
-
require 'set'
|
13
12
|
extend Forwardable
|
14
13
|
|
15
14
|
ERROR_VALUES = %w(#N/A #REF! #NAME? #DIV/0! #NULL! #VALUE! #NUM!).to_set
|
@@ -25,8 +24,9 @@ module Roo
|
|
25
24
|
require 'roo/excelx/sheet_doc'
|
26
25
|
require 'roo/excelx/coordinate'
|
27
26
|
require 'roo/excelx/format'
|
27
|
+
require 'roo/excelx/images'
|
28
28
|
|
29
|
-
delegate [:styles, :workbook, :shared_strings, :rels_files, :sheet_files, :comments_files] => :@shared
|
29
|
+
delegate [:styles, :workbook, :shared_strings, :rels_files, :sheet_files, :comments_files, :image_rels, :image_files] => :@shared
|
30
30
|
ExceedsMaxError = Class.new(StandardError)
|
31
31
|
|
32
32
|
# initialization and opening of a spreadsheet file
|
@@ -40,14 +40,23 @@ module Roo
|
|
40
40
|
sheet_options = {}
|
41
41
|
sheet_options[:expand_merged_ranges] = (options[:expand_merged_ranges] || false)
|
42
42
|
sheet_options[:no_hyperlinks] = (options[:no_hyperlinks] || false)
|
43
|
+
sheet_options[:empty_cell] = (options[:empty_cell] || false)
|
44
|
+
shared_options = {}
|
43
45
|
|
46
|
+
shared_options[:disable_html_wrapper] = (options[:disable_html_wrapper] || false)
|
44
47
|
unless is_stream?(filename_or_stream)
|
45
48
|
file_type_check(filename_or_stream, %w[.xlsx .xlsm], 'an Excel 2007', file_warning, packed)
|
46
49
|
basename = find_basename(filename_or_stream)
|
47
50
|
end
|
48
51
|
|
52
|
+
# NOTE: Create temp directory and allow Ruby to cleanup the temp directory
|
53
|
+
# when the object is garbage collected. Initially, the finalizer was
|
54
|
+
# created in the Roo::Tempdir module, but that led to a segfault
|
55
|
+
# when testing in Ruby 2.4.0.
|
49
56
|
@tmpdir = self.class.make_tempdir(self, basename, options[:tmpdir_root])
|
50
|
-
|
57
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(object_id))
|
58
|
+
|
59
|
+
@shared = Shared.new(@tmpdir, shared_options)
|
51
60
|
@filename = local_filename(filename_or_stream, @tmpdir, packed)
|
52
61
|
process_zipfile(@filename || filename_or_stream)
|
53
62
|
|
@@ -57,10 +66,10 @@ module Roo
|
|
57
66
|
end
|
58
67
|
end.compact
|
59
68
|
@sheets = []
|
60
|
-
@sheets_by_name =
|
61
|
-
|
62
|
-
[sheet_name
|
63
|
-
end
|
69
|
+
@sheets_by_name = {}
|
70
|
+
@sheet_names.each_with_index do |sheet_name, n|
|
71
|
+
@sheets_by_name[sheet_name] = @sheets[n] = Sheet.new(sheet_name, @shared, n, sheet_options)
|
72
|
+
end
|
64
73
|
|
65
74
|
if cell_max
|
66
75
|
cell_count = ::Roo::Utils.num_cells_in_range(sheet_for(options.delete(:sheet)).dimensions)
|
@@ -89,7 +98,12 @@ module Roo
|
|
89
98
|
def sheet_for(sheet)
|
90
99
|
sheet ||= default_sheet
|
91
100
|
validate_sheet!(sheet)
|
92
|
-
@sheets_by_name[sheet]
|
101
|
+
@sheets_by_name[sheet] || @sheets[sheet]
|
102
|
+
end
|
103
|
+
|
104
|
+
def images(sheet = nil)
|
105
|
+
images_names = sheet_for(sheet).images.map(&:last)
|
106
|
+
images_names.map { |iname| image_files.find { |ifile| ifile[iname] } }
|
93
107
|
end
|
94
108
|
|
95
109
|
# Returns the content of a spreadsheet-cell.
|
@@ -218,7 +232,7 @@ module Roo
|
|
218
232
|
sheet = sheet_for(sheet)
|
219
233
|
key = normalize(row, col)
|
220
234
|
cell = sheet.cells[key]
|
221
|
-
!cell || cell.empty? ||
|
235
|
+
!cell || cell.empty? ||
|
222
236
|
(row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
|
223
237
|
end
|
224
238
|
|
@@ -320,7 +334,7 @@ module Roo
|
|
320
334
|
|
321
335
|
wb.extract(path)
|
322
336
|
workbook_doc = Roo::Utils.load_xml(path).remove_namespaces!
|
323
|
-
workbook_doc.xpath('//sheet').map { |s| s
|
337
|
+
workbook_doc.xpath('//sheet').map { |s| s['id'] }
|
324
338
|
end
|
325
339
|
|
326
340
|
# Internal
|
@@ -344,17 +358,13 @@ module Roo
|
|
344
358
|
|
345
359
|
wb_rels.extract(path)
|
346
360
|
rels_doc = Roo::Utils.load_xml(path).remove_namespaces!
|
347
|
-
worksheet_type = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'
|
348
361
|
|
349
362
|
relationships = rels_doc.xpath('//Relationship').select do |relationship|
|
350
|
-
relationship
|
363
|
+
worksheet_types.include? relationship['Type']
|
351
364
|
end
|
352
365
|
|
353
|
-
relationships.
|
354
|
-
|
355
|
-
id = attributes['Id']
|
356
|
-
hash[id.value] = attributes['Target'].value
|
357
|
-
hash
|
366
|
+
relationships.each_with_object({}) do |relationship, hash|
|
367
|
+
hash[relationship['Id']] = relationship['Target']
|
358
368
|
end
|
359
369
|
end
|
360
370
|
|
@@ -371,6 +381,15 @@ module Roo
|
|
371
381
|
end
|
372
382
|
end
|
373
383
|
|
384
|
+
def extract_images(entries, tmpdir)
|
385
|
+
img_entries = entries.select { |e| e.name[/media\/image([0-9]+)/] }
|
386
|
+
img_entries.each do |entry|
|
387
|
+
path = "#{@tmpdir}/roo#{entry.name.gsub(/xl\/|\//, "_")}"
|
388
|
+
image_files << path
|
389
|
+
entry.extract(path)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
374
393
|
# Extracts all needed files from the zip file
|
375
394
|
def process_zipfile(zipfilename_or_stream)
|
376
395
|
@sheet_files = []
|
@@ -404,6 +423,7 @@ module Roo
|
|
404
423
|
sheet_ids = extract_worksheet_ids(entries, "#{@tmpdir}/roo_workbook.xml")
|
405
424
|
sheets = extract_worksheet_rels(entries, "#{@tmpdir}/roo_workbook.xml.rels")
|
406
425
|
extract_sheets_in_order(entries, sheet_ids, sheets, @tmpdir)
|
426
|
+
extract_images(entries, @tmpdir)
|
407
427
|
|
408
428
|
entries.each do |entry|
|
409
429
|
path =
|
@@ -430,6 +450,10 @@ module Roo
|
|
430
450
|
# drawings, etc.
|
431
451
|
nr = Regexp.last_match[1].to_i
|
432
452
|
rels_files[nr - 1] = "#{@tmpdir}/roo_rels#{nr}"
|
453
|
+
when /drawing([0-9]+).xml.rels$/
|
454
|
+
# Extracting drawing relationships to make images lists for each sheet
|
455
|
+
nr = Regexp.last_match[1].to_i
|
456
|
+
image_rels[nr - 1] = "#{@tmpdir}/roo_image_rels#{nr}"
|
433
457
|
end
|
434
458
|
|
435
459
|
entry.extract(path) if path
|
@@ -437,7 +461,14 @@ module Roo
|
|
437
461
|
end
|
438
462
|
|
439
463
|
def safe_send(object, method, *args)
|
440
|
-
object.send(method, *args) if object
|
464
|
+
object.send(method, *args) if object&.respond_to?(method)
|
465
|
+
end
|
466
|
+
|
467
|
+
def worksheet_types
|
468
|
+
[
|
469
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet', # OOXML Transitional
|
470
|
+
'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet' # OOXML Strict
|
471
|
+
]
|
441
472
|
end
|
442
473
|
end
|
443
474
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Roo
|
2
|
+
module Formatters
|
3
|
+
module Base
|
4
|
+
# converts an integer value to a time string like '02:05:06'
|
5
|
+
def integer_to_timestring(content)
|
6
|
+
h = (content / 3600.0).floor
|
7
|
+
content -= h * 3600
|
8
|
+
m = (content / 60.0).floor
|
9
|
+
content -= m * 60
|
10
|
+
s = content
|
11
|
+
Kernel.format("%02d:%02d:%02d", h, m, s)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Roo
|
2
|
+
module Formatters
|
3
|
+
module CSV
|
4
|
+
def to_csv(filename = nil, separator = ",", sheet = default_sheet)
|
5
|
+
if filename
|
6
|
+
File.open(filename, "w") do |file|
|
7
|
+
write_csv_content(file, sheet, separator)
|
8
|
+
end
|
9
|
+
true
|
10
|
+
else
|
11
|
+
sio = ::StringIO.new
|
12
|
+
write_csv_content(sio, sheet, separator)
|
13
|
+
sio.rewind
|
14
|
+
sio.read
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Write all cells to the csv file. File can be a filename or nil. If the
|
21
|
+
# file argument is nil the output goes to STDOUT
|
22
|
+
def write_csv_content(file = nil, sheet = nil, separator = ",")
|
23
|
+
file ||= STDOUT
|
24
|
+
return unless first_row(sheet) # The sheet is empty
|
25
|
+
|
26
|
+
1.upto(last_row(sheet)) do |row|
|
27
|
+
1.upto(last_column(sheet)) do |col|
|
28
|
+
# TODO: use CSV.generate_line
|
29
|
+
file.print(separator) if col > 1
|
30
|
+
file.print cell_to_csv(row, col, sheet)
|
31
|
+
end
|
32
|
+
file.print("\n")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# The content of a cell in the csv output
|
37
|
+
def cell_to_csv(row, col, sheet)
|
38
|
+
return "" if empty?(row, col, sheet)
|
39
|
+
|
40
|
+
onecell = cell(row, col, sheet)
|
41
|
+
|
42
|
+
case celltype(row, col, sheet)
|
43
|
+
when :string
|
44
|
+
%("#{onecell.gsub('"', '""')}") unless onecell.empty?
|
45
|
+
when :boolean
|
46
|
+
# TODO: this only works for excelx
|
47
|
+
onecell = self.sheet_for(sheet).cells[[row, col]].formatted_value
|
48
|
+
%("#{onecell.gsub('"', '""').downcase}")
|
49
|
+
when :float, :percentage
|
50
|
+
if onecell == onecell.to_i
|
51
|
+
onecell.to_i.to_s
|
52
|
+
else
|
53
|
+
onecell.to_s
|
54
|
+
end
|
55
|
+
when :formula
|
56
|
+
case onecell
|
57
|
+
when String
|
58
|
+
%("#{onecell.gsub('"', '""')}") unless onecell.empty?
|
59
|
+
when Integer
|
60
|
+
onecell.to_s
|
61
|
+
when Float
|
62
|
+
if onecell == onecell.to_i
|
63
|
+
onecell.to_i.to_s
|
64
|
+
else
|
65
|
+
onecell.to_s
|
66
|
+
end
|
67
|
+
when Date, DateTime, TrueClass, FalseClass
|
68
|
+
onecell.to_s
|
69
|
+
else
|
70
|
+
fail "unhandled onecell-class #{onecell.class}"
|
71
|
+
end
|
72
|
+
when :date, :datetime
|
73
|
+
onecell.to_s
|
74
|
+
when :time
|
75
|
+
integer_to_timestring(onecell)
|
76
|
+
when :link
|
77
|
+
%("#{onecell.url.gsub('"', '""')}")
|
78
|
+
else
|
79
|
+
fail "unhandled celltype #{celltype(row, col, sheet)}"
|
80
|
+
end || ""
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Roo
|
2
|
+
module Formatters
|
3
|
+
module Matrix
|
4
|
+
# returns a matrix object from the whole sheet or a rectangular area of a sheet
|
5
|
+
def to_matrix(from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet)
|
6
|
+
require 'matrix'
|
7
|
+
|
8
|
+
return ::Matrix.empty unless first_row
|
9
|
+
|
10
|
+
from_row ||= first_row(sheet)
|
11
|
+
to_row ||= last_row(sheet)
|
12
|
+
from_column ||= first_column(sheet)
|
13
|
+
to_column ||= last_column(sheet)
|
14
|
+
|
15
|
+
::Matrix.rows(from_row.upto(to_row).map do |row|
|
16
|
+
from_column.upto(to_column).map do |col|
|
17
|
+
cell(row, col, sheet)
|
18
|
+
end
|
19
|
+
end)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# returns an XML representation of all sheets of a spreadsheet file
|
2
|
+
module Roo
|
3
|
+
module Formatters
|
4
|
+
module XML
|
5
|
+
def to_xml
|
6
|
+
Nokogiri::XML::Builder.new do |xml|
|
7
|
+
xml.spreadsheet do
|
8
|
+
sheets.each do |sheet|
|
9
|
+
self.default_sheet = sheet
|
10
|
+
xml.sheet(name: sheet) do |x|
|
11
|
+
if first_row && last_row && first_column && last_column
|
12
|
+
# sonst gibt es Fehler bei leeren Blaettern
|
13
|
+
first_row.upto(last_row) do |row|
|
14
|
+
first_column.upto(last_column) do |col|
|
15
|
+
next if empty?(row, col)
|
16
|
+
|
17
|
+
x.cell(cell(row, col),
|
18
|
+
row: row,
|
19
|
+
column: col,
|
20
|
+
type: celltype(row, col))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end.to_xml
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Roo
|
2
|
+
module Formatters
|
3
|
+
module YAML
|
4
|
+
# returns a rectangular area (default: all cells) as yaml-output
|
5
|
+
# you can add additional attributes with the prefix parameter like:
|
6
|
+
# oo.to_yaml({"file"=>"flightdata_2007-06-26", "sheet" => "1"})
|
7
|
+
def to_yaml(prefix = {}, from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet)
|
8
|
+
# return an empty string if there is no first_row, i.e. the sheet is empty
|
9
|
+
return "" unless first_row
|
10
|
+
|
11
|
+
from_row ||= first_row(sheet)
|
12
|
+
to_row ||= last_row(sheet)
|
13
|
+
from_column ||= first_column(sheet)
|
14
|
+
to_column ||= last_column(sheet)
|
15
|
+
|
16
|
+
result = "--- \n"
|
17
|
+
from_row.upto(to_row) do |row|
|
18
|
+
from_column.upto(to_column) do |col|
|
19
|
+
next if empty?(row, col, sheet)
|
20
|
+
|
21
|
+
result << "cell_#{row}_#{col}: \n"
|
22
|
+
prefix.each do|k, v|
|
23
|
+
result << " #{k}: #{v} \n"
|
24
|
+
end
|
25
|
+
result << " row: #{row} \n"
|
26
|
+
result << " col: #{col} \n"
|
27
|
+
result << " celltype: #{celltype(row, col, sheet)} \n"
|
28
|
+
value = cell(row, col, sheet)
|
29
|
+
if celltype(row, col, sheet) == :time
|
30
|
+
value = integer_to_timestring(value)
|
31
|
+
end
|
32
|
+
result << " value: #{value} \n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roo
|
4
|
+
module Helpers
|
5
|
+
module DefaultAttrReader
|
6
|
+
def attr_reader_with_default(attr_hash)
|
7
|
+
attr_hash.each do |attr_name, default_value|
|
8
|
+
instance_variable = :"@#{attr_name}"
|
9
|
+
define_method attr_name do
|
10
|
+
if instance_variable_defined? instance_variable
|
11
|
+
instance_variable_get instance_variable
|
12
|
+
else
|
13
|
+
default_value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|