winexcel 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +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
|