rasta 0.1.8-x86-mswin32-60

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,528 @@
1
+
2
+ module Rasta
3
+ module Spreadsheet
4
+ require 'logger'
5
+ require 'singleton'
6
+
7
+ WORKING_DIR = Dir.getwd
8
+
9
+ # Singleton Class to store Excel Constant Variables
10
+ class ExcelConst; include Singleton; end
11
+
12
+ # Exceptions
13
+ class SheetNotFound < RuntimeError; end
14
+ class BookmarkNotFound < RuntimeError; end
15
+
16
+ # Singleton Class to store Excel Instance
17
+ #
18
+ # :visible Make the Excel Spreadsheet visible when processing
19
+ # :continue Continue from a bookmark
20
+ # :pagecount Continue for n pages after a bookmark
21
+ #
22
+ class Excel
23
+ include Singleton
24
+ attr_reader :excel, :visible
25
+ attr_accessor :continue, :currentrecord, :currentpage, :pagecount, :recordcount
26
+ def initialize
27
+ @visible = false
28
+ @continue = false
29
+ @currentrecord = 0
30
+ @currentpage = 0
31
+ @pagecount = 0
32
+ @recordcount = 0
33
+ @open_workbooks = []
34
+ @excel = WIN32OLE::new('excel.Application')
35
+ WIN32OLE.const_load(@excel, ExcelConst) if ExcelConst.constants == [] # load Excel constants
36
+ end
37
+
38
+ def open(filename)
39
+ @excel.Workbooks.Open(File.expand_path(filename))
40
+ end
41
+
42
+ def cleanup
43
+ if !@excel.visible
44
+ while @excel.ActiveWorkbook
45
+ @excel.ActiveWorkbook.Close(0)
46
+ end
47
+ @excel.Quit
48
+ end
49
+ end
50
+
51
+ def visible=(x)
52
+ @excel['Visible'] = x
53
+ @visible = x
54
+ end
55
+ end
56
+
57
+ # Bookmarks are ways to continue the spreadsheet from a given point.
58
+ # You can start from a specific tab and/or row/col in the spreadsheet.
59
+ # Additionally you can specify the number of pages and/or records to process
60
+ # so a user can start from SheetA and process 2 Sheets in the spreadsheet.
61
+ #
62
+ # Bookmarks are formatted as follows:
63
+ #
64
+ # PageName[Col|Row]
65
+ #
66
+ # where the Col/Row is an optional parameter. This gives the
67
+ # following bookmarks as possible continuation points:
68
+ #
69
+ # SheetA start from this sheet
70
+ # SheetA[10] this sheet is style :row, so start from row 10
71
+ # SheetA[F] this sheet is style :col, so start from column F
72
+ # true use the bookmark stored in .bookmarks
73
+ #
74
+ # The pagecount allows the user to start at the desired bookmark
75
+ # but only run through 1 or more sheets in the workbook
76
+ class Bookmark
77
+ @foundpage = false
78
+ @foundrecord = false
79
+ attr_accessor :page, :record
80
+
81
+ def initialize(bookmark)
82
+ # Parse the bookmark into it's parts so we can
83
+ # check for it as we read in the Sheet Records and Cells
84
+ @page, @record = parse_bookmark(bookmark)
85
+ raise ArgumentError, "Invalid record '#{@record}' - argument must be a row or column name" if @record && @record !~ /^([A-Z]+|\d+)$/i
86
+ end
87
+
88
+ # Check to see if the current record or page matches the bookmark.
89
+ # For example, found?(:sheet, [pagename]) or found?(:record, [row/col])
90
+ # where pagename is the name of the worksheet and row/col could be the
91
+ # row or column depending on the style of the page (eg: 'A' or 6)
92
+ def found?(how, what)
93
+ case how
94
+ when :sheet
95
+ if @foundpage
96
+ # Trap the case when a record is specified
97
+ # for a page and it does not exist
98
+ if !@foundrecord && what != @page
99
+ raise Spreadsheet::BookmarkNotFound, "Record #{@record} does not exist on page #{@page}"
100
+ end
101
+
102
+ true
103
+ else
104
+ if what == @page
105
+ @foundpage = true
106
+ # Set the foundrecord true so that it always
107
+ # passes the comparison if the record is not set
108
+ @foundrecord = true if !@record
109
+ true
110
+ else
111
+ false
112
+ end
113
+ end
114
+ when :record
115
+ raise Spreadsheet::BookmarkNotFound, 'Should never get here: page should have been found first' if !@foundpage
116
+ if @foundrecord
117
+ true
118
+ else
119
+ if what == @record
120
+ @foundrecord = true
121
+ true
122
+ else
123
+ # TODO: RAISE if wrong type. (row is specified but it's a col)
124
+ false
125
+ end
126
+ end
127
+ else
128
+ raise ArgumentError, "Don't know how to check bookmark for '#{how}'"
129
+ end
130
+ end
131
+
132
+ ##
133
+ # Returns true when either bookmark is not found or when pagecount has been reached.
134
+ def do_not_execute?(how, what)
135
+ !found?(how, what)
136
+ end
137
+
138
+ private
139
+
140
+ def parse_bookmark(name)
141
+ name =~ /([^\[]+)(\[(\S+)\])?/
142
+ pagename = $1
143
+ recordid = $3.upcase if $3
144
+ return pagename, recordid
145
+ end
146
+
147
+ end
148
+
149
+
150
+ # This object is a container for the Excel workbook and
151
+ # any options passed into the library.
152
+ #
153
+ # The book object can be parsed to find Records which in turn
154
+ # can be parsed to locate Cells.
155
+ class Book
156
+ require 'win32ole'
157
+ attr_reader :filename
158
+ attr_accessor :bookmark
159
+
160
+ def initialize(filename)
161
+ @excel = Excel.instance
162
+ @filename = filename
163
+ raise ArgumentError, 'XLS file required for Book.new()' if !@filename
164
+ raise IOError, "Unable to find file: '#{@filename}'" if ! File.file?(@filename)
165
+
166
+ # Create a bookmark
167
+ @bookmark = Bookmark.new(@excel.continue)
168
+
169
+ # Open the workbook and get the ole reference to the workbook object
170
+ @o = @excel.open(@filename)
171
+ end
172
+
173
+ def [](name)
174
+ worksheet = @o.Worksheets(name)
175
+ if valid_worksheet(worksheet)
176
+ Sheet.new(self, worksheet)
177
+ else
178
+ raise SheetNotFound, name
179
+ end
180
+ end
181
+
182
+ def each
183
+ @o.Worksheets.each do |worksheet|
184
+ next if !valid_worksheet(worksheet)
185
+ Excel.instance.currentpage += 1
186
+ return if (@excel.pagecount > 0 && @excel.currentpage > @excel.pagecount)
187
+ return if (@excel.recordcount > 0 && @excel.currentrecord > @excel.recordcount)
188
+ yield Sheet.new(self, worksheet)
189
+ end
190
+ end
191
+
192
+ def valid_worksheet(worksheet)
193
+ return false if worksheet.Visible == 0 # Skip hidden worksheets
194
+ return false if worksheet.name =~ /^#/ # Skip commented sheets
195
+ return false if worksheet.Tab.ColorIndex != ExcelConst::XlColorIndexNone # Skip worksheets with colored tabs
196
+ return false if (@excel.continue && @bookmark.do_not_execute?(:sheet, worksheet.name))
197
+ return true
198
+ end
199
+
200
+ def ole_object
201
+ @o
202
+ end
203
+
204
+ def save
205
+ @o.Save if @excel
206
+ end
207
+
208
+ def close
209
+ @o.Close(0) if @excel
210
+ end
211
+ end
212
+
213
+ # Sheets store the information about records on the sheet.
214
+ # In order to allow the user to add comments and have flexibility
215
+ # with how the data is laid out we have the following requirements:
216
+ #
217
+ # * The data will start at the first non-bold cell closest to Cell 'A1'
218
+ # * Bold cells will be ignored until we hit the first data cell.
219
+ # * The header row (this is usually the attribute you're going to set) needs
220
+ # to be bolded. We will use which side of the data (top or left) to determine
221
+ # if the data is laid out in rows or in columns.
222
+ #
223
+ # Iterating over the Sheet will return Records which represent the row/column
224
+ #
225
+
226
+ class Sheet
227
+ attr_reader :style, :name, :headers, :book,
228
+ :firstrow, :firstcol, :lastrow, :lastcol
229
+
230
+ class ObjectError < RuntimeError; end
231
+
232
+ def initialize(book, worksheet)
233
+ # TODO: Add check for duplicates if option set
234
+ @book = book
235
+ @o = worksheet
236
+ @name = worksheet.name
237
+ self.select
238
+ # Order here is important because in these functions we
239
+ # will use the class variables from the prior call
240
+ (@lastrow, @lastcol) = locate_last_data_cell
241
+ (@firstrow, @firstcol) = locate_first_data_cell
242
+ (@headers, @style) = locate_headers
243
+ end
244
+
245
+ def to_s
246
+ "firstrow = #{@firstrow}\n" +
247
+ "firstcol = #{@firstcol}\n" +
248
+ "lastrow = #{@lastrow}\n" +
249
+ "lastcol = #{@lastcol}\n" +
250
+ "style = #{@style}\n"
251
+ end
252
+
253
+ def select
254
+ begin
255
+ @o.Select
256
+ rescue WIN32OLERuntimeError
257
+ raise ObjectError, "Unable to locate worksheet #{@name}"
258
+ end
259
+ end
260
+
261
+ def select_home_cell
262
+ self.select
263
+ begin
264
+ @o.Cells(1,1).Select
265
+ rescue WIN32OLERuntimeError
266
+ raise ObjectError, "Unable to select cell in #{@name}"
267
+ end
268
+ end
269
+
270
+ # A cell is a data cell if the cell's font is not bold
271
+ # and there is no background color
272
+ def datacell?(row,col)
273
+ @o.Cells(row,col).Font.Bold == false && @o.Cells(row,col).Interior.ColorIndex == ExcelConst::XlColorIndexNone
274
+ end
275
+
276
+ # Get the ole range object for a row or column
277
+ def cellrange(index, style=@style)
278
+ case style
279
+ when :row
280
+ @o.Range("#{colname(@firstcol)}#{index}:#{colname(@lastcol)}#{index}")
281
+ when :col
282
+ @o.Range("#{colname(index)}#{@firstrow}:#{colname(index)}#{@lastrow}")
283
+ end
284
+ end
285
+
286
+ # Get an array of the values of cells in the range describing the row or column
287
+ def cellrangevals(index, style=@style)
288
+ range = cellrange(index, style)
289
+ # It looks like range['Value'] returns an array with multiple
290
+ # items and a string with one item so coerce the string into
291
+ # a one-dimensional array
292
+ return [range['Value']] if range['Value'].class != Array
293
+ case style
294
+ when :row
295
+ range['Value'][0]
296
+ when :col
297
+ range['Value'].map{ |v| v[0] }
298
+ end
299
+ end
300
+
301
+ # Translate a numerical column index to the alpha worksheet column the user sees
302
+ def colname(col)
303
+ @o.Columns(col).address.slice!(/(\w+)/)
304
+ end
305
+
306
+ def each
307
+ case @style
308
+ when :row
309
+ firstrecord = @firstrow
310
+ lastrecord = @lastrow
311
+ when :col
312
+ firstrecord = @firstcol
313
+ lastrecord = @lastcol
314
+ end
315
+ (firstrecord..lastrecord).each do |record_index|
316
+ case @style
317
+ when :row
318
+ recordid = record_index.to_s
319
+ when :col
320
+ recordid = colname(record_index)
321
+ end
322
+ excel = Excel.instance
323
+ next if excel.continue && !@book.bookmark.found?(:record, recordid)
324
+ excel.currentrecord += 1
325
+ return if (excel.recordcount > 0 && excel.currentrecord > excel.recordcount)
326
+ yield Record.new(self, record_index)
327
+ end
328
+ end
329
+
330
+ def ole_object
331
+ @o
332
+ end
333
+
334
+ def cell (record_index, cell_index)
335
+ Cell.new(self, record_index, cell_index)
336
+ end
337
+
338
+ def [](index)
339
+ Record.new(self, index)
340
+ end
341
+
342
+ def dump
343
+ vals = []
344
+ self.each { |record| vals << record.dump }
345
+ return vals
346
+ end
347
+
348
+
349
+ private
350
+
351
+ ##
352
+ # Returns the fist row and column with data in the worksheet. If no data
353
+ # is found before the last row and column, these same values are returned.
354
+ def locate_first_data_cell
355
+ (1..@lastrow).each do |row|
356
+ (1..@lastcol).each do |col|
357
+ if datacell?(row,col)
358
+ return row, col
359
+ end
360
+ end
361
+ end
362
+ return @lastrow, @lastcol
363
+ end
364
+
365
+ ##
366
+ # Returns the last row and column with data in the worksheet. If no
367
+ # data is found (i.e., the sheet is empty) 1, 1 is returned.
368
+ def locate_last_data_cell
369
+ lastrow = @o.Cells.Find('What' => '*',
370
+ 'SearchDirection' => ExcelConst::XlPrevious,
371
+ 'SearchOrder' => ExcelConst::XlByRows)
372
+ lastcol = @o.Cells.Find('What' => '*',
373
+ 'SearchDirection' => ExcelConst::XlPrevious,
374
+ 'SearchOrder' => ExcelConst::XlByColumns)
375
+ if lastcol then col = lastcol.Column else col = 0 end
376
+ if lastrow then row = lastrow.Row else row = 0 end
377
+ return row, col
378
+ end
379
+
380
+ def locate_headers
381
+ # handle empty spreadsheet
382
+ return [nil, nil] if @firstrow + @lastrow + @firstcol + @lastcol == 0
383
+ headerrow = @firstrow - 1
384
+ if headerrow == 0
385
+ testcell = @o.Cells(1,@firstcol)
386
+ else
387
+ testcell = @o.Cells(headerrow,@firstcol)
388
+ end
389
+ if headerrow > 0 && testcell.Font.Bold && testcell.Value.to_s.strip != ''
390
+ style = :row
391
+ headers = cellrangevals(@firstrow-1, :row)
392
+ else
393
+ style = :col
394
+ headers = cellrangevals(@firstcol-1, :col)
395
+ end
396
+ headers.map!{|x| x.strip if x}
397
+ return headers, style
398
+ end
399
+ end
400
+
401
+ # Records store the information of a particular row/col of
402
+ # a worksheet and allow iterating through the Record's Cells
403
+ class Record
404
+ attr_reader :recordindex, :sheet, :book
405
+ def initialize(sheet, index)
406
+ @sheet = sheet
407
+ @book = sheet.book
408
+ @recordindex = index
409
+ @range = @sheet.cellrange(@recordindex)
410
+ end
411
+ def each
412
+ @sheet.headers.each_index do |cell_index| # should be a value for each header
413
+ cell = @sheet.cell(@recordindex,cell_index + 1)
414
+ next if cell.nil? # Make sure the header exists
415
+ # Skip empty cells and italicized cells
416
+ next if (cell.value == '' || cell.italic)
417
+ yield cell
418
+ end
419
+ end
420
+ def select
421
+ @range.Select
422
+ end
423
+ def color=(c)
424
+ @range.Interior.ColorIndex = c
425
+ end
426
+ def [](index)
427
+ Cell.new(@sheet, @recordindex, index)
428
+ end
429
+ def to_s
430
+ @sheet.style.to_s + ':' + @recordindex.to_s
431
+ end
432
+ def dump
433
+ vals = []
434
+ self.each { |cell| vals << cell.value }
435
+ return vals
436
+ end
437
+ end
438
+
439
+ # Cells store information on a specific worksheet cell.
440
+ # The name is the Excel name for the cell (ie: A1, B2, etc),
441
+ # the value is the value of the cell and the header is the
442
+ # header for that row/column (usually the attribute/function parameter
443
+ # we're trying to set)
444
+ class Cell
445
+ ARRAY = /\A\s*\[.+\]\s*\Z/ms
446
+ HASH = /\A\s*\{.+\}\s*\Z/ms
447
+ BOOL = /\A\s*(true|false)\s*\Z/i
448
+ NUMBER = /\A\s*-?\d+\.??\d*?\s*\Z/
449
+ REGEXP = /\A\s*(\/.+\/)\s*\Z/ms
450
+ attr_reader :name, :value, :recordid, :recordindex,
451
+ :sheet, :book
452
+
453
+ def initialize(sheet, record_index, cell_index)
454
+ @sheet = sheet
455
+ @book = sheet.book
456
+ @o = sheet.ole_object
457
+ @cellindex = cell_index
458
+ @recordindex = record_index
459
+ case @sheet.style
460
+ when :row
461
+ @row = @recordindex
462
+ @col = @cellindex + @sheet.firstcol - 1 # taking into account the start of the used data range
463
+ @cell = @o.Cells(@row,@col)
464
+ @cell.NumberFormat == "@" ? @value = @cell.Value : @value = @cell.Text
465
+ when :col
466
+ @row = @cellindex + @sheet.firstrow - 1 # taking into account the start of the used data range
467
+ @col = @recordindex
468
+ @cell = @o.Cells(@row,@col)
469
+ @cell.NumberFormat == "@" ? @value = @cell.Value : @value = @cell.Text
470
+ end
471
+ # Ignore blank values. There's not much use for cells
472
+ # that are not set so skip them and normalize the return
473
+ # to nil so we know that's the case
474
+ @value = @value.to_s.strip
475
+ if @value =~ NUMBER
476
+ @value = eval(@value) unless @value =~ /^0\d/
477
+ elsif @value =~ ARRAY || @value =~ HASH || @value =~ BOOL || @value =~ REGEXP
478
+ @value = @value.downcase if @value =~ BOOL # make sure it's not confused with a constant
479
+ @value = eval(@value)
480
+ end
481
+
482
+ # Put together the cell's name
483
+ column_letter = @sheet.colname(@col)
484
+ @name = column_letter + @row.to_s
485
+
486
+ # The recordid is the row/col for the record
487
+ case @sheet.style
488
+ when :row
489
+ @recordid = @row.to_s
490
+ when :col
491
+ @recordid = column_letter
492
+ end
493
+ end
494
+ def header
495
+ # Return the header but strip off a trailing () if
496
+ # the user added for clarification purposes
497
+ @sheet.headers[@cellindex-1].gsub(/\(\)$/,'') if @sheet.headers[@cellindex-1]
498
+ end
499
+ def color=(c)
500
+ @cell.Interior.ColorIndex = c
501
+ end
502
+ def color
503
+ @cell.Interior.ColorIndex
504
+ end
505
+ def italic
506
+ @cell.Font.Italic
507
+ end
508
+ def comment=(c)
509
+ if c && !@cell.Comment
510
+ @cell.AddComment(c)
511
+ @cell.Comment.Shape.TextFrame.AutoSize = true
512
+ @cell.Comment.Shape.TextFrame.Characters.Font.Size=8
513
+ end
514
+ end
515
+ def selectrecord
516
+ range = @sheet.cellrange(@recordindex)
517
+ range.Select
518
+ end
519
+ def value=(v)
520
+ @cell.Value = v
521
+ end
522
+ def ole_object
523
+ @cell
524
+ end
525
+ end
526
+
527
+ end # module
528
+ end # module
@@ -0,0 +1,9 @@
1
+ module Rasta
2
+ module VERSION
3
+ STRING = '0.1.8'
4
+ FULL_VERSION = "#{STRING}" #may add subversions here later
5
+ NAME = "rasta"
6
+ URL = "http://rasta.rubyforge.org/"
7
+ DESCRIPTION = "#{NAME}-#{FULL_VERSION}: Ruby Spreadsheet Test Automation\n#{URL}"
8
+ end
9
+ end