rasta 0.1.8-x86-mswin32-60

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.
@@ -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