rubylabs 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rubylabs.rb CHANGED
@@ -1,77 +1,87 @@
1
- =begin rdoc
2
-
3
- = RubyLabs
4
-
5
- Common methods used by two or more lab projects.
6
-
7
- Methods used to monitor execution of programs during experiments.
8
-
9
- =end
1
+ # RubyLabs gem, top level module.
10
2
 
11
3
  SCRIPT_LINES__ = Hash.new unless defined? SCRIPT_LINES__
12
4
 
13
- autoload :IntroLab, "introlab.rb"
14
- autoload :SieveLab, "sievelab.rb"
5
+ autoload :IntroLab, "introlab.rb"
6
+ autoload :SieveLab, "sievelab.rb"
15
7
  autoload :IterationLab, "iterationlab.rb"
16
8
  autoload :RecursionLab, "recursionlab.rb"
17
- autoload :HashLab, "hashlab.rb"
18
- autoload :BitLab, "bitlab.rb"
19
- autoload :MARSLab, "marslab.rb"
20
- autoload :RandomLab, "randomlab.rb"
21
- autoload :EncryptionLab, "encryptionlab.rb"
22
- autoload :ElizaLab, "elizalab.rb"
9
+ autoload :HashLab, "hashlab.rb"
10
+ autoload :BitLab, "bitlab.rb"
11
+ autoload :MARSLab, "marslab.rb"
12
+ autoload :RandomLab, "randomlab.rb"
13
+ autoload :EncryptionLab, "encryptionlab.rb"
14
+ autoload :ElizaLab, "elizalab.rb"
23
15
  autoload :SphereLab, "spherelab.rb"
24
- autoload :TSPLab, "tsplab.rb"
16
+ autoload :TSPLab, "tsplab.rb"
25
17
 
26
- autoload :Demos, "demos.rb"
18
+ autoload :Demos, "demos.rb"
27
19
 
28
20
  include Math
29
21
 
30
- module RubyLabs
31
-
32
22
  =begin rdoc
33
-
34
- === hello
35
23
 
36
- A simple 'hello world' method to test the installation.
24
+ == RubyLabs
25
+
26
+ RubyLabs is a collection of modules for projects described in
27
+ <em>Explorations in Computing: An Introduction to Computer Science</em>.
37
28
 
29
+ There is one submodule for each project, e.g. RubyLabs::SieveLab has
30
+ methods used in the Sieve of Eratosthenes project in Chapter 3.
31
+ The top level RubyLabs module also has common methods used in
32
+ several projects.
38
33
  =end
39
34
 
35
+ module RubyLabs
36
+
37
+ # A simple 'hello world' method to test the installation.
38
+
40
39
  def hello
41
40
  "Hello, you have successfully installed RubyLabs!"
42
41
  end
43
42
 
44
- =begin rdoc
45
- Log base 2.
46
- =end
47
-
48
- def log2(x)
49
- log(x) / log(2.0)
50
- end
51
-
52
- =begin rdoc
53
- Return the larger of a and b
54
- =end
43
+ # Return the larger of +a+ and +b+ (determined by calling <tt>a < b</tt>).
44
+ #
45
+ # :call-seq:
46
+ # max(a,b) => Object
47
+ #
55
48
 
56
49
  def max(a,b)
57
- a > b ? a : b
50
+ a < b ? b : a
58
51
  end
59
52
 
60
- =begin rdoc
61
- Return the smaller of +a+ and +b+
62
- =end
53
+ # Return the smaller of +a+ and +b+ (determined by calling <tt>a < b</tt>).
54
+ #
55
+ # :call-seq:
56
+ # min(a,b) => Object
57
+ #
63
58
 
64
59
  def min(a,b)
65
60
  a < b ? a : b
66
61
  end
67
62
 
68
-
69
- =begin rdoc
70
-
71
- Call +time { foo(...) }+ to measure the execution time of a call to +foo+. This
72
- method will time any arbitrary Ruby expression.
63
+ # Compute the logarithm (base 2) of a number +x+.
64
+ #
65
+ # :call-seq:
66
+ # log2(x) => Float
67
+ #
73
68
 
74
- =end
69
+ def log2(x)
70
+ log(x) / log(2.0)
71
+ end
72
+
73
+ # Measure the execution time of a block of code, which can be any Ruby expression
74
+ # enclosed in braces after the method name. The return value is the number of seconds
75
+ # required to evaluate the block.
76
+ # Any output produced by the block is discarded.
77
+ #
78
+ # Example:
79
+ # >> time { sieve(1000) }
80
+ # => 0.014921
81
+ #
82
+ # :call-seq:
83
+ # time { f } => Float
84
+ #
75
85
 
76
86
  def time(&f)
77
87
  tstart = Time.now
@@ -79,91 +89,132 @@ method will time any arbitrary Ruby expression.
79
89
  return Time.now - tstart
80
90
  end
81
91
 
82
- =begin rdoc
83
-
84
- Call trace { foo(...) } to monitor the execution of the call to foo. Sets up
85
- a callback method which checks to see if a probe has been attached to a line
86
- via a call to Source.probe, and if so evaluates the expression associated with
87
- the probe.
88
-
89
- =end
90
-
92
+ # Monitor the execution of a block of code, which can be any Ruby expression enclosed in braces. Sets up
93
+ # a callback method which checks to see if a probe has been attached to a line in
94
+ # a method called by the block. See Source#probe for information about attaching
95
+ # probes. If a probe is attached, the expression associated with
96
+ # the probe is evaluated. The value returned from trace is the value returned
97
+ # by the block.
98
+ #
99
+ # See also RubyLabs#count.
100
+ #
101
+ # Example -- call a method named +brackets+ to print brackets around array
102
+ # elements as the arry is being sorted by a call to +isort+:
103
+ # >> Source.probe( "isort", 5, "puts brackets(a,i)" )
104
+ # => true
105
+ # >> trace { isort(cars) }
106
+ # mazda [ford bmw saab chrysler]
107
+ # ford mazda [bmw saab chrysler]
108
+ # bmw ford mazda [saab chrysler]
109
+ # ...
110
+ # => ["bmw", "chrysler", "ford", "mazda", "saab"]
111
+ #
112
+ # :call-seq:
113
+ # trace { f } => Object
114
+ #
115
+ #--
91
116
  # Debugging aid: put this line at the front of the trace_func:
92
117
  # p [event, file, line, id, binding, classname]
93
118
 
94
119
  def trace(&f)
95
- set_trace_func proc { |event, file, line, id, binding, classname|
120
+ set_trace_func proc { |event, file, line, id, binding, classname|
96
121
  if expr = Source.probing(file, id, line)
97
122
  eval(expr, binding) if expr != :count
98
123
  end
99
- }
124
+ }
100
125
  res = f.call
101
- set_trace_func nil
102
- res
126
+ set_trace_func nil
127
+ res
103
128
  end
104
129
 
105
- =begin rdoc
106
-
107
- A call to count { foo(...) } is similar to a call to trace, but instead of evaluating
108
- the expression associated with a probe it just counts the number of times the
109
- lines are executed and returns the count.
110
-
111
- Note: this version assumes there are two different kinds of probes. Probes monitored
112
- by the trace method are expressions that are evaluated when the probed expression is
113
- encountered, and probes monitored by the count method are :count tags.
114
-
115
- =end
130
+ # Monitor the execution of a block of code, which can be any Ruby expression enclosed in braces.
131
+ # If a counting probe (see Source#probe) is attached to any lines of code evaluated by the block
132
+ # this method will return the total number of times those lines are executed.
133
+ #
134
+ # See also RubyLabs#trace.
135
+ #
136
+ # Example -- count the number of times line 2 in the method named +less+ is executed
137
+ # during a call to +isort+:
138
+ # >> Source.probe("less", 2, :count)
139
+ # => true
140
+ # >> count { isort(cars) }
141
+ # => 8
142
+ #
143
+ # :call-seq:
144
+ # count { f } => Fixnum
145
+ #
116
146
 
117
147
  def count(&f)
118
148
  counter = 0
119
- set_trace_func proc { |event, file, line, id, binding, classname|
149
+ set_trace_func proc { |event, file, line, id, binding, classname|
120
150
  if expr = Source.probing(file, id, line)
121
151
  counter += 1 if expr == :count
122
152
  end
123
- }
153
+ }
124
154
  f.call
125
- set_trace_func nil
126
- return counter
155
+ set_trace_func nil
156
+ return counter
127
157
  end
128
158
 
129
- =begin rdoc
130
159
 
131
- === TestArray
160
+ =begin rdoc
132
161
 
133
- A TestArray is an array of random numbers to use in testing searching and sorting
134
- algorithms. Call TestArray.new(n) to make an array of n unique random numbers.
162
+ == TestArray
135
163
 
164
+ A TestArray is an array of random values that can be used to test searching and sorting
165
+ algorithms.
136
166
  A method named +random+ will return a random element to use as a search target.
137
- Call +a.random(:success)+ to get a value that is in the array +a+, or call
138
- +a.random(:fail)+ to get a number that is not in the array.
139
-
167
+ If +a+ is a TestArray object,
168
+ call <tt>a.random(:success)</tt> to get a value that is in the array +a+, or call
169
+ <tt>a.random(:fail)</tt> to get a number that is not in the array.
170
+
171
+ #--
172
+ The constructor uses a hash to create unique numbers -- it draws random numbers
173
+ and uses them as keys to insert into the hash, and returns when the hash has n
174
+ items. The hash is saved so it can be reused by a call to random(:fail) -- this
175
+ time draw random numbers until one is not a key in the hash. A lot of machinery
176
+ to keep around for very few calls, but it's efficient enough -- making an array
177
+ of 100K items takes less than a second.
178
+
179
+ An earlier version used a method named test_array to make a regular Array object
180
+ and augment it with the location method, but the singleton's methods were not passed
181
+ on to copies made by a call to sort:
182
+ >> x = test_array(3)
183
+ => [16, 13, 4]
184
+ >> x.sort.random(:fail)
185
+ NoMethodError: undefined method `random' for [4, 13, 16]:Array
186
+
140
187
  =end
141
188
 
142
- # The constructor uses a hash to create unique numbers -- it draws random numbers
143
- # and uses them as keys to insert into the hash, and returns when the hash has n
144
- # items. The hash is saved so it can be reused by a call to random(:fail) -- this
145
- # time draw random numbers until one is not a key in the hash. A lot of machinery
146
- # to keep around for very few calls, but it's efficient enough -- making an array
147
- # of 100K items takes less than a second.
148
-
149
- # An earlier version used a method named test_array to make a regular Array object
150
- # and augment it with the location method, but the singleton's methods were not passed
151
- # on to copies made by a call to sort:
152
- # >> x = test_array(3)
153
- # => [16, 13, 4]
154
- # >> x.sort.random(:fail)
155
- # NoMethodError: undefined method `random' for [4, 13, 16]:Array
156
-
157
189
  class TestArray < Array
158
190
 
159
- data = File.join(File.dirname(__FILE__), '..', 'data', 'arrays')
160
-
161
- @@sources = {
162
- :cars => "#{data}/cars.txt",
163
- :colors => "#{data}/colors.txt",
164
- :fruits => "#{data}/fruit.txt",
165
- :words => "#{data}/wordlist.txt",
166
- }
191
+ data = File.join(File.dirname(__FILE__), '..', 'data', 'arrays')
192
+
193
+ @@sources = {
194
+ :cars => "#{data}/cars.txt",
195
+ :colors => "#{data}/colors.txt",
196
+ :fruits => "#{data}/fruit.txt",
197
+ :words => "#{data}/wordlist.txt",
198
+ }
199
+
200
+ # Create a new TestArray of size +n+ containing items of the specified +type+.
201
+ # If a type is not supplied, create an array of integers. Types are identified
202
+ # by symbols, e.g. <tt>:cars</tt> tells the constructor to return an array of
203
+ # car names (see TestArray.sources). If a type is specified, the symbol <tt>:all</tt>
204
+ # can be given instead of an array size, in which case the constructor returns
205
+ # all items of that type.
206
+ #
207
+ # Examples:
208
+ # >> TestArray.new(5)
209
+ # => [3, 28, 48, 64, 4]
210
+ # >> TestArray.new(5, :cars)
211
+ # => ["lamborghini", "lincoln", "chrysler", "toyota", "rolls-royce"]
212
+ # >> TestArray.new(:all, :colors)
213
+ # => ["almond", "antique white", ... "yellow green"]
214
+ #
215
+ # :call-seq:
216
+ # TestArray.new(n, type) => Array
217
+ #
167
218
 
168
219
  def initialize(size, src = nil)
169
220
 
@@ -179,41 +230,59 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
179
230
  raise "TestArray: array size must be an integer or :all" unless size.class == Fixnum || size == :all
180
231
  end
181
232
 
182
- @h = Hash.new
183
-
184
- # if @max is defined make an array of integers, otherwise src defines the type of data;
185
- # size might be :all, in which case return the whole file, and set @all to true so random
186
- # doesn't try to make a random value not in the array.
187
-
188
- if @max
189
- while @h.size < size
190
- @h[ rand( @max ) ] = 1
191
- end
192
- else
193
- fn = @@sources[src] or raise "TestArray: undefined source: #{src}"
194
- @words = File.open(fn).readlines
195
- if size != :all
233
+ @h = Hash.new
234
+
235
+ # if @max is defined make an array of integers, otherwise src defines the type of data;
236
+ # size might be :all, in which case return the whole file, and set @all to true so random
237
+ # doesn't try to make a random value not in the array.
238
+
239
+ if @max
240
+ while @h.size < size
241
+ @h[ rand( @max ) ] = 1
242
+ end
243
+ else
244
+ fn = @@sources[src] or raise "TestArray: undefined source: #{src}"
245
+ @words = File.open(fn).readlines
246
+ if size != :all
196
247
  max = @words.length
197
- raise "TestArray: size must be less than #{max} for an array of #{src}" unless size < max
198
- while @h.size < size
199
- @h[ @words[ rand(max) ].chomp ] = 1
200
- end
201
- end
202
- end
203
-
204
- if size == :all
205
- self.concat @words.map { |s| s.chomp! }
206
- @all = true
207
- else
208
- self.concat @h.keys
248
+ raise "TestArray: size must be less than #{max} for an array of #{src}" unless size < max
249
+ while @h.size < size
250
+ @h[ @words[ rand(max) ].chomp ] = 1
251
+ end
252
+ end
253
+ end
254
+
255
+ if size == :all
256
+ self.concat @words.map { |s| s.chomp! }
257
+ @all = true
258
+ else
259
+ self.concat @h.keys
209
260
  for i in 0..length-2
210
261
  r = rand(length-i) + i # i <= r < length
211
262
  self[i],self[r] = self[r],self[i]
212
263
  end
213
- end
214
-
264
+ end
265
+
215
266
  end
216
267
 
268
+ # Return a value that is guaranteed to be in the array or not in the array,
269
+ # depending on the value of +outcome+. Pass <tt>:success</tt> to get a random
270
+ # value in the array, or pass <tt>:fail</tt> to get an item of the same type as the
271
+ # items in the array but which is not itself in the array. Call <tt>a.random(:fail)</tt>
272
+ # to get a value that will cause a search algorithm to do the maximum number of comparisons.
273
+ #
274
+ # Example:
275
+ # >> a = TestArray.new(10).sort
276
+ # => [13, 23, 24, 26, 47, 49, 86, 88, 92, 95]
277
+ # >> x = a.random(:fail)
278
+ # => 22
279
+ # >> search(a, x)
280
+ # => nil
281
+ #
282
+ # :call-seq:
283
+ # a.random(outcome) => Object
284
+ #
285
+
217
286
  def random(outcome)
218
287
  if outcome == :success
219
288
  return self[ rand(self.length) ]
@@ -231,108 +300,176 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
231
300
  return nil
232
301
  end
233
302
  end
234
-
235
- def TestArray.sources
236
- return @@sources.keys.sort { |a,b| a.to_s <=> b.to_s }
237
- end
238
-
239
- end # class TestArray
303
+
304
+ # Return a list of types of items that can be passed as arguments
305
+ # to <tt>TestArray.new</tt>
306
+ #
307
+ # :call-seq:
308
+ # TestArray.sources() => Array
309
+ #
310
+
311
+ def TestArray.sources
312
+ return @@sources.keys.sort { |a,b| a.to_s <=> b.to_s }
313
+ end
314
+
315
+ end # class TestArray
240
316
 
241
317
 
318
+ # Equivalent to calling <tt>TestArray.new</tt>.
319
+ #
320
+ # Examples:
321
+ # >> TestArray(5)
322
+ # => [65, 79, 60, 88, 30]
323
+ # >> TestArray(5, :cars)
324
+ # => ["mini", "opel", "chevrolet", "isuzu", "cadillac"]
325
+ #
326
+ # :call-seq:
327
+ # TestArray(n, type) => Array
328
+ #
329
+
242
330
  def TestArray(n, type = nil)
243
331
  TestArray.new(n, type)
244
332
  end
245
-
246
-
247
- =begin
248
- The Source module has methods that access the source code for a lab
249
- project. Since the outer module (RubyLabs) assigns SCRIPT_LINES__ the
250
- source code has already been read from the file -- these methods just have
251
- to look for the code in memory. The methods assume the code students will
252
- look at has been delimited by comments containing the strings :begin and :end.
333
+
334
+
335
+ =begin rdoc
336
+
337
+ == Source
338
+
339
+ The Source module provides access the source code for a lab
340
+ project. When IRB reads the modules from a file, the source is
341
+ saved in a global array named <tt>SCRIPT_LINES__</tt>. The methods
342
+ defined in this module scan the source code to look for tags that
343
+ mark the first and last lines of methods described in the book.
344
+
345
+ A method name can be passed either as a String or a Symbol. For
346
+ example, to print a listing of the +isort+ method a user can call
347
+ Source.listing("isort")
348
+ or
349
+ Source.listing(:isort)
350
+
351
+ #--
352
+ Code that will be accessed by methods in this module should be delimited
353
+ by :begin and :end tags. See the definition of isort in iterationlab.rb
354
+ for an example of how to name a method and its helper methods.
355
+
253
356
  =end
254
357
 
255
358
  module Source
256
359
 
257
360
  @@probes = Hash.new
258
- @@file = Hash.new
259
- @@size = Hash.new
260
- @@base = Hash.new
261
- @@helpers = Hash.new
262
- @@line = nil
263
-
264
- =begin
265
- Print the source code for method +name+.
266
- =end
361
+ @@file = Hash.new
362
+ @@size = Hash.new
363
+ @@base = Hash.new
364
+ @@helpers = Hash.new
365
+ @@line = nil
267
366
 
367
+ # Print a listing (source code along with line numbers) for method +name+.
368
+ #
369
+ # :call-seq:
370
+ # Source.listing(name)
371
+ #
372
+
268
373
  def Source.listing(name)
269
374
  begin
270
375
  id = Source.find(name)
271
- for i in @@base[id]..(@@base[id]+@@size[id]-1)
272
- line = SCRIPT_LINES__[@@file[id]][i-1].chomp
376
+ for i in @@base[id]..(@@base[id]+@@size[id]-1)
377
+ line = SCRIPT_LINES__[@@file[id]][i-1].chomp
273
378
  printf "%3d: %s\n", i - @@base[id] + 1, line.gsub(/\t/," ")
274
- end
379
+ end
275
380
  rescue Exception => e
276
381
  puts e
277
382
  end
278
383
  return true
279
384
  end
280
385
 
281
- =begin
282
- Write a copy of the source code for method +name+. If a file name is not
283
- specified, the output file name is the name of the method with ".rb" appended.
284
- Prompts the user before overwriting an existing file.
285
- =end
386
+ # Save a copy of the source code for method +name+ in a file. If a file name is not
387
+ # specified, the output file name is the name of the method with ".rb" appended.
388
+ # Prompts the user before overwriting an existing file.
389
+ #
390
+ # Example -- Write the source for the method +isort+ to "isort.rb":
391
+ # Source.checkout("isort")
392
+ #
393
+ # Example -- Write the source for +isort+ to "mysort.rb"
394
+ # Source.checkout("isort", "mysort.rb")
395
+ #
396
+ # :call-seq:
397
+ # Source.checkout(name)
398
+ #
286
399
 
287
400
  def Source.checkout(name, newfilename = nil)
288
401
  begin
289
402
  id = Source.find(name)
290
- if newfilename.nil?
291
- newfilename = id.to_s
292
- end
293
- if newfilename[-1] == ?? || newfilename[1] == ?!
294
- newfilename.chop!
295
- end
296
- if newfilename !~ /\.rb$/
297
- newfilename += ".rb"
298
- end
299
- if File.exists?(newfilename)
300
- print "Replace existing #{newfilename}? [yn] "
301
- if STDIN.gets[0] != ?y
302
- puts "File not written"
303
- return false
304
- end
305
- end
306
- File.open(newfilename, "w") do |f|
307
- f.puts "# #{name} method exported from #{File.basename(@@file[id])}"
308
- f.puts
403
+ if newfilename.nil?
404
+ newfilename = id.to_s
405
+ end
406
+ if newfilename[-1] == ?? || newfilename[1] == ?!
407
+ newfilename.chop!
408
+ end
409
+ if newfilename !~ /\.rb$/
410
+ newfilename += ".rb"
411
+ end
412
+ if File.exists?(newfilename)
413
+ print "Replace existing #{newfilename}? [yn] "
414
+ if STDIN.gets[0] != ?y
415
+ puts "File not written"
416
+ return false
417
+ end
418
+ end
419
+ File.open(newfilename, "w") do |f|
420
+ f.puts "# #{name} method exported from #{File.basename(@@file[id])}"
421
+ f.puts
309
422
  Source.print_source(f, id)
310
- @@helpers[id].each do |m|
311
- f.puts
312
- xid = Source.find(m)
423
+ @@helpers[id].each do |m|
424
+ f.puts
425
+ xid = Source.find(m)
313
426
  Source.print_source(f, xid)
314
- end
315
- end
427
+ end
428
+ end
316
429
  rescue Exception => e
317
430
  puts e
318
431
  end
319
- puts "Saved a copy of source in #{newfilename}"
320
- return true
432
+ puts "Saved a copy of source in #{newfilename}"
433
+ return true
321
434
  end
322
435
 
323
- def Source.print_source(f, id)
324
- for i in @@base[id]..(@@base[id]+@@size[id]-1)
325
- line = SCRIPT_LINES__[@@file[id]][i-1].chomp
436
+ # Helper method called from Source.checkout -- print the code for
437
+ # method +id+ in the file +f+
438
+
439
+ def Source.print_source(f, id) # :nodoc:
440
+ for i in @@base[id]..(@@base[id]+@@size[id]-1)
441
+ line = SCRIPT_LINES__[@@file[id]][i-1].chomp
326
442
  f.puts line.gsub(/\t/," ")
327
- end
443
+ end
328
444
  end
329
445
 
330
- =begin rdoc
331
- Attach a probe to a line in method +name+. The line can be specified
332
- by line number or a pattern. If a string is passed as a third parameter
333
- that string will be evaluated when the method is called from a block passed
334
- to trace, otherwise a count probe is created.
335
- =end
446
+ # Attach a software probe to a line in method +name+. The line can be specified
447
+ # by line number relative to the start of the method or a string the specifies a pattern. The third argument
448
+ # is either a string or the symbol <tt>:count</tt>.
449
+ # If the third argument is a string, it should be a Ruby expression that will be
450
+ # evaluated whenever the probe is activated via a call to +trace+ (see RubyLabs#trace).
451
+ # If the third argument is <tt>:count</tt>, a call to +count+ (see RubyLabs#count)
452
+ # will count the number of times the specified line is executed.
453
+ #
454
+ # Example: attach a probe to the +sieve+ method, so that when +sieve+ is called
455
+ # via +trace+ the statement "p worklist" will be evaluated whenever line 6 is executed:
456
+ # >> Source.probe("sieve", 6, "p worksheet")
457
+ # => true
458
+ #
459
+ # Example: attach a counting probe, so +count+ can record how many times line 6 is executed:
460
+ # >> Source.probe("sieve", 6, :count)
461
+ # => true
462
+ #
463
+ # If the second argument to Source.probe is a string, the probe is attached to every line that
464
+ # contains that string. For example, to attach a counting probe to every line in +merge+ that
465
+ # has a call to the << operator:
466
+ # >> Source.probe("merge", '<<', :count)
467
+ # => true
468
+ #
469
+ # :call-seq:
470
+ # Source.probe(name, line, spec)
471
+ #
472
+
336
473
 
337
474
  def Source.probe(name, spec, expr = :count)
338
475
  begin
@@ -345,24 +482,25 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
345
482
  puts e
346
483
  end
347
484
  return true
348
- end
349
-
350
- =begin rdoc
351
- Return probes (if any) attached to the specified file, method, and line number.
352
- Intended to be called from a trace func callback (which is why the file is one
353
- of the parameters).
354
- =end
485
+ end
486
+
487
+ # -- Method for internal use only --
488
+ # Return probes (if any) attached to the specified file, method, and line number.
489
+ # Intended to be called from a trace func callback (which is why the file is one
490
+ # of the parameters).
355
491
 
356
- def Source.probing(filename, method, line)
492
+ def Source.probing(filename, method, line) # :nodoc:
357
493
  return nil if line == @@line
358
494
  @@line = line
359
495
  return nil unless @@probes[method] && @@file[method] == filename
360
496
  return @@probes[method][line]
361
497
  end
362
-
363
- =begin rdoc
364
- Print the currently defined probes.
365
- =end
498
+
499
+ # Print a description of all the currently defined probes.
500
+ #
501
+ # :call-seq:
502
+ # Source.probes()
503
+ #
366
504
 
367
505
  def Source.probes
368
506
  @@probes.each do |name, plist|
@@ -371,12 +509,15 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
371
509
  printf "%s %2d: %s\n", name, n, exprs
372
510
  end
373
511
  end
374
- return true
512
+ return true
375
513
  end
376
514
 
377
- =begin rdoc
378
- Clear the probes on a designated method (or all methods)
379
- =end
515
+ # Remove all the probes on a designated method, or if no method name is passed,
516
+ # remove all probes from all methods.
517
+ #
518
+ # :call-seq:
519
+ # Source.clear(name)
520
+ #
380
521
 
381
522
  def Source.clear(name = nil)
382
523
  @@probes.each do |id, plist|
@@ -386,62 +527,56 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
386
527
  return true
387
528
  end
388
529
 
389
- =begin
390
- Internal use only -- locate the filename, starting line number, and length
391
- of a method named +name+ (+name+ can be a string or a symbol), record the
392
- information for any methods that need it. This information only needs to
393
- be found once, so it is recorded in a set of class variables. Revisit this
394
- decision if monitoring user-defined methods....
530
+ # Internal use only -- locate the filename, starting line number, and length
531
+ # of method +name+, record the
532
+ # information for any methods that need it. This information only needs to
533
+ # be found once, so it is recorded in a set of class variables. Revisit this
534
+ # decision if monitoring user-defined methods....
395
535
 
396
- TODO: save helper files listed on :begin line
397
- =end
398
-
399
- def Source.find(name)
536
+ def Source.find(name) # :nodoc:
400
537
  id = name.to_sym
401
538
  return id if @@file[id] # return if we looked for this source previously
402
539
 
403
540
  filename, base, size, helpers = nil, nil, nil, nil
404
541
 
405
- catch (:found) do
406
- SCRIPT_LINES__.each do |file, lines|
407
- line_num = 0
408
- lines.each do |s|
409
- line_num += 1
410
- if match = s.match(/:(begin|end)\s+(.*)/)
411
- verb = match[1]
412
- names = match[2].split.collect{|x| eval(x)}
413
- if names[0] == id
414
- if verb == "begin"
415
- filename = file
416
- base = line_num + 1
417
- helpers = names[1..-1]
418
- else
419
- size = line_num - base
420
- throw :found
421
- end
422
- end
423
- end
424
- end
425
- end
426
- end
427
-
428
- raise "Can't find method named '#{name}'" if size.nil?
429
-
430
- @@file[id] = filename
431
- @@size[id] = size
432
- @@base[id] = base
433
- @@probes[id] = Hash.new
434
- @@helpers[id] = helpers
435
- return id
542
+ catch (:found) do
543
+ SCRIPT_LINES__.each do |file, lines|
544
+ line_num = 0
545
+ lines.each do |s|
546
+ line_num += 1
547
+ if match = s.match(/:(begin|end)\s+(.*)/)
548
+ verb = match[1]
549
+ names = match[2].split.collect{|x| eval(x)}
550
+ if names[0] == id
551
+ if verb == "begin"
552
+ filename = file
553
+ base = line_num + 1
554
+ helpers = names[1..-1]
555
+ else
556
+ size = line_num - base
557
+ throw :found
558
+ end
559
+ end
560
+ end
561
+ end
562
+ end
563
+ end
564
+
565
+ raise "Can't find method named '#{name}'" if size.nil?
566
+
567
+ @@file[id] = filename
568
+ @@size[id] = size
569
+ @@base[id] = base
570
+ @@probes[id] = Hash.new
571
+ @@helpers[id] = helpers
572
+ return id
436
573
  end
437
574
 
438
- =begin rdoc
439
- Internal use only -- make an array of line numbers to use for probing method +name+.
440
- Argument can be a single line number, an array of line numbers, or a pattern. Checks
441
- to make sure line numbers are valid.
442
- =end
575
+ # Internal use only -- make an array of line numbers to use for probing method +name+.
576
+ # Argument can be a single line number, an array of line numbers, or a pattern. Checks
577
+ # to make sure line numbers are valid.
443
578
 
444
- def Source.lines(spec, id)
579
+ def Source.lines(spec, id) # :nodoc:
445
580
  if spec.class == Fixnum && Source.range_check(spec, id)
446
581
  return [spec]
447
582
  elsif spec.class == Array
@@ -453,27 +588,25 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
453
588
  return res
454
589
  elsif spec.class == String
455
590
  res = Array.new
456
- for i in @@base[id]..(@@base[id]+@@size[id]-1)
457
- line = SCRIPT_LINES__[@@file[id]][i-1].chomp
458
- res << i - @@base[id] + 1 if line.index(spec)
459
- end
460
- return res
591
+ for i in @@base[id]..(@@base[id]+@@size[id]-1)
592
+ line = SCRIPT_LINES__[@@file[id]][i-1].chomp
593
+ res << i - @@base[id] + 1 if line.index(spec)
594
+ end
595
+ return res
461
596
  else
462
597
  raise "invalid spec: '#{spec}' (must be an integer, array of integers, or a pattern)"
463
598
  end
464
599
  end
465
600
 
466
- def Source.range_check(n, id)
601
+ def Source.range_check(n, id) # :nodoc:
467
602
  max = @@size[id]
468
603
  raise "line number must be between 1 and #{max}" unless n >= 1 && n <= max
469
604
  return true
470
605
  end
471
606
 
472
- =begin rdoc
473
- Internal use only -- show info about method
474
- =end
607
+ # Internal use only -- show info about method to verify it's being found by Source.lines
475
608
 
476
- def Source.info(name)
609
+ def Source.info(name) # :nodoc:
477
610
  unless id = Source.find(name)
478
611
  puts "Can't find method named '#{name}'"
479
612
  return
@@ -488,26 +621,95 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
488
621
 
489
622
  end # Source
490
623
 
624
+
491
625
  =begin rdoc
492
- Module for interactive graphics.
626
+
627
+ == Canvas
628
+
629
+ The Canvas module defines a graphics window that can be used
630
+ for interactive visualizations. Classes in this module describe objects that are
631
+ drawn in the window; for example, objects of type Canvas::Circle are circles
632
+ on the canvas.
633
+
634
+ In the current implementation all drawing objects are derived from a base class
635
+ named TkObject, which provides an interface to the Tk library of ActiveTcl.
636
+ Instances of TkObject are proxies for the actual objects defined in Tk.
637
+ When the user calls a method that opens the RubyLabs Canvas, the method
638
+ initializes a pipe to a new process running the Tcl shell (wish). If a TkObject is
639
+ updated, it sends a Tcl command over the pipe, and the wish process updates
640
+ the window.
641
+
493
642
  =end
494
643
 
495
644
  module Canvas
496
645
 
646
+ =begin rdoc
647
+
648
+ == TkObject
649
+
650
+ Base class of all objects defined for RubyLabs::Canvas. Objects derived from
651
+ this class are proxies for Tk objects in a wish shell, opened when the canvas
652
+ is initialized.
653
+
654
+ Applications should not try to instantiate a TkObject directly,
655
+ but instead should call a constructor of one of the derived classes. For
656
+ example, to draw a circle on the canvas:
657
+
658
+ c = Canvas::Circle.new(x, y, r)
659
+
660
+ Public instance methods defined here are inherited by all objects derived from TkObject,
661
+ and can be used to manipulate the object. For example, to change the color of the circle:
662
+
663
+ c.fill = 'green'
664
+
665
+ The current implementation is very sparse: the only operations defined are the
666
+ ones needs for the visualizations described in the textbook, which are just
667
+ the basic methods that create an object or move it around on the screen.
668
+
669
+ In addition to the usual attributes, these objects have a "pen point", which is
670
+ the location of an imaginary pen relative to the object's base
671
+ coordinate. The pen point is used by methods that draw a track as an object is
672
+ moved (e.g. see the methods that control the motion of the robot in SphereLab)
673
+
674
+ =end
675
+
497
676
  class TkObject
498
677
 
499
678
  attr_accessor :name, :coords, :penpoint
500
679
 
680
+ # Initialization: set the object ID to 0 and save the file descriptor
681
+ # for the connection to the wish shell.
682
+ #
683
+ # :call-seq:
684
+ # TkObject.reset(p)
685
+ #
686
+
501
687
  def TkObject.reset(p)
502
688
  @@pipe = p
503
689
  @@id = 0
504
690
  end
505
691
 
692
+ # Return a unique ID number for a new proxy object.
693
+ #
694
+ # :call-seq:
695
+ # TkObject.nextId()
696
+ #
697
+
506
698
  def TkObject.nextId
507
699
  @@id += 1
508
700
  return "obj" + @@id.to_s
509
701
  end
510
702
 
703
+ # Translate a Ruby hash +h+ into a Tk option string. Example:
704
+ # >> Canvas::TkObject.options( { :outline => :black, :fill => :red } )
705
+ # => "-fill red -outline black"
706
+ # Note the ordering of the options in the Tk string may not be the same as
707
+ # the order they are listed when the hash object is created.
708
+ #
709
+ # :call-seq:
710
+ # TkObject.options(h) => String
711
+ #
712
+
511
713
  def TkObject.options(h)
512
714
  a = []
513
715
  h.each do |k,v|
@@ -516,23 +718,64 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
516
718
  return a.join(" ")
517
719
  end
518
720
 
721
+ # Bring an object to the foreground (on top of all other objects on the canvas).
722
+ #
723
+ # :call-seq:
724
+ # x.raise()
725
+ #
726
+
519
727
  def raise
520
728
  @@pipe.puts ".canvas raise $#{@name}"
521
729
  end
522
730
 
731
+ # Put an object in the background (below all other objects on the canvas).
732
+ #
733
+ # :call-seq:
734
+ # x.lower()
735
+ #
736
+
523
737
  def lower
524
738
  @@pipe.puts ".canvas lower $#{@name}"
525
739
  end
526
740
 
741
+ # Remove an object from the canvas.
742
+ #
743
+ # :call-seq:
744
+ # x.erase()
745
+ #
746
+
527
747
  def erase
528
748
  @@pipe.puts ".canvas delete $#{@name}"
529
749
  end
530
750
 
751
+ # Assign new coordinates for an object. Can be used to move or resize an
752
+ # object, but applications should use the more abstract interface defined by class methods in Canvas
753
+ # (e.g. Canvas#move).
754
+ #
755
+ # Example: if an object x is a proxy for a rectangle
756
+ # that has an upper left corner at (10,10) and
757
+ # a lower right corner at (20,50), this call will move it down and to the right
758
+ # by 10 pixels:
759
+ # >> x.coords = [20, 20, 30, 60]
760
+ # => [20, 20, 30, 60]
761
+ #
762
+ # :call-seq:
763
+ # x.coords = [...]
764
+ #
765
+
531
766
  def coords=(a)
532
767
  @coords = a
533
768
  @@pipe.puts ".canvas coords $#{@name} #{a.join(' ')}"
534
769
  end
535
770
 
771
+ # Set the fill color for an object. Example:
772
+ # >> x.fill = "green"
773
+ # => "green"
774
+ #
775
+ # :call-seq:
776
+ # x.fill = String
777
+ #
778
+
536
779
  def fill=(x)
537
780
  @@pipe.puts ".canvas itemconfigure $#{name} -fill #{x}"
538
781
  end
@@ -540,10 +783,27 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
540
783
  end
541
784
 
542
785
  =begin rdoc
543
- Draw a line from (x0,y0) to (x1,y1)
786
+
787
+ == Line
788
+
789
+ A Line object is a proxy for a line segment defined by a pair of (x,y) coordinates.
790
+
791
+ There are no instance methods for Lines beyond those defined in the TkObject base class.
544
792
  =end
545
793
 
546
794
  class Line < TkObject
795
+
796
+ # Create a new line segment that runs from (x0,y0) to (x1,y1). Attributes
797
+ # of the line can be passed in a hash object at the end of the argument list.
798
+ #
799
+ # Example -- create a gray line one pixel wide from (0,0) to (100,100):
800
+ # >> z = Line.new(0, 0, 100, 100, :fill => :gray, :width => 1)
801
+ # => #<RubyLabs::Canvas::Line:...>
802
+ #
803
+ # :call-seq:
804
+ # x = Line.new(x0, y0, x1, y1, args = {})
805
+ #
806
+
547
807
  def initialize(x0, y0, x1, y1, args = {})
548
808
  raise "No canvas" unless @@pipe
549
809
  @name = TkObject.nextId
@@ -553,6 +813,15 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
553
813
  @@pipe.puts cmnd
554
814
  end
555
815
 
816
+ # Erase all Line objects that are tagged with the ID +tag+. Tags are optional
817
+ # arguments defined by passing <tt>-tag x</tt> to <tt>Line.new</tt> (see the
818
+ # visualization methods in tsplab.rb, which uses tags to erase all
819
+ # edges from a graph in a single call).
820
+ #
821
+ # :call-seq:
822
+ # Line.erase_all(tag)
823
+ #
824
+
556
825
  def Line.erase_all(tag)
557
826
  @@pipe.puts ".canvas delete withtag #{tag}"
558
827
  end
@@ -560,10 +829,27 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
560
829
  end
561
830
 
562
831
  =begin rdoc
563
- Create a circle with center at (x,y) and radius r.
832
+
833
+ == Circle
834
+
835
+ A Circle object is a proxy for a Tk oval.
836
+
837
+ There are no instance methods for Circles beyond those defined in the TkObject base class.
564
838
  =end
565
839
 
566
840
  class Circle < TkObject
841
+
842
+ # Create a new circle with center at (x,y) and radius r. Attributes
843
+ # of the circle can be passed in a hash object at the end of the argument list.
844
+ #
845
+ # Example -- create a green cicle with radius 5 at location (20,20):
846
+ # >> x = Canvas::Circle.new(20, 20, 5, :fill => "green")
847
+ # => #<RubyLabs::Canvas::Circle:...>
848
+ #
849
+ # :call-seq:
850
+ # x = Circle.new(x, y, r, args = {})
851
+ #
852
+
567
853
  def initialize(x, y, r, args = {})
568
854
  raise "No canvas" unless @@pipe
569
855
  @name = TkObject.nextId
@@ -575,10 +861,29 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
575
861
  end
576
862
 
577
863
  =begin rdoc
578
- Create a rectangle with upper left at (x0,y0) and lower right at (x1,y1).
864
+
865
+ == Rectangle
866
+
867
+ A Rectangle object is a proxy for a Tk rectangle.
868
+
869
+ There are no instance methods for Rectangles beyond those defined in the TkObject base class.
579
870
  =end
580
871
 
581
872
  class Rectangle < TkObject
873
+
874
+ # Create a new rectangle with its upper left corner at (x0,y0)
875
+ # lower right corner at (x1,y1). Attributes
876
+ # of the rectangle can be passed in a hash object at the end of the argument list.
877
+ #
878
+ # Example -- create a 20x20 square with upper left corner at (10,10), with a dark green
879
+ # outline and yellow interior:
880
+ # >> x = Canvas::Rectangle.new(10, 10, 30, 30, :fill => "yellow", :outline => "darkgreen")
881
+ # => #<RubyLabs::Canvas::Rectangle:...>
882
+ #
883
+ # :call-seq:
884
+ # x = Rectangle.new(x0, y0, x1, y1, args = {})
885
+ #
886
+
582
887
  def initialize(x0, y0, x1, y1, args = {})
583
888
  raise "No canvas" unless @@pipe
584
889
  @name = TkObject.nextId
@@ -590,12 +895,30 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
590
895
  end
591
896
 
592
897
  =begin rdoc
593
- Create a polygon with vertices defined by array a. The array can be a flat
594
- list of x's and y's (e.g. [100,100,100,200,200,100]) or a list of (x,y)
595
- pairs (e.g. [[100,100], [100,200], [200,100]]).
898
+
899
+ == Polygon
900
+
901
+ A Polygon object is a proxy for a Tk polygon.
902
+
903
+ In addition to the methods defined in the TkObject base class, a method named +rotate+
904
+ will rotate a Polygon by a specified angle.
596
905
  =end
597
906
 
598
907
  class Polygon < TkObject
908
+
909
+ # Create a new polygon. The first argument is an array of vertex coordinates. If
910
+ # the polygon has +n+ vertices, the argument should be a flat array of 2*n points.
911
+ # Attributes of the polygon can be passed in a hash object at the end of the argument list.
912
+ #
913
+ # Example -- create a small triangle in the upper left corner of the canvas, with a black
914
+ # outline and green interior:
915
+ # >> x = Canvas::Polygon.new([10,10,20,20,30,10], :fill => "green", :outline => "black")
916
+ # => #<RubyLabs::Canvas::Polygon ... >
917
+ #
918
+ # :call-seq:
919
+ # x = Polygon.new(a, args = {})
920
+ #
921
+
599
922
  def initialize(a, args = {})
600
923
  raise "No canvas" unless @@pipe
601
924
  @name = TkObject.nextId
@@ -604,14 +927,56 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
604
927
  cmnd = "set #{@name} [.canvas create polygon #{@coords.join(" ")} #{TkObject.options(args)}]"
605
928
  @@pipe.puts cmnd
606
929
  end
930
+
931
+ # Rotate the polygon by an angle +theta+ (expressed in degrees). The object is
932
+ # rotated about the point defined by the first pair of (x,y) coordinates. The
933
+ # return value is an array with the new coordinates.
934
+ #
935
+ # Example: Suppose +x+ is a square with coordinates (10,10), (20,10), (20,20), and (10,20).
936
+ # This call will rotate it clockwise by 45 degrees and return the new vertices:
937
+ # >> x.rotate(45)
938
+ # => [10.0, 10.0, 17.07, 17.07, 10.0, 24.14, 2.93, 17.07]
939
+
940
+ def rotate(theta)
941
+ theta = Canvas.radians(theta)
942
+ a = self.coords
943
+ x0 = a[0]
944
+ y0 = a[1]
945
+ (0...a.length).step(2) do |i|
946
+ x = a[i] - x0
947
+ y = a[i+1] - y0
948
+ a[i] = x0 + x * cos(theta) - y * sin(theta)
949
+ a[i+1] = y0 + x * sin(theta) + y * cos(theta)
950
+ end
951
+ self.coords = a
952
+ return a
953
+ end
607
954
  end
608
955
 
609
956
  =begin rdoc
610
- Add text at (x, y). Sets the anchor point to northwest unless it is passed
611
- as an argument.
957
+
958
+ == Text
959
+
960
+ A Text object is a string displayed on the RubyLabs canvas.
961
+
612
962
  =end
613
963
 
614
964
  class Text < TkObject
965
+
966
+ # Display string +s+ at location (+x+,+y+) on the canvas. By default the coordinates
967
+ # specify the location of the top left of the first character in the string (in Tk
968
+ # terminology, the anchor point for the text is "northwest"). A different anchor
969
+ # position and other attributes of the text can be passed in a hash object at the
970
+ # end of the argument list.
971
+ #
972
+ # Example -- display a message in dark green letters:
973
+ # >> x = Canvas::Text.new("hello", 50, 50, :fill => 'darkgreen')
974
+ # => #<RubyLabs::Canvas::Text: ... >
975
+ #
976
+ # :call-seq:
977
+ # x = Text.new(s, x, y, args = {})
978
+ #
979
+
615
980
  def initialize(s, x, y, args = {})
616
981
  raise "No canvas" unless @@pipe
617
982
  @name = TkObject.nextId
@@ -623,15 +988,48 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
623
988
  @@pipe.puts cmnd
624
989
  end
625
990
 
991
+ # Replace the string displayed by a text object. Example: erase the current string
992
+ # for object +x+ and replace it with "hello, world":
993
+ # >> x.update("hello, world")
994
+ # => nil
995
+ # The new string is displayed at the same location and with the same attributes
996
+ # as the old string.
997
+
626
998
  def update(s)
627
999
  @@pipe.puts ".canvas itemconfigure $#{name} -text \"#{s}\""
628
1000
  end
629
1001
  end
630
1002
 
1003
+ =begin rdoc
1004
+
1005
+ == Font
1006
+
1007
+ A Font object defines the appearance of strings displayed by a Text object. A Font
1008
+ object is a proxy for a Tk font object. The proxy is never used by an application;
1009
+ instead, when text is created, it is passed the name of the Tk font (see the example in
1010
+ the description of Font.new).
1011
+
1012
+ =end
1013
+
631
1014
  class Font < TkObject
632
1015
 
633
1016
  @@fonts = []
634
1017
 
1018
+ # Create a font named +name+ with attributes defined in +args+. When creating
1019
+ # a new Text object, +name+ can be passed as an argument to Text.new so the
1020
+ # string will be displayed in the specified font.
1021
+ #
1022
+ # Example -- create a new font for displaying fixed width text, and then display
1023
+ # a message using this font:
1024
+ # >> f = Canvas::Font.new('code', :family => 'Courier', :size => 20)
1025
+ # => #<RubyLabs::Canvas::Font:0x1012587b0 @name="code">
1026
+ # >> t = Canvas::Text.new("Go Ducks!", 100, 100, :font => 'code')
1027
+ # => #<RubyLabs::Canvas::Text:0x101249300 @coords=[100, 100], @name="obj3", @penpoint=nil>
1028
+ #
1029
+ # :call-seq:
1030
+ # x = Font.new(name,args)
1031
+ #
1032
+
635
1033
  def initialize(name, args)
636
1034
  @name = name
637
1035
  if @@fonts.index(name).nil?
@@ -643,22 +1041,32 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
643
1041
 
644
1042
  end
645
1043
 
646
- =begin rdoc
647
- Initialize a drawing canvas. This version uses Tk to draw objects used during
648
- a visualization. If it's not already initialized, open a connection to a wish
649
- shell and configure the window.
650
- =end
651
-
652
- =begin
653
- TODO Read the path to wish from a configuration file, so users can alter it
654
- TODO there are probably other configuration options that can go there, too
655
- =end
1044
+ # Initialize a drawing canvas with the specified width and height. If this is the first
1045
+ # call in an IRB session, open a connection to a wish shell and send it Tcl commands to
1046
+ # make a window with a single widget, a canvas centered in the middle. The +title+ argument
1047
+ # passed to Canvas.init becomes part of the new window.
1048
+ #
1049
+ # If this is not the first call to Canvas.init, the existing Tk canvas is resized according
1050
+ # to the new width and height arguments and the window is renamed using the new name.
1051
+ #
1052
+ # An optional fourth argument can be the symbol <tt>:debug</tt>, in which case the return
1053
+ # value is the Ruby Pipe object used to communicate with Tk. The Pipe can be useful for
1054
+ # developing new TkObject objects, since it can be used to see how Tcl commands are
1055
+ # processed. Example:
1056
+ #
1057
+ # >> p = Canvas.init(200, 200, "Test", :debug)
1058
+ # => #<IO:0x1012a1cd0>
1059
+ # >> p.puts ".canvas create text 30 30 -text hello"
1060
+ # => nil
1061
+ #
1062
+ # :call-seq:
1063
+ # Canvas.init(width, height, title, opts = nil)
1064
+ #
1065
+ #--
1066
+ # TODO Read the path to wish from a configuration file, so users can alter it
1067
+ # TODO there are probably other configuration options that can go there, too
1068
+ # TODO use popen3 on OSX, capture stderr
656
1069
 
657
- @@tkpipe = nil
658
- @@title = ""
659
- @@height = 0
660
- @@width = 0
661
-
662
1070
  def Canvas.init(width, height, title, *opts)
663
1071
 
664
1072
  @@title = "RubyLabs::#{title}"
@@ -669,7 +1077,7 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
669
1077
  if @@tkpipe.nil?
670
1078
  if Canvas.OS == "Windows"
671
1079
  @@tkpipe = IO.popen("wish", "w")
672
- elsif Canvas.OS == "Linux"
1080
+ elsif Canvas.OS == "Linux"
673
1081
  @@tkpipe = IO.popen("/opt/ActiveTcl-8.5/bin/wish", "w")
674
1082
  else
675
1083
  @@tkpipe = IO.popen("/usr/local/bin/wish", "w")
@@ -687,9 +1095,16 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
687
1095
  @@tkpipe.puts "wm geometry . #{width+2*pad}x#{height+2*pad}+30+50"
688
1096
  @@tkpipe.puts "wm title . #{@@title}"
689
1097
 
690
- return opts[0] == :debug ? @@tkpipe : true
1098
+ return opts[0] == :debug ? @@tkpipe : true
691
1099
  end
692
1100
 
1101
+ # Send an +exit+ command to the wish shell, which closes the drawing window and
1102
+ # terminates the shell.
1103
+ #
1104
+ # :call-seq:
1105
+ # Canvas.close()
1106
+ #
1107
+
693
1108
  def Canvas.close
694
1109
  if @@tkpipe
695
1110
  @@tkpipe.puts "exit"
@@ -698,22 +1113,37 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
698
1113
  end
699
1114
  end
700
1115
 
701
- def Canvas.flush
702
- @@tkpipe.flush
703
- end
1116
+ # Return the current width of the canvas.
1117
+ #
1118
+ # :call-seq:
1119
+ # Canvas.width() => Fixnum
1120
+ #
704
1121
 
705
1122
  def Canvas.width
706
1123
  @@width
707
1124
  end
708
1125
 
1126
+ # Return the current height of the canvas.
1127
+ #
1128
+ # :call-seq:
1129
+ # Canvas.height() => Fixnum
1130
+ #
1131
+
709
1132
  def Canvas.height
710
1133
  @@height
711
1134
  end
712
1135
 
1136
+ # Return a reference to the Pipe object used to communicate with the wish shell.
1137
+ #
1138
+ # :call-seq:
1139
+ # Canvas.pipe() => IO
1140
+ #
1141
+
713
1142
  def Canvas.pipe
714
1143
  @@tkpipe
715
1144
  end
716
1145
 
1146
+ #--
717
1147
  # Idea for the future, abandoned for now: allow applications to define a coordinate
718
1148
  # system, e.g. cartesian with the origin in the middle of the canvas, and map coords
719
1149
  # pass to line, rectangle, etc from user-specified coordinates to Tk coordinates.
@@ -737,14 +1167,22 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
737
1167
  # a[i], a[i+1] = @@map.call( a[i], a[i+1] )
738
1168
  # end
739
1169
  # end
1170
+ #++
740
1171
 
741
- # Figure out what type of system we're on.
1172
+ # Return a string that uses the <tt>RUBY_PLATFORM</tt> environment variable to
1173
+ # determine the host operating system type. Possible return values are "Mac OS X",
1174
+ # "Linux", or "Windows".
1175
+ #
1176
+ # :call-seq:
1177
+ # Canvas.OS() => String
1178
+ #
1179
+ #--
742
1180
  # TODO: find out what the platform string is for the legacy windows one-click installer;
743
1181
  # the new installer uses the "mingw" compiler.
744
1182
 
745
1183
  def Canvas.OS
746
1184
  if RUBY_PLATFORM =~ %r{darwin}
747
- return "OS X"
1185
+ return "Mac OS X"
748
1186
  elsif RUBY_PLATFORM =~ %r{linux}
749
1187
  return "Linux"
750
1188
  elsif RUBY_PLATFORM =~ %r{mingw}
@@ -754,9 +1192,20 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
754
1192
  end
755
1193
  end
756
1194
 
757
- =begin rdoc
758
- Move an object by an amount dx, dy
759
- =end
1195
+ # Move an object by a distance +dx+ vertically and a distance +dy+ horizontally.
1196
+ # If the fourth argument is the symbol <tt>:track</tt> a line segment is drawn
1197
+ # connecting the pen position of the object's previous location with the
1198
+ # pen position of its new location. The return value is an array with the new
1199
+ # coordinates.
1200
+ #
1201
+ # Example: Suppose +x+ is a Polygon with coordinates (10,10), (20,20), and (30,10).
1202
+ # This call moves it down and to the right by 10 pixels and returns the new location:
1203
+ # >> Canvas.move(x, 10, 10)
1204
+ # => [20, 20, 30, 30, 40, 20]
1205
+ #
1206
+ # :call-seq:
1207
+ # Canvas.move(obj, dx, dy, track = nil)
1208
+ #
760
1209
 
761
1210
  def Canvas.move(obj, dx, dy, option = nil)
762
1211
  a = obj.coords
@@ -778,91 +1227,116 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
778
1227
  return a
779
1228
  end
780
1229
 
781
- =begin rdoc
782
- Attach a "pen point" to an object by adding new accessor methods named penx and peny,
783
- and save the object in a local list so it can be erased by Canvas.clear
784
- =end
785
-
786
- # def Canvas.makeObject(obj, xoff, yoff)
787
- # class <<obj
788
- # attr_accessor :penx, :peny
789
- # end
790
- # obj.penx = xoff
791
- # obj.peny = yoff
792
- # @@objects << obj
793
- # return obj
794
- # end
795
-
796
- =begin rdoc
797
- Rotate an object by an angle theta (expressed in degrees). The object is
798
- rotated about the point defined by the first pair of (x,y) coordinates.
799
- =end
800
-
801
- def Canvas.rotate(obj, theta)
802
- theta = Canvas.radians(theta)
803
- a = obj.coords
804
- x0 = a[0]
805
- y0 = a[1]
806
- (0...a.length).step(2) do |i|
807
- x = a[i] - x0
808
- y = a[i+1] - y0
809
- a[i] = x0 + x * cos(theta) - y * sin(theta)
810
- a[i+1] = y0 + x * sin(theta) + y * cos(theta)
811
- end
812
- obj.coords = a
813
- return a
814
- end
815
-
1230
+ # Convert an angle from degrees to radians. Example:
1231
+ # >> Math::PI / 4
1232
+ # => 0.785398163397448
1233
+ # >> Canvas.radians(45)
1234
+ # => 0.785398163397448
1235
+ #
1236
+ # :call-seq:
1237
+ # Canvas.radians(deg) => Float
1238
+ #
1239
+
816
1240
  def Canvas.radians(deg)
817
1241
  deg * Math::PI / 180
818
1242
  end
819
1243
 
1244
+ # Convert an angle from radians to degrees. Example:
1245
+ # >> Canvas.degrees( Math::PI / 2 )
1246
+ # => 90.0
1247
+ #
1248
+ # :call-seq:
1249
+ # Canvas.degrees(rad) => Float
1250
+ #
1251
+
820
1252
  def Canvas.degrees(rad)
821
1253
  180 * rad / Math::PI
822
1254
  end
823
1255
 
824
- =begin rdoc
825
- # Make a range of colors starting from first and going to last in n steps.
826
- # First and last are expected to be 3-tuples of integer RGB values. The
827
- # result is an array that starts with first, has n-1 intermediate colors,
828
- # and ends with last. Example:
829
- # makePalette( [255,0,0], [0,0,0], 10)
830
- # makes 11 colors starting with red and ending with black.
831
- =end
1256
+ # Make a range of colors starting from +first+ and going to +last+ in +n+ steps.
1257
+ # Color arguments should be be 3-tuples of integer RGB values. The
1258
+ # result is an array that starts with +first+, has +n+-1 intermediate colors,
1259
+ # and ends with +last+.
1260
+ #
1261
+ # Example:
1262
+ # >> Canvas.palette( [255,0,0], [0,0,0], 10)
1263
+ # => ["#FF0000", "#E60000", "#CD0000", ... "#1E0000", "#000000"]
1264
+ # The return value is an array of 11 colors starting with red and ending with black.
1265
+ #
1266
+ # :call-seq:
1267
+ # Canvas.palette(first, last, n) => Array
1268
+ #
832
1269
 
833
1270
  def Canvas.palette(first, last, n)
834
- d = Array.new(3)
835
- 3.times { |i| d[i] = (first[i] - last[i]) / n }
836
- a = [first]
837
- (n-1).times do |i|
838
- a << a.last.clone
839
- 3.times { |j| a.last[j] -= d[j] }
840
- end
841
- a << last
842
- a.map { |c| sprintf("#%02X%02X%02X",c[0],c[1],c[2]) }
843
- end
844
-
1271
+ d = Array.new(3)
1272
+ 3.times { |i| d[i] = (first[i] - last[i]) / n }
1273
+ a = [first]
1274
+ (n-1).times do |i|
1275
+ a << a.last.clone
1276
+ 3.times { |j| a.last[j] -= d[j] }
1277
+ end
1278
+ a << last
1279
+ a.map { |c| sprintf("#%02X%02X%02X",c[0],c[1],c[2]) }
1280
+ end
1281
+
1282
+ @@tkpipe = nil
1283
+ @@title = ""
1284
+ @@height = 0
1285
+ @@width = 0
1286
+
845
1287
  end # Canvas
846
1288
 
847
1289
  =begin rdoc
848
- Priority queue class -- simple wrapper for an array that can only be updated via
849
- +<<+ and +shift+ operations. Also responds to +length+, +first+, and +last+,
850
- and allows direct access to an item through an index expression, but does not allow
851
- assignment via an index or any other array operation.
852
- The +<<+ method checks to make sure an object is comparable (responds to <) before
853
- adding it to the queue.
854
-
855
- If a program that uses a priority queue adds an instance variable named @on_canvas
856
- the shift and << methods will call a method named update so drawings that show
857
- a queue can be updated.
1290
+
1291
+ == PriorityQueue
1292
+
1293
+ A PriorityQueue is a collection of objects that is always maintained in order.
1294
+ Any kind of object can be in the queue as long as it can be compared with the < operator.
1295
+ More precisely, if an object +x+ responds to the < operator, and +x+ < +y+ is
1296
+ defined for every object +y+ already in the queue, then +x+ can be inserted.
1297
+
1298
+ The methods that insert and remove items check to see if an instance variable
1299
+ named <tt>@on_canvas</tt> is +true+. If so, a method named +update+ is called.
1300
+ +update+ is not defined here, but is added to the class by modules that
1301
+ use the queue during visualizations (see the definition of the priority queue
1302
+ used in the Huffman tree project in bitlab.rb for an example).
1303
+
1304
+ Several methods of the Array class are defined dynamically in the PriorityQueue
1305
+ class when this module
1306
+ is loaded. These methods have the same meaning for PriorityQueue objects as
1307
+ they do for Array objects:
1308
+
1309
+ +length+:: return the number of items in the queue
1310
+ +first+:: return a reference to the first item in the queue
1311
+ +last+:: return a reference to the last item in the queue
1312
+ +to_s+:: generate a String representation of the queue (by calling Array#to_s)
1313
+ +inspect+:: generate a String representation of the queue (by calling Array#inspect)
1314
+ +clear+:: remove all items from the queue
1315
+ <tt>empty?</tt>:: return +true+ if the queue has no items
1316
+
858
1317
  =end
859
1318
 
860
1319
  class PriorityQueue
861
1320
 
1321
+ # Create a new, initially empty, priority queue.
1322
+
862
1323
  def initialize
863
1324
  @q = Array.new
864
1325
  end
865
-
1326
+
1327
+ # Insert an item into the queue. The item's location is determined by how
1328
+ # it compares with other items, according to the < operator. Specifically,
1329
+ # when item +x+ is added to a queue, it it put before the first item +y+
1330
+ # where +x+ < +y+.
1331
+ #
1332
+ # Example: suppose object +q+ has three strings:
1333
+ # >> q
1334
+ # => ["Au", "He", "O"]
1335
+ # This expression adds the string "Fe" to the queue:
1336
+ # >> q << "Fe"
1337
+ # => ["Au", "Fe", "He", "O"]
1338
+ # The new string went into the second position because "Fe" < "Au" is false but "Fe" < "He" is true.
1339
+
866
1340
  def <<(obj)
867
1341
  raise "Object cannot be inserted into priority queue" unless obj.respond_to?(:<)
868
1342
  i = 0
@@ -875,57 +1349,129 @@ Call +a.random(:success)+ to get a value that is in the array +a+, or call
875
1349
  return @q
876
1350
  end
877
1351
 
1352
+ # Remove the first item from the queue, returning a reference to that item.
1353
+ #
1354
+ # Example: suppose object +q+ has three strings:
1355
+ # >> q
1356
+ # => ["Au", "He", "O"]
1357
+ # Then a call to shift removes the string "Au" and leaves the remaining items in order in the queue:
1358
+ # >> x = q.shift
1359
+ # => "Au"
1360
+ # >> q
1361
+ # => ["He", "O"]
1362
+ #
1363
+
878
1364
  def shift
879
1365
  res = @q.shift
880
1366
  update(:shift, res) if @on_canvas
881
1367
  return res
882
1368
  end
883
-
884
- %w{length first last to_s inspect clear empty?}.each do |name|
885
- eval "def #{name}() @q.#{name} end"
886
- end
1369
+
1370
+ # Return the item at location +i+ in the queue. *Note*: unlike Array objects, an
1371
+ # index expression cannot be used on the left side of an assignment. If
1372
+ # +q+ is a PriorityQueue object,
1373
+ # x = q[i]
1374
+ # is valid, but
1375
+ # q[i] = x
1376
+ # is undefined.
887
1377
 
888
1378
  def [](i)
889
1379
  @q[i]
890
1380
  end
891
1381
 
1382
+ # Call +f+ for every item in the queue, and return an array with
1383
+ # the result of each call (essentially the same as the +map+ operation
1384
+ # defined for Ruby's Enumeration interface).
1385
+ #
1386
+ # Example:
1387
+ # >> q
1388
+ # => ["avocado", "boysenberry", "clementine", "elderberry", "loquat"]
1389
+ # >> q.collect { |x| x.length }
1390
+ # => [7, 11, 10, 10, 6]
1391
+
892
1392
  def collect(&f)
893
1393
  @q.map { |x| f.call(x) }
894
1394
  end
895
1395
 
1396
+ # Evaluate block +b+ for every item in the queue (equivalent to <tt>Array#each</tt>)
1397
+ #
1398
+ # Example:
1399
+ # >> q
1400
+ # => ["Au", "He", "O"]
1401
+ # >> q.each { |x| puts x }
1402
+ # Ar
1403
+ # He
1404
+ # O
1405
+ # => ["Au", "He", "O"]
1406
+
896
1407
  def each(&b)
897
1408
  @q.each &b
898
1409
  end
899
1410
 
1411
+ # Evaluate block +b+ for every item in the queue, also passing the item's
1412
+ # location in the queue to the block (equivalent to <tt>Array#each_with_index</tt>)
1413
+ #
1414
+ # Example:
1415
+ # >> q
1416
+ # => ["Au", "He", "O"]
1417
+ # >> q.each_with_index { |x,i| puts "#{i}: #{x}" }
1418
+ # 0: Ar
1419
+ # 1: He
1420
+ # 2: O
1421
+ # => ["Au", "He", "O"]
1422
+
900
1423
  def each_with_index(&b)
901
1424
  @q.each_with_index &b
902
1425
  end
903
1426
 
1427
+ %w{length first last to_s inspect clear empty?}.each do |name|
1428
+ eval "def #{name}() @q.#{name} end"
1429
+ end
1430
+
904
1431
  end # PriorityQueue
905
1432
 
906
1433
  end # RubyLabs
907
1434
 
908
- class Fixnum
909
-
910
1435
  =begin rdoc
911
1436
 
912
- An 'ord' method for the Fixnum class that maps ASCII codes for letters
913
- to numbers between 0 and 25.
1437
+ == Fixnum
914
1438
 
915
- <b>NOTE:</b> +ord+ is built in to Ruby 1.9, and will be sligthly different; for
916
- characters (1-letter strings) +ord+ will return the ASCII value.
917
-
1439
+ When the RubyLabs module is loaded it defines a new method named +ord+ to the
1440
+ Fixnum class. In Ruby 1.8, using the <tt>[]</tt> operator to access items in a String object
1441
+ returns the ASCII value of a character. The +ord+ method defined here (and used by hash functions defined in hashlab.rb)
1442
+ maps the ASCII value of a letter to a number between 0 and 25.
1443
+
1444
+ The BitLab module also extends Fixnum by defining a method named +code+ that returns a Code
1445
+ object containing the binary or hexadecimal representation of an integer.
1446
+ #--
1447
+ NOTE: +ord+ is built in to Ruby 1.9, so this method will have to be renamed or reimplemented
1448
+ when RubyLabs is ported to 1.9.
918
1449
  =end
919
1450
 
920
- def ord
921
- if self >= ?a && self <= ?z
922
- self - ?a
923
- elsif self >= ?A && self <= ?Z
924
- self - ?A
925
- else
926
- self
927
- end
928
- end
1451
+ class Fixnum
1452
+
1453
+ # If a number is the ASCII code for a letter from the Roman alphabet (upper or lower case,
1454
+ # in the range 'A' to 'Z') map it to a number between 0 and 25, otherwise just return the
1455
+ # value of the number.
1456
+ #
1457
+ # Example:
1458
+ # >> "Ducks!".each_byte { |x| puts x.ord }
1459
+ # 3
1460
+ # 20
1461
+ # 2
1462
+ # 10
1463
+ # 18
1464
+ # 33
1465
+
1466
+ def ord
1467
+ if self >= ?a && self <= ?z
1468
+ self - ?a
1469
+ elsif self >= ?A && self <= ?Z
1470
+ self - ?A
1471
+ else
1472
+ self
1473
+ end
1474
+ end
929
1475
 
930
1476
  end # Fixnum
931
1477