rubylabs 0.9.0 → 0.9.1

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