robust_excel_ole 1.4 → 1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Changelog +6 -0
- data/README.rdoc +7 -1
- data/docs/README_ranges.rdoc +1 -1
- data/lib/reo_console.rb +19 -17
- data/lib/robust_excel_ole.rb +1 -2
- data/lib/robust_excel_ole/bookstore.rb +45 -45
- data/lib/robust_excel_ole/cell.rb +11 -12
- data/lib/robust_excel_ole/cygwin.rb +11 -11
- data/lib/robust_excel_ole/excel.rb +218 -201
- data/lib/robust_excel_ole/general.rb +23 -21
- data/lib/robust_excel_ole/range.rb +18 -19
- data/lib/robust_excel_ole/reo_common.rb +95 -82
- data/lib/robust_excel_ole/version.rb +1 -1
- data/lib/robust_excel_ole/workbook.rb +199 -205
- data/lib/robust_excel_ole/worksheet.rb +33 -56
- data/lib/spec_helper.rb +3 -3
- data/spec/workbook_spec.rb +18 -0
- data/spec/workbook_specs/workbook_misc_spec.rb +2 -2
- data/spec/worksheet_spec.rb +18 -0
- metadata +2 -2
@@ -11,27 +11,27 @@ module RobustExcelOle
|
|
11
11
|
attr_accessor :excel
|
12
12
|
attr_accessor :ole_workbook
|
13
13
|
attr_accessor :stored_filename
|
14
|
-
attr_accessor :options
|
14
|
+
attr_accessor :options
|
15
15
|
attr_accessor :modified_cells
|
16
16
|
attr_reader :workbook
|
17
17
|
|
18
18
|
alias ole_object ole_workbook
|
19
19
|
|
20
|
-
DEFAULT_OPEN_OPTS = {
|
20
|
+
DEFAULT_OPEN_OPTS = {
|
21
21
|
:default => {:excel => :current},
|
22
|
-
:force => {},
|
22
|
+
:force => {},
|
23
23
|
:if_unsaved => :raise,
|
24
24
|
:if_obstructed => :raise,
|
25
25
|
:if_absent => :raise,
|
26
26
|
:read_only => false,
|
27
|
-
:check_compatibility => false,
|
27
|
+
:check_compatibility => false,
|
28
28
|
:update_links => :never
|
29
|
-
}
|
30
|
-
|
31
|
-
ABBREVIATIONS = [[:default,:d], [:force, :f], [:excel, :e], [:visible, :v]]
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
ABBREVIATIONS = [[:default,:d], [:force, :f], [:excel, :e], [:visible, :v]].freeze
|
32
32
|
|
33
33
|
class << self
|
34
|
-
|
34
|
+
|
35
35
|
# opens a workbook.
|
36
36
|
# @param [String] file the file name
|
37
37
|
# @param [Hash] opts the options
|
@@ -42,66 +42,66 @@ module RobustExcelOle
|
|
42
42
|
# @option opts [Symbol] :if_absent :raise (default) or :create
|
43
43
|
# @option opts [Boolean] :read_only true (default) or false
|
44
44
|
# @option opts [Boolean] :update_links :never (default), :always, :alert
|
45
|
-
# @option opts [Boolean] :calculation :manual, :automatic, or nil (default)
|
46
|
-
# options:
|
45
|
+
# @option opts [Boolean] :calculation :manual, :automatic, or nil (default)
|
46
|
+
# options:
|
47
47
|
# :default : if the workbook was already open before, then use (unchange) its properties,
|
48
48
|
# otherwise, i.e. if the workbook cannot be reopened, use the properties stated in :default
|
49
|
-
# :force : no matter whether the workbook was already open before, use the properties stated in :force
|
49
|
+
# :force : no matter whether the workbook was already open before, use the properties stated in :force
|
50
50
|
# :default and :force contain: :excel, :visible
|
51
|
-
# :excel :current (or :active or :reuse)
|
51
|
+
# :excel :current (or :active or :reuse)
|
52
52
|
# -> connects to a running (the first opened) Excel instance,
|
53
53
|
# excluding the hidden Excel instance, if it exists,
|
54
54
|
# otherwise opens in a new Excel instance.
|
55
|
-
# :new -> opens in a new Excel instance
|
55
|
+
# :new -> opens in a new Excel instance
|
56
56
|
# <excel-instance> -> opens in the given Excel instance
|
57
57
|
# :visible true, false, or nil (default)
|
58
58
|
# alternatives: :default_excel, :force_excel, :visible, :d, :f, :e, :v
|
59
59
|
# :if_unsaved if an unsaved workbook with the same name is open, then
|
60
60
|
# :raise -> raises an exception
|
61
|
-
# :forget -> close the unsaved workbook, open the new workbook
|
62
|
-
# :accept -> lets the unsaved workbook open
|
61
|
+
# :forget -> close the unsaved workbook, open the new workbook
|
62
|
+
# :accept -> lets the unsaved workbook open
|
63
63
|
# :alert or :excel -> gives control to Excel
|
64
64
|
# :new_excel -> opens the new workbook in a new Excel instance
|
65
65
|
# :if_obstructed if a workbook with the same name in a different path is open, then
|
66
|
-
# :raise -> raises an exception
|
66
|
+
# :raise -> raises an exception
|
67
67
|
# :forget -> closes the old workbook, open the new workbook
|
68
68
|
# :save -> saves the old workbook, close it, open the new workbook
|
69
69
|
# :close_if_saved -> closes the old workbook and open the new workbook, if the old workbook is saved,
|
70
70
|
# otherwise raises an exception.
|
71
|
-
# :new_excel -> opens the new workbook in a new Excel instance
|
71
|
+
# :new_excel -> opens the new workbook in a new Excel instance
|
72
72
|
# :if_absent :raise -> raises an exception , if the file does not exists
|
73
|
-
# :create -> creates a new Excel file, if it does not exists
|
74
|
-
# :read_only true -> opens in read-only mode
|
73
|
+
# :create -> creates a new Excel file, if it does not exists
|
74
|
+
# :read_only true -> opens in read-only mode
|
75
75
|
# :visible true -> makes the workbook visible
|
76
76
|
# :check_compatibility true -> check compatibility when saving
|
77
77
|
# :update_links true -> user is being asked how to update links, false -> links are never updated
|
78
78
|
# @return [Workbook] a representation of a workbook
|
79
|
-
def open(file, opts={ }, &block)
|
79
|
+
def open(file, opts = { }, &block)
|
80
80
|
options = @options = process_options(opts)
|
81
81
|
book = nil
|
82
|
-
if (
|
82
|
+
if (options[:force][:excel] != :new) && (options[:force][:excel] != :reserved_new)
|
83
83
|
# if readonly is true, then prefer a book that is given in force_excel if this option is set
|
84
84
|
forced_excel = if options[:force][:excel]
|
85
|
-
|
85
|
+
options[:force][:excel] == :current ? excel_class.new(:reuse => true) : excel_of(options[:force][:excel])
|
86
86
|
end
|
87
|
-
book = bookstore.fetch(file,
|
88
|
-
:prefer_writable => (
|
87
|
+
book = bookstore.fetch(file,
|
88
|
+
:prefer_writable => !(options[:read_only]),
|
89
89
|
:prefer_excel => (options[:read_only] ? forced_excel : nil)) rescue nil
|
90
90
|
if book
|
91
|
-
#if forced_excel != book.excel &&
|
91
|
+
# if forced_excel != book.excel &&
|
92
92
|
# (not (book.alive? && (not book.saved) && (not options[:if_unsaved] == :accept)))
|
93
|
-
if ((
|
94
|
-
|
93
|
+
if (!(options[:force][:excel]) || (forced_excel == book.excel)) &&
|
94
|
+
!(book.alive? && !book.saved && (options[:if_unsaved] != :accept))
|
95
95
|
book.options = options
|
96
96
|
book.ensure_excel(options) # unless book.excel.alive?
|
97
97
|
# if the ReadOnly status shall be changed, save, close and reopen it
|
98
|
-
if book.alive?
|
99
|
-
(book.writable
|
100
|
-
book.save if book.writable &&
|
98
|
+
if book.alive? && ((!book.writable && !(options[:read_only])) ||
|
99
|
+
(book.writable && options[:read_only]))
|
100
|
+
book.save if book.writable && !book.saved
|
101
101
|
book.close(:if_unsaved => :forget)
|
102
|
-
end
|
103
|
-
# reopens the book if it was closed
|
104
|
-
book.ensure_workbook(file,options) unless book.alive?
|
102
|
+
end
|
103
|
+
# reopens the book if it was closed
|
104
|
+
book.ensure_workbook(file,options) unless book.alive?
|
105
105
|
book.visible = options[:force][:visible] unless options[:force][:visible].nil?
|
106
106
|
book.CheckCompatibility = options[:check_compatibility] unless options[:check_compatibility].nil?
|
107
107
|
book.excel.calculation = options[:calculation] unless options[:calculation].nil?
|
@@ -111,7 +111,7 @@ module RobustExcelOle
|
|
111
111
|
end
|
112
112
|
new(file, options, &block)
|
113
113
|
end
|
114
|
-
end
|
114
|
+
end
|
115
115
|
|
116
116
|
# creates a Workbook object by opening an Excel file given its filename workbook
|
117
117
|
# or by promoting a Win32OLE object representing an Excel file
|
@@ -119,16 +119,16 @@ module RobustExcelOle
|
|
119
119
|
# @param [Hash] opts the options
|
120
120
|
# @option opts [Symbol] see above
|
121
121
|
# @return [Workbook] a workbook
|
122
|
-
def self.new(workbook, opts={ }, &block)
|
122
|
+
def self.new(workbook, opts = { }, &block)
|
123
123
|
opts = process_options(opts)
|
124
124
|
if workbook && (workbook.is_a? WIN32OLE)
|
125
125
|
filename = workbook.Fullname.tr('\\','/') rescue nil
|
126
126
|
if filename
|
127
127
|
book = bookstore.fetch(filename)
|
128
128
|
if book && book.alive?
|
129
|
-
book.visible = opts[:force][:visible] unless opts[:force].nil?
|
129
|
+
book.visible = opts[:force][:visible] unless opts[:force].nil? || opts[:force][:visible].nil?
|
130
130
|
book.excel.calculation = opts[:calculation] unless opts[:calculation].nil?
|
131
|
-
return book
|
131
|
+
return book
|
132
132
|
else
|
133
133
|
super
|
134
134
|
end
|
@@ -139,20 +139,20 @@ module RobustExcelOle
|
|
139
139
|
end
|
140
140
|
|
141
141
|
# creates a new Workbook object, if a file name is given
|
142
|
-
# Promotes the win32ole workbook to a Workbook object, if a win32ole-workbook is given
|
142
|
+
# Promotes the win32ole workbook to a Workbook object, if a win32ole-workbook is given
|
143
143
|
# @param [Variant] file_or_workbook file name or workbook
|
144
144
|
# @param [Hash] opts the options
|
145
145
|
# @option opts [Symbol] see above
|
146
146
|
# @return [Workbook] a workbook
|
147
|
-
def initialize(file_or_workbook, options={ }, &block)
|
148
|
-
#options = @options = self.class.process_options(options) if options.empty?
|
149
|
-
if file_or_workbook.is_a? WIN32OLE
|
147
|
+
def initialize(file_or_workbook, options = { }, &block)
|
148
|
+
# options = @options = self.class.process_options(options) if options.empty?
|
149
|
+
if file_or_workbook.is_a? WIN32OLE
|
150
150
|
workbook = file_or_workbook
|
151
|
-
@ole_workbook = workbook
|
151
|
+
@ole_workbook = workbook
|
152
152
|
# use the Excel instance where the workbook is opened
|
153
|
-
win32ole_excel = WIN32OLE.connect(workbook.Fullname).Application rescue nil
|
154
|
-
@excel = excel_class.new(win32ole_excel)
|
155
|
-
@excel.visible = options[force][:visible] unless options[:force][:visible].nil?
|
153
|
+
win32ole_excel = WIN32OLE.connect(workbook.Fullname).Application rescue nil
|
154
|
+
@excel = excel_class.new(win32ole_excel)
|
155
|
+
@excel.visible = options[force][:visible] unless options[:force][:visible].nil?
|
156
156
|
@excel.calculation = options[:calculation] unless options[:calculation].nil?
|
157
157
|
ensure_excel(options)
|
158
158
|
else
|
@@ -180,12 +180,12 @@ module RobustExcelOle
|
|
180
180
|
erg = {}
|
181
181
|
opts.each do |key,value|
|
182
182
|
new_key = key
|
183
|
-
ABBREVIATIONS.each{|long,short| new_key = long if key == short}
|
183
|
+
ABBREVIATIONS.each { |long,short| new_key = long if key == short }
|
184
184
|
if value.is_a?(Hash)
|
185
185
|
erg[new_key] = {}
|
186
186
|
value.each do |k,v|
|
187
187
|
new_k = k
|
188
|
-
ABBREVIATIONS.each{|l,s| new_k = l if k == s}
|
188
|
+
ABBREVIATIONS.each { |l,s| new_k = l if k == s }
|
189
189
|
erg[new_key][new_k] = v
|
190
190
|
end
|
191
191
|
else
|
@@ -195,20 +195,20 @@ module RobustExcelOle
|
|
195
195
|
erg[:default] = {} if erg[:default].nil?
|
196
196
|
erg[:force] = {} if erg[:force].nil?
|
197
197
|
force_list = [:visible, :excel]
|
198
|
-
erg.each {|key,value| erg[:force][key] = value if force_list.include?(key)}
|
198
|
+
erg.each { |key,value| erg[:force][key] = value if force_list.include?(key) }
|
199
199
|
erg[:default][:excel] = erg[:default_excel] unless erg[:default_excel].nil?
|
200
200
|
erg[:force][:excel] = erg[:force_excel] unless erg[:force_excel].nil?
|
201
|
-
erg[:default][:excel] = :current if
|
202
|
-
erg[:force][:excel] = :current if
|
201
|
+
erg[:default][:excel] = :current if erg[:default][:excel] == :reuse || erg[:default][:excel] == :active
|
202
|
+
erg[:force][:excel] = :current if erg[:force][:excel] == :reuse || erg[:force][:excel] == :active
|
203
203
|
erg
|
204
204
|
end
|
205
205
|
opts = translator.call(options)
|
206
|
-
default_open_opts = proc_opts[:use_defaults] ? DEFAULT_OPEN_OPTS :
|
206
|
+
default_open_opts = proc_opts[:use_defaults] ? DEFAULT_OPEN_OPTS :
|
207
207
|
{:default => {:excel => :current}, :force => {}, :update_links => :never }
|
208
208
|
default_opts = translator.call(default_open_opts)
|
209
209
|
opts = default_opts.merge(opts)
|
210
210
|
opts[:default] = default_opts[:default].merge(opts[:default]) unless opts[:default].nil?
|
211
|
-
opts[:force] = default_opts[:force].merge(opts[:force]) unless opts[:force].nil?
|
211
|
+
opts[:force] = default_opts[:force].merge(opts[:force]) unless opts[:force].nil?
|
212
212
|
opts
|
213
213
|
end
|
214
214
|
|
@@ -216,8 +216,8 @@ module RobustExcelOle
|
|
216
216
|
def self.excel_of(object) # :nodoc: #
|
217
217
|
if object.is_a? WIN32OLE
|
218
218
|
case object.ole_obj_help.name
|
219
|
-
when /Workbook/i
|
220
|
-
new(object).excel
|
219
|
+
when /Workbook/i
|
220
|
+
new(object).excel
|
221
221
|
when /Application/i
|
222
222
|
excel_class.new(object)
|
223
223
|
else
|
@@ -227,7 +227,7 @@ module RobustExcelOle
|
|
227
227
|
begin
|
228
228
|
object.excel
|
229
229
|
rescue
|
230
|
-
raise TypeREOError,
|
230
|
+
raise TypeREOError, 'given object is neither an Excel, a Workbook, nor a Win32ole'
|
231
231
|
end
|
232
232
|
end
|
233
233
|
end
|
@@ -235,34 +235,34 @@ module RobustExcelOle
|
|
235
235
|
public
|
236
236
|
|
237
237
|
def ensure_excel(options) # :nodoc: #
|
238
|
-
if excel && @excel.alive?
|
238
|
+
if excel && @excel.alive?
|
239
239
|
@excel.created = false
|
240
240
|
return
|
241
241
|
end
|
242
|
-
excel_option =
|
243
|
-
@excel = self.class.excel_of(excel_option) unless
|
244
|
-
excel_class.new(:reuse => false) if excel_option == :reserved_new
|
245
|
-
@excel = excel_class.new(:reuse => (excel_option == :current)) unless
|
242
|
+
excel_option = options[:force].nil? || options[:force][:excel].nil? ? options[:default][:excel] : options[:force][:excel]
|
243
|
+
@excel = self.class.excel_of(excel_option) unless excel_option == :current || excel_option == :new || excel_option == :reserved_new
|
244
|
+
excel_class.new(:reuse => false) if (excel_option == :reserved_new) && Excel.known_excel_instances.empty?
|
245
|
+
@excel = excel_class.new(:reuse => (excel_option == :current)) unless @excel && @excel.alive?
|
246
246
|
@excel
|
247
|
-
end
|
247
|
+
end
|
248
248
|
|
249
249
|
def ensure_workbook(file, options) # :nodoc: #
|
250
250
|
file = @stored_filename ? @stored_filename : file
|
251
|
-
raise(FileNameNotGiven,
|
252
|
-
raise(FileNotFound, "file #{General
|
251
|
+
raise(FileNameNotGiven, 'filename is nil') if file.nil?
|
252
|
+
raise(FileNotFound, "file #{General.absolute_path(file).inspect} is a directory") if File.directory?(file)
|
253
253
|
unless File.exist?(file)
|
254
254
|
if options[:if_absent] == :create
|
255
255
|
@ole_workbook = excel_class.current.generate_workbook(file)
|
256
|
-
else
|
257
|
-
raise FileNotFound, "file #{General
|
256
|
+
else
|
257
|
+
raise FileNotFound, "file #{General.absolute_path(file).inspect} not found"
|
258
258
|
end
|
259
259
|
end
|
260
260
|
@ole_workbook = @excel.Workbooks.Item(File.basename(file)) rescue nil
|
261
|
-
if @ole_workbook
|
262
|
-
obstructed_by_other_book = (File.basename(file) == File.basename(@ole_workbook.Fullname)) &&
|
263
|
-
(
|
261
|
+
if @ole_workbook
|
262
|
+
obstructed_by_other_book = (File.basename(file) == File.basename(@ole_workbook.Fullname)) &&
|
263
|
+
(General.absolute_path(file) != @ole_workbook.Fullname)
|
264
264
|
# if workbook is obstructed by a workbook with same name and different path
|
265
|
-
if obstructed_by_other_book
|
265
|
+
if obstructed_by_other_book
|
266
266
|
case options[:if_obstructed]
|
267
267
|
when :raise
|
268
268
|
raise WorkbookBlocked, "blocked by a workbook with the same name in a different path: #{@ole_workbook.Fullname.tr('\\','/')}"
|
@@ -276,14 +276,14 @@ module RobustExcelOle
|
|
276
276
|
@ole_workbook = nil
|
277
277
|
open_or_create_workbook(file, options)
|
278
278
|
when :close_if_saved
|
279
|
-
if
|
279
|
+
if !@ole_workbook.Saved
|
280
280
|
raise WorkbookBlocked, "workbook with the same name in a different path is unsaved: #{@ole_workbook.Fullname.tr('\\','/')}"
|
281
|
-
else
|
281
|
+
else
|
282
282
|
@ole_workbook.Close
|
283
283
|
@ole_workbook = nil
|
284
284
|
open_or_create_workbook(file, options)
|
285
285
|
end
|
286
|
-
when :new_excel
|
286
|
+
when :new_excel
|
287
287
|
@excel = excel_class.new(:reuse => false)
|
288
288
|
open_or_create_workbook(file, options)
|
289
289
|
else
|
@@ -291,7 +291,7 @@ module RobustExcelOle
|
|
291
291
|
end
|
292
292
|
else
|
293
293
|
# book open, not obstructed by an other book, but not saved and writable
|
294
|
-
|
294
|
+
unless @ole_workbook.Saved
|
295
295
|
case options[:if_unsaved]
|
296
296
|
when :raise
|
297
297
|
raise WorkbookNotSaved, "workbook is already open but not saved: #{File.basename(file).inspect}"
|
@@ -320,9 +320,9 @@ module RobustExcelOle
|
|
320
320
|
private
|
321
321
|
|
322
322
|
def open_or_create_workbook(file, options) # :nodoc: #
|
323
|
-
if
|
323
|
+
if !@ole_workbook || (options[:if_unsaved] == :alert) || options[:if_obstructed]
|
324
324
|
begin
|
325
|
-
filename = General
|
325
|
+
filename = General.absolute_path(file)
|
326
326
|
begin
|
327
327
|
workbooks = @excel.Workbooks
|
328
328
|
rescue WIN32OLERuntimeError => msg
|
@@ -330,7 +330,7 @@ module RobustExcelOle
|
|
330
330
|
end
|
331
331
|
begin
|
332
332
|
with_workaround_linked_workbooks_excel2007(options) do
|
333
|
-
workbooks.Open(filename, { 'ReadOnly' => options[:read_only]
|
333
|
+
workbooks.Open(filename, { 'ReadOnly' => options[:read_only],
|
334
334
|
'UpdateLinks' => updatelinks_vba(options[:update_links]) })
|
335
335
|
end
|
336
336
|
rescue WIN32OLERuntimeError => msg
|
@@ -345,9 +345,9 @@ module RobustExcelOle
|
|
345
345
|
rescue WIN32OLERuntimeError => msg
|
346
346
|
raise UnexpectedREOError, "WIN32OLERuntimeError: #{msg.message}"
|
347
347
|
end
|
348
|
-
if options[:force][:visible].nil? &&
|
349
|
-
if @excel.created
|
350
|
-
self.visible = options[:default][:visible]
|
348
|
+
if options[:force][:visible].nil? && !options[:default][:visible].nil?
|
349
|
+
if @excel.created
|
350
|
+
self.visible = options[:default][:visible]
|
351
351
|
else
|
352
352
|
self.window_visible = options[:default][:visible]
|
353
353
|
end
|
@@ -359,43 +359,43 @@ module RobustExcelOle
|
|
359
359
|
self.Saved = true # unless self.Saved # ToDo: this is too hard
|
360
360
|
rescue WIN32OLERuntimeError => msg
|
361
361
|
raise UnexpectedREOError, "WIN32OLERuntimeError: #{msg.message} #{msg.backtrace}"
|
362
|
-
end
|
362
|
+
end
|
363
363
|
end
|
364
364
|
end
|
365
365
|
end
|
366
366
|
|
367
|
-
# translating the option UpdateLinks from REO to VBA
|
367
|
+
# translating the option UpdateLinks from REO to VBA
|
368
368
|
# setting UpdateLinks works only if calculation mode is automatic,
|
369
369
|
# parameter 'UpdateLinks' has no effect
|
370
370
|
def updatelinks_vba(updatelinks_reo)
|
371
371
|
case updatelinks_reo
|
372
|
-
when :alert
|
373
|
-
when :never
|
374
|
-
when :always
|
372
|
+
when :alert then RobustExcelOle::XlUpdateLinksUserSetting
|
373
|
+
when :never then RobustExcelOle::XlUpdateLinksNever
|
374
|
+
when :always then RobustExcelOle::XlUpdateLinksAlways
|
375
375
|
else RobustExcelOle::XlUpdateLinksNever
|
376
376
|
end
|
377
377
|
end
|
378
378
|
|
379
|
-
# workaround for linked workbooks for Excel 2007:
|
379
|
+
# workaround for linked workbooks for Excel 2007:
|
380
380
|
# opening and closing a dummy workbook if Excel has no workbooks.
|
381
381
|
# delay: with visible: 0.2 sec, without visible almost none
|
382
382
|
def with_workaround_linked_workbooks_excel2007(options)
|
383
383
|
old_visible_value = @excel.Visible
|
384
384
|
workbooks = @excel.Workbooks
|
385
|
-
workaround_condition = @excel.Version.split(
|
385
|
+
workaround_condition = @excel.Version.split('.').first.to_i == 12 && workbooks.Count == 0
|
386
386
|
if workaround_condition
|
387
|
-
workbooks.Add
|
388
|
-
@excel.calculation = options[:calculation].nil? ? @excel.calculation : options[:calculation]
|
387
|
+
workbooks.Add
|
388
|
+
@excel.calculation = options[:calculation].nil? ? @excel.calculation : options[:calculation]
|
389
389
|
end
|
390
390
|
begin
|
391
|
-
|
391
|
+
# @excel.with_displayalerts(update_links_opt == :alert ? true : @excel.displayalerts) do
|
392
392
|
yield self
|
393
393
|
ensure
|
394
|
-
@excel.with_displayalerts(false){workbooks.Item(1).Close} if workaround_condition
|
395
|
-
@excel.visible = old_visible_value
|
394
|
+
@excel.with_displayalerts(false) { workbooks.Item(1).Close } if workaround_condition
|
395
|
+
@excel.visible = old_visible_value
|
396
396
|
end
|
397
397
|
end
|
398
|
-
|
398
|
+
|
399
399
|
public
|
400
400
|
|
401
401
|
# closes the workbook, if it is alive
|
@@ -403,15 +403,15 @@ module RobustExcelOle
|
|
403
403
|
# @option opts [Symbol] :if_unsaved :raise (default), :save, :forget, :keep_open, or :alert
|
404
404
|
# options:
|
405
405
|
# :if_unsaved if the workbook is unsaved
|
406
|
-
# :raise -> raises an exception
|
407
|
-
# :save -> saves the workbook before it is closed
|
408
|
-
# :forget -> closes the workbook
|
406
|
+
# :raise -> raises an exception
|
407
|
+
# :save -> saves the workbook before it is closed
|
408
|
+
# :forget -> closes the workbook
|
409
409
|
# :keep_open -> keep the workbook open
|
410
410
|
# :alert or :excel -> gives control to excel
|
411
411
|
# @raise WorkbookNotSaved if the option :if_unsaved is :raise and the workbook is unsaved
|
412
412
|
# @raise OptionInvalid if the options is invalid
|
413
413
|
def close(opts = {:if_unsaved => :raise})
|
414
|
-
if
|
414
|
+
if alive? && !@ole_workbook.Saved && writable
|
415
415
|
case opts[:if_unsaved]
|
416
416
|
when :raise
|
417
417
|
raise WorkbookNotSaved, "workbook is unsaved: #{File.basename(self.stored_filename).inspect}"
|
@@ -430,13 +430,13 @@ module RobustExcelOle
|
|
430
430
|
else
|
431
431
|
close_workbook
|
432
432
|
end
|
433
|
-
#trace "close: canceled by user" if alive? &&
|
433
|
+
# trace "close: canceled by user" if alive? &&
|
434
434
|
# (opts[:if_unsaved] == :alert || opts[:if_unsaved] == :excel) && (not @ole_workbook.Saved)
|
435
435
|
end
|
436
436
|
|
437
437
|
private
|
438
438
|
|
439
|
-
def close_workbook
|
439
|
+
def close_workbook
|
440
440
|
@ole_workbook.Close if alive?
|
441
441
|
@ole_workbook = nil unless alive?
|
442
442
|
end
|
@@ -447,7 +447,7 @@ module RobustExcelOle
|
|
447
447
|
def retain_saved
|
448
448
|
saved = self.Saved
|
449
449
|
begin
|
450
|
-
|
450
|
+
yield self
|
451
451
|
ensure
|
452
452
|
self.Saved = saved
|
453
453
|
end
|
@@ -470,25 +470,25 @@ module RobustExcelOle
|
|
470
470
|
end
|
471
471
|
|
472
472
|
# allows to read or modify a workbook such that its state remains unchanged
|
473
|
-
# state comprises: open, saved, writable, visible, calculation mode, check compatibility
|
473
|
+
# state comprises: open, saved, writable, visible, calculation mode, check compatibility
|
474
474
|
# remarks: works only for workbooks opened with RobustExcelOle
|
475
475
|
# @param [String] file the file name
|
476
476
|
# @param [Hash] opts the options
|
477
477
|
# @option opts [Variant] :if_closed :current (default), :new or an Excel instance
|
478
478
|
# @option opts [Boolean] :read_only true/false, open the workbook in read-only/read-write modus (save changes)
|
479
|
-
# @option opts [Boolean] :writable true/false changes of the workbook shall be saved/not saved
|
479
|
+
# @option opts [Boolean] :writable true/false changes of the workbook shall be saved/not saved
|
480
480
|
# @option opts [Boolean] :rw_change_excel Excel instance in which the workbook with the new
|
481
|
-
# write permissions shall be opened :current (default), :new or an Excel instance
|
482
|
-
# @option opts [Boolean] :keep_open whether the workbook shall be kept open after unobtrusively opening
|
481
|
+
# write permissions shall be opened :current (default), :new or an Excel instance
|
482
|
+
# @option opts [Boolean] :keep_open whether the workbook shall be kept open after unobtrusively opening
|
483
483
|
# @return [Workbook] a workbook
|
484
|
-
def self.unobtrusively(file, opts = { }
|
484
|
+
def self.unobtrusively(file, opts = { })
|
485
485
|
opts = {:if_closed => :current,
|
486
486
|
:rw_change_excel => :current,
|
487
487
|
:keep_open => false}.merge(opts)
|
488
|
-
raise OptionInvalid,
|
489
|
-
prefer_writable = (((
|
490
|
-
(
|
491
|
-
do_not_write = (opts[:read_only]
|
488
|
+
raise OptionInvalid, 'contradicting options' if opts[:writable] && opts[:read_only]
|
489
|
+
prefer_writable = ((!(opts[:read_only]) || opts[:writable] == true) &&
|
490
|
+
!(opts[:read_only].nil? && opts[:writable] == false))
|
491
|
+
do_not_write = (opts[:read_only] || (opts[:read_only].nil? && opts[:writable] == false))
|
492
492
|
book = bookstore.fetch(file, :prefer_writable => prefer_writable)
|
493
493
|
was_open = book && book.alive?
|
494
494
|
if was_open
|
@@ -497,17 +497,17 @@ module RobustExcelOle
|
|
497
497
|
was_visible = book.visible
|
498
498
|
was_calculation = book.calculation
|
499
499
|
was_check_compatibility = book.check_compatibility
|
500
|
-
if (
|
501
|
-
|
502
|
-
raise NotImplementedREOError,
|
500
|
+
if (opts[:writable] && !was_writable && !was_saved) ||
|
501
|
+
(opts[:read_only] && was_writable && !was_saved)
|
502
|
+
raise NotImplementedREOError, 'unsaved read-only workbook shall be written'
|
503
503
|
end
|
504
|
-
opts[:rw_change_excel] = book.excel if opts[:rw_change_excel]
|
505
|
-
end
|
506
|
-
change_rw_mode = ((opts[:read_only] && was_writable)
|
504
|
+
opts[:rw_change_excel] = book.excel if opts[:rw_change_excel] == :current
|
505
|
+
end
|
506
|
+
change_rw_mode = ((opts[:read_only] && was_writable) || (opts[:writable] && !was_writable))
|
507
507
|
begin
|
508
|
-
book =
|
509
|
-
if was_open
|
510
|
-
if change_rw_mode
|
508
|
+
book =
|
509
|
+
if was_open
|
510
|
+
if change_rw_mode
|
511
511
|
open(file, :force => {:excel => opts[:rw_change_excel]}, :read_only => do_not_write)
|
512
512
|
else
|
513
513
|
book
|
@@ -520,45 +520,45 @@ module RobustExcelOle
|
|
520
520
|
if book && book.alive?
|
521
521
|
book.save unless book.saved || do_not_write || book.ReadOnly
|
522
522
|
if was_open
|
523
|
-
if opts[:rw_change_excel]==book.excel && change_rw_mode
|
523
|
+
if opts[:rw_change_excel] == book.excel && change_rw_mode
|
524
524
|
book.close
|
525
|
-
book = open(file, :force => {:excel => opts[:rw_change_excel]}, :read_only =>
|
526
|
-
end
|
525
|
+
book = open(file, :force => {:excel => opts[:rw_change_excel]}, :read_only => !was_writable)
|
526
|
+
end
|
527
527
|
book.excel.calculation = was_calculation
|
528
528
|
book.CheckCompatibility = was_check_compatibility
|
529
|
-
#book.visible = was_visible # not necessary
|
529
|
+
# book.visible = was_visible # not necessary
|
530
530
|
end
|
531
|
-
book.Saved = (was_saved ||
|
531
|
+
book.Saved = (was_saved || !was_open)
|
532
532
|
book.close unless was_open || opts[:keep_open]
|
533
533
|
end
|
534
534
|
end
|
535
535
|
end
|
536
536
|
|
537
537
|
# reopens a closed workbook
|
538
|
-
# @options options
|
538
|
+
# @options options
|
539
539
|
def reopen(options = { })
|
540
540
|
book = self.class.open(@stored_filename, options)
|
541
|
-
raise WorkbookREOError(
|
541
|
+
raise WorkbookREOError('cannot reopen book') unless book && book.alive?
|
542
542
|
book
|
543
543
|
end
|
544
544
|
|
545
545
|
# simple save of a workbook.
|
546
546
|
# @option opts [Boolean] :discoloring states, whether colored ranges shall be discolored
|
547
547
|
# @return [Boolean] true, if successfully saved, nil otherwise
|
548
|
-
def save(opts = {:discoloring => false})
|
549
|
-
raise ObjectNotAlive,
|
550
|
-
raise WorkbookReadOnly,
|
548
|
+
def save(opts = {:discoloring => false})
|
549
|
+
raise ObjectNotAlive, 'workbook is not alive' unless alive?
|
550
|
+
raise WorkbookReadOnly, 'Not opened for writing (opened with :read_only option)' if @ole_workbook.ReadOnly
|
551
551
|
begin
|
552
|
-
discoloring if opts[:discoloring]
|
552
|
+
discoloring if opts[:discoloring]
|
553
553
|
@modified_cells = []
|
554
|
-
@ole_workbook.Save
|
554
|
+
@ole_workbook.Save
|
555
555
|
rescue WIN32OLERuntimeError => msg
|
556
|
-
if msg.message =~ /SaveAs/
|
557
|
-
raise WorkbookNotSaved,
|
556
|
+
if msg.message =~ /SaveAs/ && msg.message =~ /Workbook/
|
557
|
+
raise WorkbookNotSaved, 'workbook not saved'
|
558
558
|
else
|
559
559
|
raise UnexpectedREOError, "unknown WIN32OLERuntimeError:\n#{msg.message}"
|
560
|
-
end
|
561
|
-
end
|
560
|
+
end
|
561
|
+
end
|
562
562
|
true
|
563
563
|
end
|
564
564
|
|
@@ -567,28 +567,28 @@ module RobustExcelOle
|
|
567
567
|
# @param [Hash] opts the options
|
568
568
|
# @option opts [Symbol] :if_exists :raise (default), :overwrite, or :alert, :excel
|
569
569
|
# @option opts [Symbol] :if_obstructed :raise (default), :forget, :save, or :close_if_saved
|
570
|
-
# options:
|
571
|
-
# :if_exists if a file with the same name exists, then
|
570
|
+
# options:
|
571
|
+
# :if_exists if a file with the same name exists, then
|
572
572
|
# :raise -> raises an exception, dont't write the file (default)
|
573
573
|
# :overwrite -> writes the file, delete the old file
|
574
574
|
# :alert or :excel -> gives control to Excel
|
575
575
|
# :if_obstructed if a workbook with the same name and different path is already open and blocks the saving, then
|
576
|
-
# :raise -> raises an exception
|
576
|
+
# :raise -> raises an exception
|
577
577
|
# :forget -> closes the blocking workbook
|
578
578
|
# :save -> saves the blocking workbook and closes it
|
579
|
-
# :close_if_saved -> closes the blocking workbook, if it is saved,
|
579
|
+
# :close_if_saved -> closes the blocking workbook, if it is saved,
|
580
580
|
# otherwise raises an exception
|
581
581
|
# :discoloring states, whether colored ranges shall be discolored
|
582
582
|
# @return [Workbook], the book itself, if successfully saved, raises an exception otherwise
|
583
|
-
def save_as(file, opts = { }
|
584
|
-
raise FileNameNotGiven,
|
585
|
-
raise ObjectNotAlive,
|
586
|
-
raise WorkbookReadOnly,
|
583
|
+
def save_as(file, opts = { })
|
584
|
+
raise FileNameNotGiven, 'filename is nil' if file.nil?
|
585
|
+
raise ObjectNotAlive, 'workbook is not alive' unless alive?
|
586
|
+
raise WorkbookReadOnly, 'Not opened for writing (opened with :read_only option)' if @ole_workbook.ReadOnly
|
587
587
|
options = {
|
588
588
|
:if_exists => :raise,
|
589
|
-
:if_obstructed => :raise
|
589
|
+
:if_obstructed => :raise
|
590
590
|
}.merge(opts)
|
591
|
-
if File.exist?(file)
|
591
|
+
if File.exist?(file)
|
592
592
|
case options[:if_exists]
|
593
593
|
when :overwrite
|
594
594
|
if file == self.filename
|
@@ -598,10 +598,10 @@ module RobustExcelOle
|
|
598
598
|
begin
|
599
599
|
File.delete(file)
|
600
600
|
rescue Errno::EACCES
|
601
|
-
raise WorkbookBeingUsed,
|
601
|
+
raise WorkbookBeingUsed, 'workbook is open and used in Excel'
|
602
602
|
end
|
603
603
|
end
|
604
|
-
when :alert, :excel
|
604
|
+
when :alert, :excel
|
605
605
|
@excel.with_displayalerts true do
|
606
606
|
save_as_workbook(file, options)
|
607
607
|
end
|
@@ -613,7 +613,7 @@ module RobustExcelOle
|
|
613
613
|
end
|
614
614
|
end
|
615
615
|
other_workbook = @excel.Workbooks.Item(File.basename(file)) rescue nil
|
616
|
-
if other_workbook &&
|
616
|
+
if other_workbook && self.filename != other_workbook.Fullname.tr('\\','/')
|
617
617
|
case options[:if_obstructed]
|
618
618
|
when :raise
|
619
619
|
raise WorkbookBlocked, "blocked by another workbook: #{other_workbook.Fullname.tr('\\','/')}"
|
@@ -635,29 +635,27 @@ module RobustExcelOle
|
|
635
635
|
private
|
636
636
|
|
637
637
|
def discoloring
|
638
|
-
@modified_cells.each{|cell| cell.Interior.ColorIndex = XlNone}
|
638
|
+
@modified_cells.each { |cell| cell.Interior.ColorIndex = XlNone }
|
639
639
|
end
|
640
640
|
|
641
641
|
def save_as_workbook(file, options) # :nodoc: #
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
if
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
raise UnexpectedREOError, "unknown WIN32OELERuntimeError:\n#{msg.message}"
|
660
|
-
end
|
642
|
+
dirname, basename = File.split(file)
|
643
|
+
file_format =
|
644
|
+
case File.extname(basename)
|
645
|
+
when '.xls' then RobustExcelOle::XlExcel8
|
646
|
+
when '.xlsx' then RobustExcelOle::XlOpenXMLWorkbook
|
647
|
+
when '.xlsm' then RobustExcelOle::XlOpenXMLWorkbookMacroEnabled
|
648
|
+
end
|
649
|
+
discoloring if options[:discoloring]
|
650
|
+
@modified_cells = []
|
651
|
+
@ole_workbook.SaveAs(General.absolute_path(file), file_format)
|
652
|
+
bookstore.store(self)
|
653
|
+
rescue WIN32OLERuntimeError => msg
|
654
|
+
if msg.message =~ /SaveAs/ && msg.message =~ /Workbook/
|
655
|
+
# trace "save: canceled by user" if options[:if_exists] == :alert || options[:if_exists] == :excel
|
656
|
+
# another possible semantics. raise WorkbookREOError, "could not save Workbook"
|
657
|
+
else
|
658
|
+
raise UnexpectedREOError, "unknown WIN32OELERuntimeError:\n#{msg.message}"
|
661
659
|
end
|
662
660
|
end
|
663
661
|
|
@@ -686,12 +684,10 @@ module RobustExcelOle
|
|
686
684
|
# @param [String] or [Number]
|
687
685
|
# @returns [Worksheet]
|
688
686
|
def sheet(name)
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
end
|
694
|
-
end
|
687
|
+
worksheet_class.new(@ole_workbook.Worksheets.Item(name))
|
688
|
+
rescue WIN32OLERuntimeError => msg
|
689
|
+
raise NameNotFound, "could not return a sheet with name #{name.inspect}"
|
690
|
+
end
|
695
691
|
|
696
692
|
def each
|
697
693
|
@ole_workbook.Worksheets.each do |sheet|
|
@@ -706,7 +702,7 @@ module RobustExcelOle
|
|
706
702
|
i += 1
|
707
703
|
end
|
708
704
|
end
|
709
|
-
|
705
|
+
|
710
706
|
# copies a sheet to another position
|
711
707
|
# default: copied sheet is appended
|
712
708
|
# @param [Worksheet] sheet a sheet that shall be copied
|
@@ -723,7 +719,7 @@ module RobustExcelOle
|
|
723
719
|
new_sheet = worksheet_class.new(@excel.Activesheet)
|
724
720
|
new_sheet.name = new_sheet_name if new_sheet_name
|
725
721
|
new_sheet
|
726
|
-
end
|
722
|
+
end
|
727
723
|
|
728
724
|
# adds an empty sheet
|
729
725
|
# default: empty sheet is appended
|
@@ -740,7 +736,7 @@ module RobustExcelOle
|
|
740
736
|
new_sheet = worksheet_class.new(@excel.Activesheet)
|
741
737
|
new_sheet.name = new_sheet_name if new_sheet_name
|
742
738
|
new_sheet
|
743
|
-
end
|
739
|
+
end
|
744
740
|
|
745
741
|
# copies a sheet to another position if a sheet is given, or adds an empty sheet
|
746
742
|
# default: copied or empty sheet is appended, i.e. added behind the last sheet
|
@@ -756,12 +752,12 @@ module RobustExcelOle
|
|
756
752
|
sheet = nil
|
757
753
|
end
|
758
754
|
sheet ? copy_sheet(sheet, opts) : add_empty_sheet(opts)
|
759
|
-
end
|
755
|
+
end
|
760
756
|
|
761
757
|
# for compatibility to older versions
|
762
758
|
def add_sheet(sheet = nil, opts = { })
|
763
759
|
add_or_copy_sheet(sheet, opts)
|
764
|
-
end
|
760
|
+
end
|
765
761
|
|
766
762
|
def last_sheet
|
767
763
|
worksheet_class.new(@ole_workbook.Worksheets.Item(@ole_workbook.Worksheets.Count))
|
@@ -794,12 +790,12 @@ module RobustExcelOle
|
|
794
790
|
check_compatibility_before = check_compatibility
|
795
791
|
unless opts[:read_only].nil?
|
796
792
|
# if the ReadOnly status shall be changed, then close and reopen it
|
797
|
-
if (
|
798
|
-
opts[:check_compatibility] = check_compatibility if opts[:check_compatibility].nil?
|
799
|
-
close(:if_unsaved => true)
|
793
|
+
if (!writable && !(opts[:read_only])) || (writable && opts[:read_only])
|
794
|
+
opts[:check_compatibility] = check_compatibility if opts[:check_compatibility].nil?
|
795
|
+
close(:if_unsaved => true)
|
800
796
|
open_or_create_workbook(@stored_filename, opts)
|
801
797
|
end
|
802
|
-
end
|
798
|
+
end
|
803
799
|
self.visible = opts[:force][:visible].nil? ? visible_before : opts[:force][:visible]
|
804
800
|
self.CheckCompatibility = opts[:check_compatibility].nil? ? check_compatibility_before : opts[:check_compatibility]
|
805
801
|
@excel.calculation = opts[:calculation] unless opts[:calculation].nil?
|
@@ -814,14 +810,12 @@ module RobustExcelOle
|
|
814
810
|
|
815
811
|
# returns true, if the workbook reacts to methods, false otherwise
|
816
812
|
def alive?
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
false
|
824
|
-
end
|
813
|
+
@ole_workbook.Name
|
814
|
+
true
|
815
|
+
rescue
|
816
|
+
@ole_workbook = nil # dead object won't be alive again
|
817
|
+
# t $!.message
|
818
|
+
false
|
825
819
|
end
|
826
820
|
|
827
821
|
# returns the full file name of the workbook
|
@@ -830,7 +824,7 @@ module RobustExcelOle
|
|
830
824
|
end
|
831
825
|
|
832
826
|
def writable # :nodoc: #
|
833
|
-
|
827
|
+
!@ole_workbook.ReadOnly if @ole_workbook
|
834
828
|
end
|
835
829
|
|
836
830
|
def saved # :nodoc: #
|
@@ -845,7 +839,7 @@ module RobustExcelOle
|
|
845
839
|
@ole_workbook.CheckCompatibility if @ole_workbook
|
846
840
|
end
|
847
841
|
|
848
|
-
|
842
|
+
# returns true, if the workbook is visible, false otherwise
|
849
843
|
def visible
|
850
844
|
@excel.visible && @ole_workbook.Windows(@ole_workbook.Name).Visible
|
851
845
|
end
|
@@ -859,7 +853,7 @@ module RobustExcelOle
|
|
859
853
|
|
860
854
|
# returns true, if the window of the workbook is set to visible, false otherwise
|
861
855
|
def window_visible
|
862
|
-
|
856
|
+
@ole_workbook.Windows(@ole_workbook.Name).Visible
|
863
857
|
end
|
864
858
|
|
865
859
|
# makes the window of the workbook visible or invisible
|
@@ -870,11 +864,11 @@ module RobustExcelOle
|
|
870
864
|
end
|
871
865
|
end
|
872
866
|
|
873
|
-
# @return [Boolean] true, if the full book names and excel Instances are identical, false otherwise
|
867
|
+
# @return [Boolean] true, if the full book names and excel Instances are identical, false otherwise
|
874
868
|
def == other_book
|
875
869
|
other_book.is_a?(Workbook) &&
|
876
|
-
|
877
|
-
|
870
|
+
@excel == other_book.excel &&
|
871
|
+
self.filename == other_book.filename
|
878
872
|
end
|
879
873
|
|
880
874
|
def self.books
|
@@ -887,14 +881,14 @@ module RobustExcelOle
|
|
887
881
|
|
888
882
|
def bookstore # :nodoc: #
|
889
883
|
self.class.bookstore
|
890
|
-
end
|
884
|
+
end
|
891
885
|
|
892
886
|
def to_s # :nodoc: #
|
893
|
-
|
887
|
+
self.filename.to_s
|
894
888
|
end
|
895
889
|
|
896
890
|
def inspect # :nodoc: #
|
897
|
-
|
891
|
+
'#<Workbook: ' + ('not alive ' unless alive?).to_s + (File.basename(self.filename) if alive?).to_s + " #{@ole_workbook} #{@excel}" + '>'
|
898
892
|
end
|
899
893
|
|
900
894
|
def self.excel_class # :nodoc: #
|
@@ -902,7 +896,7 @@ module RobustExcelOle
|
|
902
896
|
module_name = self.parent_name
|
903
897
|
"#{module_name}::Excel".constantize
|
904
898
|
rescue NameError => e
|
905
|
-
#trace "excel_class: NameError: #{e}"
|
899
|
+
# trace "excel_class: NameError: #{e}"
|
906
900
|
Excel
|
907
901
|
end
|
908
902
|
end
|
@@ -929,23 +923,23 @@ module RobustExcelOle
|
|
929
923
|
private
|
930
924
|
|
931
925
|
def method_missing(name, *args) # :nodoc: #
|
932
|
-
if name.to_s[0,1] =~ /[A-Z]/
|
926
|
+
if name.to_s[0,1] =~ /[A-Z]/
|
933
927
|
begin
|
934
|
-
raise ObjectNotAlive,
|
928
|
+
raise ObjectNotAlive, 'method missing: workbook not alive' unless alive?
|
935
929
|
@ole_workbook.send(name, *args)
|
936
930
|
rescue WIN32OLERuntimeError => msg
|
937
931
|
if msg.message =~ /unknown property or method/
|
938
932
|
raise VBAMethodMissingError, "unknown VBA property or method #{name.inspect}"
|
939
|
-
else
|
933
|
+
else
|
940
934
|
raise msg
|
941
935
|
end
|
942
936
|
end
|
943
|
-
else
|
944
|
-
super
|
937
|
+
else
|
938
|
+
super
|
945
939
|
end
|
946
940
|
end
|
947
941
|
end
|
948
|
-
|
942
|
+
|
949
943
|
public
|
950
944
|
|
951
945
|
Book = Workbook
|