winexcel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +156 -0
- data/Rakefile +7 -0
- data/examples/basic.rb +49 -0
- data/examples/extended.rb +73 -0
- data/examples/fixtures/a.xls +0 -0
- data/examples/fixtures/b.xlsx +0 -0
- data/examples/fixtures/template.xls +0 -0
- data/examples/my_basic_file.xls +0 -0
- data/examples/my_extended_file.xls +0 -0
- data/features/connect_to_open_excel_file.feature +32 -0
- data/features/create_excel_file_from_template_and_open.feature +35 -0
- data/features/fixtures/a.xls +0 -0
- data/features/fixtures/b.xlsx +0 -0
- data/features/open_excel_file.feature +41 -0
- data/features/step_definitions/excel_file_steps.rb +71 -0
- data/features/support/common.rb +37 -0
- data/features/support/env.rb +14 -0
- data/features/support/matchers.rb +9 -0
- data/lib/winexcel.rb +12 -0
- data/lib/winexcel/core_ext/object.rb +34 -0
- data/lib/winexcel/core_ext/ordered_hash.rb +100 -0
- data/lib/winexcel/excel/xl_file_format.rb +74 -0
- data/lib/winexcel/excel_file.rb +255 -0
- data/lib/winexcel/excel_file/common_methods.rb +422 -0
- data/lib/winexcel/excel_file/other_methods.rb +315 -0
- data/lib/winexcel/excel_file/write_2D_array.rb +169 -0
- data/lib/winexcel/fileutils_ext/backup_file.rb +25 -0
- data/lib/winexcel/fileutils_ext/create_dir_if_missing.rb +18 -0
- data/lib/winexcel/version.rb +3 -0
- data/lib/winexcel/win32olerot_ext/win32olerot.so +0 -0
- data/tasks/cucumber.rake +23 -0
- data/tasks/rspec.rake +9 -0
- data/winexcel.gemspec +26 -0
- metadata +163 -0
@@ -0,0 +1,422 @@
|
|
1
|
+
# excel_file.rb
|
2
|
+
#
|
3
|
+
# File based on Xls.rb being part of 'wwatf' project
|
4
|
+
# Copyright (C) 2010-2011 Sobieraj Kamil <ksob@dslowl.com>.
|
5
|
+
#
|
6
|
+
# This file is published under New BSD License
|
7
|
+
# You can redistribute and/or
|
8
|
+
# modify it under the terms of the New BSD License.
|
9
|
+
#
|
10
|
+
# New BSD License claims:
|
11
|
+
# Redistribution and use in source and binary forms, with or without
|
12
|
+
# modification, are permitted provided that the following conditions
|
13
|
+
# are met:
|
14
|
+
#
|
15
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
16
|
+
# this list of conditions and the following disclaimer.
|
17
|
+
#
|
18
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
19
|
+
# this list of conditions and the following disclaimer in the documentation
|
20
|
+
# and/or other materials provided with the distribution.
|
21
|
+
#
|
22
|
+
# 3. Neither the name of Zend Technologies USA, Inc. nor the names of its
|
23
|
+
# contributors may be used to endorse or promote products derived from this
|
24
|
+
# software without specific prior written permission.
|
25
|
+
#
|
26
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
27
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
28
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
29
|
+
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
30
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
31
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
32
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
33
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
34
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
35
|
+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
36
|
+
|
37
|
+
|
38
|
+
require 'logger'
|
39
|
+
require 'win32ole'
|
40
|
+
|
41
|
+
require 'rubygems'
|
42
|
+
if RUBY_VERSION >= '1.9'
|
43
|
+
require 'win32olerot'
|
44
|
+
else
|
45
|
+
require 'winexcel/win32olerot_ext/win32olerot'
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Excel interface class.
|
50
|
+
# This class provides many simple methods for using Excel spreadsheets.
|
51
|
+
#
|
52
|
+
# Inner working:
|
53
|
+
# If the File is already open in excel, data is read from the open file
|
54
|
+
# and left open after the call to the close/finalize method.
|
55
|
+
# If the file is not open, It will be opened in the background
|
56
|
+
# and closed when the close/finalize method is called.
|
57
|
+
#
|
58
|
+
# For examples look at the examples folder as well as Cucumber/RSpec
|
59
|
+
# files in features and spec directories
|
60
|
+
#
|
61
|
+
# Information for developers:
|
62
|
+
# It is helpful to use COM/Win32OLE tracking tool like "oakland ActiveX Inspector"
|
63
|
+
# to clearly see what Excel instances are beeing created
|
64
|
+
#
|
65
|
+
module WinExcel
|
66
|
+
|
67
|
+
class ExcelFile
|
68
|
+
|
69
|
+
#returns a 2D Array representing the given range of Data stored in a given worksheet
|
70
|
+
#Note: All contiguious ranges are supported, however, only non-contigious column sellections of equal size are accepted.
|
71
|
+
# *myRange* can either be a string representing a range: "A1:C4", a named range defined in the workbook: "SomeNamedRange", or the text in a cell "myRangeing" a contigious table of values below it. If it is nil or not specified the CurrentRegion starting at "A1" is used.
|
72
|
+
#
|
73
|
+
#EXAMPLE DATA:
|
74
|
+
# A B C
|
75
|
+
# 1 ID name nickname
|
76
|
+
# 2 001 Fredrick White fred
|
77
|
+
# 3 002 Robert Green bob
|
78
|
+
#RETURNS:
|
79
|
+
# Calling get2DArray("A1:C3") would return the following 2D array
|
80
|
+
# [[ID, name, nickname],
|
81
|
+
# [001, Fredrick White, fred],
|
82
|
+
# [002, Robert Green, bob]]
|
83
|
+
def get2DArray(myRange=nil, sheet=nil, enableCaching=false)
|
84
|
+
if myRange != nil and myRange.instance_of?(String)
|
85
|
+
if myRange.match(/^[A-Za-z]+$/)
|
86
|
+
worksheet = getWorksheet(sheet)
|
87
|
+
row_min = worksheet.UsedRange.Row
|
88
|
+
row_max = row_min + worksheet.UsedRange.Rows.Count - 1
|
89
|
+
myRange = myRange + row_min.to_s + ':' + myRange + row_max.to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
if enableCaching
|
93
|
+
$ExcelCache ||= {}
|
94
|
+
if myRange != nil and myRange.instance_of?(String)
|
95
|
+
cacheKey = @workbook.Name.gsub(/\s+/, "").upcase + sheet.to_s + myRange.to_s
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
data = nil
|
101
|
+
if enableCaching and $ExcelCache[cacheKey]
|
102
|
+
data = $ExcelCache[cacheKey]
|
103
|
+
else
|
104
|
+
@log.info("get2DArray(myRange=#{myRange}, sheet = #{sheet}")
|
105
|
+
|
106
|
+
if myRange == nil or myRange.instance_of?(String)
|
107
|
+
myRange = getRange(myRange, sheet)
|
108
|
+
if myRange == nil
|
109
|
+
return nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
data = []
|
113
|
+
areas = []
|
114
|
+
|
115
|
+
#Deal with non-contigious regions by looping through each region.
|
116
|
+
myRange.Areas.each do |area|
|
117
|
+
areas << area.value #get the data from each area
|
118
|
+
end
|
119
|
+
|
120
|
+
numRecords = myRange.Rows.Count
|
121
|
+
(0..numRecords-1).each do |i|
|
122
|
+
record=[]
|
123
|
+
areas.each do |area|
|
124
|
+
if (area.kind_of? Array)
|
125
|
+
record.concat(area[i])
|
126
|
+
else
|
127
|
+
record << area
|
128
|
+
end
|
129
|
+
end
|
130
|
+
#Clean up formatting
|
131
|
+
record.collect! do |x|
|
132
|
+
if x.is_a?(Float) and x % 1 == 0
|
133
|
+
x.to_i.to_s
|
134
|
+
else
|
135
|
+
x.to_s.strip # need to_s.strip to get realword.
|
136
|
+
end
|
137
|
+
end
|
138
|
+
data << record
|
139
|
+
end
|
140
|
+
|
141
|
+
# save it to global cache
|
142
|
+
$ExcelCache[cacheKey] = data if enableCaching
|
143
|
+
end
|
144
|
+
|
145
|
+
return data.clone
|
146
|
+
end
|
147
|
+
|
148
|
+
class ExcelCell
|
149
|
+
attr_accessor :addr, :val
|
150
|
+
end
|
151
|
+
|
152
|
+
def getVisibleUsed2DArray(sheet=nil, offsetTolerance=5)
|
153
|
+
worksheet = getWorksheet(sheet)
|
154
|
+
if worksheet == nil
|
155
|
+
return nil
|
156
|
+
end
|
157
|
+
|
158
|
+
offsetTolerance = 30
|
159
|
+
contents = []
|
160
|
+
records = []
|
161
|
+
rowOffset = 0
|
162
|
+
maxOccupiedColCnt = 0
|
163
|
+
worksheet.UsedRange.Rows.each do |row|
|
164
|
+
record = []
|
165
|
+
colOffset = 0
|
166
|
+
isRowEmpty = true
|
167
|
+
row.Cells.each do |cell|
|
168
|
+
if cell.HasFormula or cell.Rows.Hidden or cell.Columns.Hidden
|
169
|
+
next
|
170
|
+
end
|
171
|
+
xlsCell = ExcelCell.new
|
172
|
+
xlsCell.addr = cell.Address
|
173
|
+
xlsCell.val = cell.Value.to_s.strip
|
174
|
+
if xlsCell.val.empty? then
|
175
|
+
colOffset = colOffset.succ
|
176
|
+
if colOffset > offsetTolerance + maxOccupiedColCnt then
|
177
|
+
break
|
178
|
+
end
|
179
|
+
else
|
180
|
+
isRowEmpty = false
|
181
|
+
colOffset = 0
|
182
|
+
end
|
183
|
+
|
184
|
+
record << xlsCell
|
185
|
+
end
|
186
|
+
|
187
|
+
if record.length > maxOccupiedColCnt then
|
188
|
+
maxOccupiedColCnt = record.length
|
189
|
+
end
|
190
|
+
|
191
|
+
if record.empty? then
|
192
|
+
next
|
193
|
+
elsif isRowEmpty then
|
194
|
+
rowOffset = rowOffset.succ
|
195
|
+
if rowOffset > offsetTolerance then
|
196
|
+
break
|
197
|
+
end
|
198
|
+
else
|
199
|
+
rowOffset = 0
|
200
|
+
records << record
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
return records
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
#Searches for the first occurance of *myRange* on *sheet* and returns the address of the range representing the contigious set of cells below(xlDown) and to the right(xlRight) of *myRange*
|
209
|
+
#If *sheet* is not specified, the first sheet is used.
|
210
|
+
# *myRange* can either be a string representing a range: "A1:C4", a named range defined in the workbook: "SomeNamedRange", or the text in a cell "myRangeing" a contigious table of values below it.
|
211
|
+
# If it is nil or not specified the CurrentRegion starting at "A1" is used.
|
212
|
+
def getRange(myRange="", sheet=nil)
|
213
|
+
@log.info(self.class) { "getRange(myRange=#{myRange}, sheet=#{sheet}" }
|
214
|
+
worksheet = getWorksheet(sheet)
|
215
|
+
if worksheet == nil
|
216
|
+
return nil
|
217
|
+
end
|
218
|
+
#find where the data is
|
219
|
+
if myRange.nil? or myRange == ""
|
220
|
+
#rng=worksheet.Range("A1").CurrentRegion
|
221
|
+
rng = worksheet.UsedRange
|
222
|
+
rng = worksheet.Range("A1:#{rng.Address.split(':')[1]}")
|
223
|
+
else
|
224
|
+
begin
|
225
|
+
#use myRange as an excel range if it is one
|
226
|
+
rng = worksheet.Range(myRange)
|
227
|
+
rescue WIN32OLERuntimeError #must not be a standard excel range... look for the myRange.
|
228
|
+
rng = worksheet.Range("A1", worksheet.UsedRange.SpecialCells(11)).Find(myRange) #xlCellTypeLastCell
|
229
|
+
raise "getRange(myRange=#{myRange}, sheet=#{sheet}) --> Could not locate range via specified myRange." unless rng
|
230
|
+
rng = rng.Offset(1)
|
231
|
+
rng = worksheet.Range(rng, rng.End(-4121)) #-4121 --> xlDown
|
232
|
+
rng = worksheet.Range(rng, rng.End(-4161)) #-4161 --> xlToRight
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
return rng
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
def getWorksheetCount()
|
241
|
+
return @workbook.Worksheets.Count
|
242
|
+
end
|
243
|
+
|
244
|
+
def getAllWorksheets()
|
245
|
+
return @workbook.Worksheets
|
246
|
+
end
|
247
|
+
|
248
|
+
def getWorksheet(sheet=nil)
|
249
|
+
if sheet.nil?
|
250
|
+
worksheet = @workbook.Worksheets(1)
|
251
|
+
if worksheet then
|
252
|
+
@log.info("getWorksheet(sheet=#{sheet}) --> #{worksheet.Name}")
|
253
|
+
end
|
254
|
+
return worksheet
|
255
|
+
elsif sheet.instance_of?(Regexp)
|
256
|
+
@workbook.Worksheets.each do |s|
|
257
|
+
if s.Name.upcase.match(sheet)
|
258
|
+
if s.Visible == 0 then
|
259
|
+
return nil
|
260
|
+
end
|
261
|
+
return s
|
262
|
+
end
|
263
|
+
end
|
264
|
+
elsif sheet.instance_of?(String)
|
265
|
+
@workbook.Worksheets.each do |s|
|
266
|
+
if s.Name.gsub(/\s+/, '').upcase == sheet.gsub(/\s+/, '').upcase
|
267
|
+
if s.Visible == 0 then
|
268
|
+
return nil
|
269
|
+
end
|
270
|
+
return s
|
271
|
+
end
|
272
|
+
end
|
273
|
+
#puts "Could not find sheet #{sheet} in #{@workbook.Name}" if @workbook.Name
|
274
|
+
else
|
275
|
+
return sheet
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def getWorksheets(visibleOnly=true)
|
280
|
+
if not visibleOnly then
|
281
|
+
return @workbook.Worksheets
|
282
|
+
end
|
283
|
+
arrWorksheets = []
|
284
|
+
@workbook.Worksheets.each do |s|
|
285
|
+
if s.Visible != 0 then
|
286
|
+
arrWorksheets << s
|
287
|
+
end
|
288
|
+
end
|
289
|
+
return arrWorksheets
|
290
|
+
end
|
291
|
+
|
292
|
+
|
293
|
+
#Closes Workbook if it was opened.
|
294
|
+
def closeWorkbookOnly(forceClose=false)
|
295
|
+
if forceClose or not @connectedToOpenWorkBook
|
296
|
+
@workbook.Close(false) # false for not to save changes
|
297
|
+
@log.info(self.class) { "Workbook Closed" }
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def close(forceClose=false)
|
302
|
+
closeWorkbookOnly(forceClose)
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
#writes out the 2D Array *data* starting at the specified range *myRange* on the specified sheet
|
307
|
+
def write2DArrayAtOnce(data, myRange, sheet = nil)
|
308
|
+
@log.info("write2DArray(data='...',myRange='#{myRange}', sheet = '#{sheet})'")
|
309
|
+
worksheet = getWorksheet(sheet)
|
310
|
+
#get the actual excel range object
|
311
|
+
myRange = worksheet.Range(myRange)
|
312
|
+
|
313
|
+
|
314
|
+
# find maximum row length (or column quantity) for the 'data' array,
|
315
|
+
# it would be column quantity of 2D array if we would put the 'data' array into one
|
316
|
+
maxWidth = 0
|
317
|
+
maxCellLength = 0
|
318
|
+
data.each do |row|
|
319
|
+
maxWidth = row.length if maxWidth < row.length
|
320
|
+
row.each do |cell|
|
321
|
+
maxCellLength = cell.to_s.length if cell and maxCellLength < cell.to_s.length
|
322
|
+
end
|
323
|
+
end
|
324
|
+
data.collect do |row|
|
325
|
+
while row.length < maxWidth
|
326
|
+
if not row.kind_of? Array
|
327
|
+
row = []
|
328
|
+
end
|
329
|
+
row << ''
|
330
|
+
end
|
331
|
+
end
|
332
|
+
if maxCellLength < 8204
|
333
|
+
myRange.Resize(data.length, maxWidth).Value = data if data and data.length > 0
|
334
|
+
else
|
335
|
+
(0..data.length-1).each do |row|
|
336
|
+
(0..data[row].length-1).each do |col|
|
337
|
+
myRange.Offset(row, col).value = data[row][col]
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
def appendWorksheet(name, visible = true)
|
347
|
+
sheet = @workbook.Worksheets.Add('After' => @workbook.Worksheets(@workbook.Worksheets.Count))
|
348
|
+
sheet.Name = name
|
349
|
+
sheet.Cells.NumberFormat = "@"
|
350
|
+
sheet.Visible = 0 if not visible
|
351
|
+
end
|
352
|
+
|
353
|
+
#appends the 2D Array *data* starting at the first empty row on the specified sheet.
|
354
|
+
def append2DArrayAtOnce(data, sheet = nil)
|
355
|
+
@log.info("append2DArray(data='...', sheet = '#{sheet})'")
|
356
|
+
worksheet = getWorksheet(sheet)
|
357
|
+
throw "append2DArray method cannot find '#{sheet}' sheet" if not worksheet
|
358
|
+
row_min = worksheet.UsedRange.Row
|
359
|
+
row_max = row_min + worksheet.UsedRange.Rows.Count - 1
|
360
|
+
col_min = worksheet.UsedRange.Column
|
361
|
+
col_max = col_min + worksheet.UsedRange.Columns.Count - 1
|
362
|
+
firstEmptyRow = row_max + 1
|
363
|
+
write2DArrayAtOnce(data, "A#{firstEmptyRow}", sheet)
|
364
|
+
end
|
365
|
+
|
366
|
+
#appends the 2D Array *data* starting at the first empty row on the specified sheet.
|
367
|
+
def append2DArray(data, sheet = nil)
|
368
|
+
@log.info("append2DArray(data='...', sheet = '#{sheet})'")
|
369
|
+
worksheet = getWorksheet(sheet)
|
370
|
+
throw "append2DArray method cannot find '#{sheet}' sheet" if not worksheet
|
371
|
+
row_min = worksheet.UsedRange.Row
|
372
|
+
row_max = row_min + worksheet.UsedRange.Rows.Count - 1
|
373
|
+
col_min = worksheet.UsedRange.Column
|
374
|
+
col_max = col_min + worksheet.UsedRange.Columns.Count - 1
|
375
|
+
firstEmptyRow = row_max + 1
|
376
|
+
write2DArray(data, "A#{firstEmptyRow}", sheet)
|
377
|
+
end
|
378
|
+
|
379
|
+
|
380
|
+
def setDisplayAlerts(val=true)
|
381
|
+
@excel.DisplayAlerts = val
|
382
|
+
end
|
383
|
+
|
384
|
+
#Saves the current workbook.
|
385
|
+
def save
|
386
|
+
setDisplayAlerts(false)
|
387
|
+
begin
|
388
|
+
@workbook.Save
|
389
|
+
rescue
|
390
|
+
@excel.Save
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
def worksheetExists?(sheet)
|
396
|
+
@workbook.Sheets(sheet).Name != ""
|
397
|
+
end
|
398
|
+
|
399
|
+
#Saves as the current workbook.
|
400
|
+
def saveAs(fileName)
|
401
|
+
@workbook.SaveAs(fileName.gsub("/", "\\"))
|
402
|
+
end
|
403
|
+
|
404
|
+
def setVisible(val=true)
|
405
|
+
@excel.Visible = val
|
406
|
+
end
|
407
|
+
|
408
|
+
def setInteractive(val=true)
|
409
|
+
@excel.Interactive = val
|
410
|
+
end
|
411
|
+
|
412
|
+
def setScreenUpdating(val=true)
|
413
|
+
@excel.ScreenUpdating = val
|
414
|
+
end
|
415
|
+
|
416
|
+
def setDisplayAlerts(val=true)
|
417
|
+
@excel.DisplayAlerts = val
|
418
|
+
end
|
419
|
+
|
420
|
+
end
|
421
|
+
|
422
|
+
end
|
@@ -0,0 +1,315 @@
|
|
1
|
+
# excel_file.rb
|
2
|
+
#
|
3
|
+
# File based on Xls.rb being part of 'wwatf' project
|
4
|
+
# Copyright (C) 2010-2011 Sobieraj Kamil <ksob@dslowl.com>.
|
5
|
+
#
|
6
|
+
# This file is published under New BSD License
|
7
|
+
# You can redistribute and/or
|
8
|
+
# modify it under the terms of the New BSD License.
|
9
|
+
#
|
10
|
+
# New BSD License claims:
|
11
|
+
# Redistribution and use in source and binary forms, with or without
|
12
|
+
# modification, are permitted provided that the following conditions
|
13
|
+
# are met:
|
14
|
+
#
|
15
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
16
|
+
# this list of conditions and the following disclaimer.
|
17
|
+
#
|
18
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
19
|
+
# this list of conditions and the following disclaimer in the documentation
|
20
|
+
# and/or other materials provided with the distribution.
|
21
|
+
#
|
22
|
+
# 3. Neither the name of Zend Technologies USA, Inc. nor the names of its
|
23
|
+
# contributors may be used to endorse or promote products derived from this
|
24
|
+
# software without specific prior written permission.
|
25
|
+
#
|
26
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
27
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
28
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
29
|
+
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
30
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
31
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
32
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
33
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
34
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
35
|
+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
36
|
+
|
37
|
+
|
38
|
+
require 'logger'
|
39
|
+
|
40
|
+
module WinExcel
|
41
|
+
|
42
|
+
class ExcelFile
|
43
|
+
|
44
|
+
module Range
|
45
|
+
CHARS_IN_ALPHABET = 26
|
46
|
+
SEPARATOR = ':'
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
#Given row and column number, returns Excel-formatted range
|
51
|
+
#e.g.
|
52
|
+
#given: row=5, column=4
|
53
|
+
#returns: "D5"
|
54
|
+
def self.toExcelRange(row, column)
|
55
|
+
unless (row > 0 and column > 0)
|
56
|
+
raise ArgumentError.new("Row and column should be positive numbers; given: row=#{row}, column=#{column}.")
|
57
|
+
end
|
58
|
+
|
59
|
+
columnName = ''
|
60
|
+
begin
|
61
|
+
column -= 1
|
62
|
+
column, rest = column.divmod(Range::CHARS_IN_ALPHABET)
|
63
|
+
columnName = (?A + rest).chr + columnName
|
64
|
+
end while column > 0
|
65
|
+
|
66
|
+
return columnName + row.to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.toExcelCompoundRange(rangeBegin, rangeEnd)
|
70
|
+
return (rangeBegin + Range::SEPARATOR + rangeEnd)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
#
|
75
|
+
# Protect method as it applies to the Worksheet object.
|
76
|
+
# Protects a worksheet so that it cannot be modified.
|
77
|
+
#
|
78
|
+
# sheet.Protect(Password, DrawingObjects, Contents, Scenarios, UserInterfaceOnly, AllowFormattingCells,
|
79
|
+
# AllowFormattingColumns, AllowFormattingRows, AllowInsertingColumns, AllowInsertingRows, AllowInsertingHyperlinks,
|
80
|
+
# AllowDeletingColumns, AllowDeletingRows, AllowSorting, AllowFiltering, AllowUsingPivotTables)
|
81
|
+
#
|
82
|
+
def protect(password="zx")
|
83
|
+
@workbook.Worksheets.each do |s|
|
84
|
+
s.Protect(
|
85
|
+
'Password' => password,
|
86
|
+
'DrawingObjects' => true,
|
87
|
+
'Contents' => true,
|
88
|
+
'Scenarios' => true,
|
89
|
+
'UserInterFaceOnly' => true,
|
90
|
+
'AllowFormattingCells' => true,
|
91
|
+
'AllowFormattingColumns' => true,
|
92
|
+
'AllowFormattingRows' => true,
|
93
|
+
'AllowInsertingColumns' => false,
|
94
|
+
'AllowInsertingRows' => false,
|
95
|
+
'AllowInsertingHyperlinks' => false,
|
96
|
+
'AllowDeletingColumns' => false,
|
97
|
+
'AllowDeletingRows' => false,
|
98
|
+
'AllowSorting' => true,
|
99
|
+
'AllowFiltering' => true,
|
100
|
+
'AllowUsingPivotTables' => true
|
101
|
+
)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def unprotect(password="zx")
|
106
|
+
@workbook.Worksheets.each do |s|
|
107
|
+
s.Unprotect(password)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def find(expr, sheet, alphaNumOnly=true)
|
113
|
+
if records[sheet].nil?
|
114
|
+
records[sheet] = getVisibleUsed2DArray(sheet)
|
115
|
+
end
|
116
|
+
hpExpr = expr.human_proof(alphaNumOnly)
|
117
|
+
hpExpr = Regexp.escape(hpExpr)
|
118
|
+
hpExpr = hpExpr.gsub("\\*", '.*')
|
119
|
+
hpExpr = /\A#{hpExpr}\Z/
|
120
|
+
records[sheet].each_with_index do |record, ri|
|
121
|
+
record.each_with_index do |cell, ci|
|
122
|
+
if hpExpr.match(cell.val.human_proof(alphaNumOnly))
|
123
|
+
return [ri, ci, cell.val]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
return nil
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Find an expression/text using standard excel find dialog
|
133
|
+
#
|
134
|
+
def findDialog(expr, sheet=nil)
|
135
|
+
|
136
|
+
worksheet = getWorksheet(sheet)
|
137
|
+
if worksheet == nil
|
138
|
+
return nil
|
139
|
+
end
|
140
|
+
|
141
|
+
range = worksheet.Cells.Find('What' => expr,
|
142
|
+
'SearchDirection' => ExcelConst::XlNext,
|
143
|
+
'SearchOrder' => ExcelConst::XlByRows,
|
144
|
+
'LookIn' => ExcelConst::XlValues,
|
145
|
+
'LookAt' => ExcelConst::XlWhole)
|
146
|
+
# Other options:
|
147
|
+
# ExcelConst::XlPrevious,
|
148
|
+
# ExcelConst::XlByColumns,
|
149
|
+
# MatchCase => False,
|
150
|
+
# After => ActiveCell,
|
151
|
+
return range
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
#Returns an array of hashes representing data records stored in rows in the given *myRange* and *sheet*.
|
156
|
+
# *myRange* can either be a string representing a range: "A1:C4", a named range defined in the workbook: "SomeNamedRange", or the text in a cell "myRangeing" a contigious table of values below it. If it is nil or not specified the CurrentRegion starting at "A1" is used.
|
157
|
+
#Note: *myRange* should include headers, and may contain non-contigious column ranges of equal size
|
158
|
+
#EXAMPLE DATA:
|
159
|
+
# A B C
|
160
|
+
# 1 ID name nickname
|
161
|
+
# 2 001 Fredrick White fred
|
162
|
+
# 3 002 Robert Green bob
|
163
|
+
#
|
164
|
+
#Standard Range example:
|
165
|
+
# getRowRecords("A1:C3") would return the following array of hashes:
|
166
|
+
# [ {'ID'=>'001', 'Name'=>'Fredrick White', 'Nickname'=>'fred'},
|
167
|
+
# {'ID'=>'002', 'Name'=>'Robert Green', 'Nickname'=>'bob'} ]
|
168
|
+
#
|
169
|
+
#Non-Contigious Range Example:
|
170
|
+
# getRowRecords("A1:A3,C1:C3") would return the following array of hashes:
|
171
|
+
# [ {'ID'=>'001', 'Nickname'=>'fred'},
|
172
|
+
# {'ID'=>'002', 'Nickname'=>'bob'} ]
|
173
|
+
def getRowRecords(myRange, sheet = nil)
|
174
|
+
return convert2DArrayToArrayHash(get2DArray(myRange, sheet), true)
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
#Returns an array of hashes representing data records stored in rows in the given *myRange* and *sheet*.
|
179
|
+
# *myRange* can either be a string representing a range: "A1:C4", a named range defined in the workbook: "SomeNamedRange", or the text in a cell "myRangeing" a contigious table of values below it. If it is nil or not specified the CurrentRegion starting at "A1" is used.
|
180
|
+
#Note: *myRange* should include headers, and may contain non-contigious column ranges of equal size
|
181
|
+
#EXAMPLE DATA:
|
182
|
+
# A B C
|
183
|
+
# 1 ID 001 002
|
184
|
+
# 2 Name Fredrick White Robert Green
|
185
|
+
# 3 NickName fred bob
|
186
|
+
#
|
187
|
+
#Standard Range Example:
|
188
|
+
# getColumnRecords("A1:C3") would return the following array of hashes:
|
189
|
+
# [ {'ID'=>'001', 'Name'=>'Fredrick White', 'Nickname'=>'fred'},
|
190
|
+
# {'ID'=>'002', 'Name'=>'Robert Green', 'Nickname'=>'bob'} ]
|
191
|
+
def getColumnRecords(myRange, sheet = nil)
|
192
|
+
return convert2DArrayToArrayHash(get2DArray(myRange, sheet), false)
|
193
|
+
end
|
194
|
+
|
195
|
+
#Returns a hash of key-value pairs where the keys are pulled from column 1 and the values are pulled form column 2 of *myRange* on *sheet*.
|
196
|
+
# *myRange* can either be a string representing a range: "A1:C4", a named range defined in the workbook: "SomeNamedRange", or the text in a cell "labeling" a contigious table of values below it. If it is nil or not specified the CurrentRegion starting at "A1" is used.
|
197
|
+
#Note: ':'s are striped off of each key if they exist.
|
198
|
+
#EXAMPLE DATA:
|
199
|
+
# A B
|
200
|
+
# 1 ID 001
|
201
|
+
# 2 Name: Fredrick White
|
202
|
+
# 3 NickName: fred
|
203
|
+
#Example usage:
|
204
|
+
# getHashFromRange("A1:B3") would return the following hash:
|
205
|
+
# {'ID'=>'001', 'Name'=>'Fredrick White', 'Nickname'=>'fred'}
|
206
|
+
def getHash(myRange = nil, sheet = nil, columnHeaders = false)
|
207
|
+
tmpHash = convert2DArrayToArrayHash(get2DArray(myRange, sheet), columnHeaders)[0]
|
208
|
+
newHash = CoreExt::OrderedHash.new
|
209
|
+
tmpHash.each do |key, value|
|
210
|
+
newHash[key.sub(/:/, '')] = value
|
211
|
+
end
|
212
|
+
return newHash
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
#*myArray* should either have column or row headers to use as keys. columnHeader=false implies that there are row headers.
|
217
|
+
def convert2DArrayToArrayHash(myArray, columnHeaders=true)
|
218
|
+
myArray = myArray.transpose unless columnHeaders
|
219
|
+
arrayHash=[]
|
220
|
+
(1..myArray.length-1).each do |i|
|
221
|
+
rowHash = Hash.new #OrderedHash.new #
|
222
|
+
(0..myArray[i].length-1).each do |j|
|
223
|
+
rowHash[myArray[0][j]] = myArray[i][j]
|
224
|
+
end
|
225
|
+
arrayHash << rowHash
|
226
|
+
end
|
227
|
+
return arrayHash
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
def convertArrayHashTo2DArray(myArrayHash)
|
232
|
+
@log.info(self.class) { "convertArrayHashTo2DArray(myArrayHash)" }
|
233
|
+
return [] if myArrayHash.empty?
|
234
|
+
|
235
|
+
my2DArray = []
|
236
|
+
#iterate through keys write out header row
|
237
|
+
myKeys = myArrayHash[0].keys
|
238
|
+
my2DArray << myKeys
|
239
|
+
#write out data
|
240
|
+
(0..myArrayHash.length-1).each do |row|
|
241
|
+
myRow = []
|
242
|
+
myKeys.each do |key|
|
243
|
+
myRow << myArrayHash[row][key]
|
244
|
+
end
|
245
|
+
my2DArray << myRow
|
246
|
+
end
|
247
|
+
return my2DArray
|
248
|
+
end
|
249
|
+
|
250
|
+
# Adds a new worksheet to workbook
|
251
|
+
def addSheet(sheetName)
|
252
|
+
@workbook.Sheets.Add
|
253
|
+
@workbook.ActiveSheet.Name = sheetName
|
254
|
+
end
|
255
|
+
|
256
|
+
# Deletes a worksheet from the workbook
|
257
|
+
def deleteSheet(sheetName = nil)
|
258
|
+
@excel.DisplayAlerts=false
|
259
|
+
sheet = getWorksheet(sheetName)
|
260
|
+
sheet.delete
|
261
|
+
end
|
262
|
+
|
263
|
+
|
264
|
+
def removeRowsWithSkipping(worksheet, rowsToSkip)
|
265
|
+
ur = worksheet.UsedRange.Rows.Count
|
266
|
+
start = rowsToSkip + 1
|
267
|
+
if ur >= start
|
268
|
+
myRange = "A#{start}:A#{ur}" #xlDown = -4121
|
269
|
+
range = worksheet.Range(myRange).EntireRow
|
270
|
+
range.Delete()
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
def write1DArrayColor(data, myRange, sheet = nil, col = 0)
|
276
|
+
@log.info(self.class) { "write2DArray(data='...',myRange='#{myRange}', sheet = '#{sheet})'" }
|
277
|
+
worksheet = getWorksheet(sheet)
|
278
|
+
#get the actual excel range object
|
279
|
+
myRange = worksheet.Range(myRange)
|
280
|
+
data.each_index do |row|
|
281
|
+
@log.debug(self.class) { data[row] }
|
282
|
+
myRange.Offset(row, col).Interior.Color = data[row]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
#writes out the Array hash *data* starting at the specified range *myRange* on the specified sheet.
|
288
|
+
#the keys are used as column headers starting at the specified range.
|
289
|
+
def writeArrayHash(data, myRange, sheet = nil)
|
290
|
+
@log.info(self.class) { "writeArrayHash(data='...',myRange='#{myRange}', sheet = '#{sheet})'" }
|
291
|
+
write2DArray(convertArrayHashTo2DArray(data), myRange, sheet)
|
292
|
+
end
|
293
|
+
|
294
|
+
#appends the 2D Array *data* starting at the first empty row on the specified sheet.
|
295
|
+
#the keys are used as column headers.
|
296
|
+
def appendArrayHash(data, sheet = nil)
|
297
|
+
@log.info(self.class) { "appendArrayHash(data='...', sheet = '#{sheet})'" }
|
298
|
+
data = convertArrayHashTo2DArray(data)
|
299
|
+
data.slice!(0)
|
300
|
+
append2DArray(data, sheet)
|
301
|
+
end
|
302
|
+
|
303
|
+
#outputs a 2DArray *myArray* to a CSV file specified by *file*.
|
304
|
+
def save2DArraytoCSVFile(myArray, file)
|
305
|
+
myFile = File.open(file, 'w')
|
306
|
+
@log.info(self.class) { "2DArraytoCSVFile(myArray=..., file=#{file})" }
|
307
|
+
(0..myArray.length-1).each do |i|
|
308
|
+
myFile.puts(myArray[i].join(',')) unless myArray[i].nil?
|
309
|
+
end
|
310
|
+
myFile.close
|
311
|
+
end
|
312
|
+
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|