rubylabs 0.6.4 → 0.7.0
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/VERSION +1 -1
- data/lib/randomlab.rb +143 -224
- data/lib/rubylabs.rb +262 -38
- data/lib/spherelab.rb +566 -209
- data/lib/tsplab.rb +308 -276
- data/test/sphere_test.rb +32 -0
- metadata +2 -22
- data/data/aafreq.txt +0 -20
- data/data/cars.txt +0 -50
- data/data/century.txt +0 -1
- data/data/colors.txt +0 -64
- data/data/earth.yaml +0 -15
- data/data/fruit.txt +0 -45
- data/data/hacodes.txt +0 -35
- data/data/hafreq.txt +0 -16
- data/data/hvfreq.txt +0 -5
- data/data/nbody.R +0 -23
- data/data/nbody.out +0 -731
- data/data/nbody.pdf +0 -3111
- data/data/nbody.png +0 -0
- data/data/nbody3d.pdf +0 -3201
- data/data/outer.pdf +0 -182785
- data/data/solarsystem.txt +0 -17
- data/data/suits.txt +0 -1
- data/data/wordlist.txt +0 -210653
- data/lib/sortlab.rb +0 -213
- data/lib/viewer.rb +0 -65
data/lib/tsplab.rb
CHANGED
@@ -12,7 +12,11 @@
|
|
12
12
|
TODO plot space: get all 180K costs, order lexicographically, plot (smoothed)
|
13
13
|
=end
|
14
14
|
|
15
|
-
|
15
|
+
module RubyLabs
|
16
|
+
|
17
|
+
module TSPLab
|
18
|
+
|
19
|
+
require 'set'
|
16
20
|
|
17
21
|
=begin rdoc
|
18
22
|
|
@@ -33,7 +37,7 @@ minutes.
|
|
33
37
|
# distance(x,y) returns the distance between cities with keys x and y
|
34
38
|
|
35
39
|
|
36
|
-
class CityList
|
40
|
+
class CityList
|
37
41
|
|
38
42
|
=begin rdoc
|
39
43
|
The lines in the input file passed to the constructor come from a
|
@@ -43,130 +47,101 @@ city 1, city 2, days, hours, minutes, all separated by tabs. Each
|
|
43
47
|
city is assigned a one-letter key.
|
44
48
|
=end
|
45
49
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
50
|
+
def initialize(matrix)
|
51
|
+
matrixfilename = matrix.to_s + ".txt"
|
52
|
+
matrixfilename = File.join(@@dataDirectory, matrixfilename)
|
53
|
+
raise "Matrix not found: #{matrixfilename}" unless File.exists?(matrixfilename)
|
54
|
+
|
55
|
+
@key = ?A
|
56
|
+
@cities = Hash.new # initially a map from name string to one-letter key
|
57
|
+
@matrix = Hash.new
|
58
|
+
|
59
|
+
File.open(matrixfilename).each do |line|
|
60
|
+
next if line.chomp.length == 0
|
61
|
+
rec = line.split(/\t/)
|
62
|
+
i = getKey(rec[0])
|
63
|
+
j = getKey(rec[1])
|
64
|
+
dist = ((24 * getValue(rec[2]) + getValue(rec[3])) * 60) + getValue(rec[4])
|
65
|
+
assign(i,j,dist)
|
66
|
+
assign(j,i,dist)
|
67
|
+
end
|
62
68
|
|
63
|
-
|
69
|
+
@cities = @cities.invert # from now on a map from key to name string
|
64
70
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
+
errs = 0
|
72
|
+
(?A..@key-1).each do |i|
|
73
|
+
(i+1..@key-1).each do |j|
|
74
|
+
unless @matrix[i][j] && @matrix[j][i]
|
75
|
+
errs += 1
|
76
|
+
puts "CityList.new: missing distance between #{@cities[i]} and #{@cities[j]}"
|
77
|
+
end
|
71
78
|
end
|
72
79
|
end
|
73
|
-
|
74
|
-
raise "CityList.new: errors in input file" if errs > 0
|
80
|
+
raise "CityList.new: errors in input file" if errs > 0
|
75
81
|
|
76
|
-
|
82
|
+
end
|
77
83
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
84
|
+
def inspect()
|
85
|
+
n = @matrix.length
|
86
|
+
return sprintf "TSPLab::CityList (%d x %d)", n, n-1
|
87
|
+
end
|
82
88
|
|
83
|
-
|
84
|
-
|
85
|
-
|
89
|
+
def size()
|
90
|
+
return @key - ?A
|
91
|
+
end
|
86
92
|
|
87
|
-
|
88
|
-
|
89
|
-
|
93
|
+
def keys()
|
94
|
+
return @cities.keys.sort.map{|x| x.chr}
|
95
|
+
end
|
90
96
|
|
91
|
-
|
92
|
-
|
93
|
-
|
97
|
+
def values()
|
98
|
+
return @cities.keys.sort.collect{|x| @cities[x]}
|
99
|
+
end
|
94
100
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
101
|
+
def [](x)
|
102
|
+
x = x[0] if x.class == String
|
103
|
+
return nil unless x >= ?A && x < @key
|
104
|
+
return @cities[x]
|
105
|
+
end
|
100
106
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
107
|
+
def distance(x,y)
|
108
|
+
x = x[0] if x.class == String
|
109
|
+
y = y[0] if y.class == String
|
110
|
+
return nil unless x >= ?A && x < @key
|
111
|
+
return nil unless y >= ?A && y < @key
|
112
|
+
return @matrix[x][y]
|
113
|
+
end
|
108
114
|
|
109
115
|
# private methods -- return the key for a city (make a new one if necessary),
|
110
116
|
# get a time value from an input line, save a distance
|
111
117
|
|
112
|
-
|
113
|
-
|
114
|
-
def getKey(s)
|
115
|
-
if @cities[s] == nil
|
116
|
-
@cities[s] = @key
|
117
|
-
@key += 1
|
118
|
-
end
|
119
|
-
return @cities[s]
|
120
|
-
end
|
121
|
-
|
122
|
-
def getValue(s)
|
123
|
-
a = s.scan(/(\d+)/)
|
124
|
-
if a.length == 1
|
125
|
-
return a[0][0].to_i
|
126
|
-
else
|
127
|
-
return 0
|
128
|
-
end
|
129
|
-
end
|
118
|
+
private
|
130
119
|
|
131
|
-
|
132
|
-
|
133
|
-
|
120
|
+
def getKey(s)
|
121
|
+
if @cities[s] == nil
|
122
|
+
@cities[s] = @key
|
123
|
+
@key += 1
|
124
|
+
end
|
125
|
+
return @cities[s]
|
134
126
|
end
|
135
|
-
@matrix[i][j] = v
|
136
|
-
end
|
137
|
-
|
138
|
-
end
|
139
127
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
for i in 0..length-2
|
148
|
-
r = rand(length-i) + i # i <= r < length
|
149
|
-
self[i],self[r] = self[r],self[i]
|
128
|
+
def getValue(s)
|
129
|
+
a = s.scan(/(\d+)/)
|
130
|
+
if a.length == 1
|
131
|
+
return a[0][0].to_i
|
132
|
+
else
|
133
|
+
return 0
|
134
|
+
end
|
150
135
|
end
|
151
|
-
self
|
152
|
-
end
|
153
|
-
|
154
|
-
|
155
|
-
=begin rdoc
|
156
|
-
Call <tt>s.rotate</tt> to "rotate" the characters in string +s+, i.e. detach
|
157
|
-
the last character in +s+ and reinsert it at the front.
|
158
|
-
=end
|
159
136
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
137
|
+
def assign(i,j,v)
|
138
|
+
if @matrix[i] == nil
|
139
|
+
@matrix[i] = Hash.new
|
140
|
+
end
|
141
|
+
@matrix[i][j] = v
|
165
142
|
end
|
166
|
-
self
|
167
|
-
end
|
168
143
|
|
169
|
-
end
|
144
|
+
end # class CityList
|
170
145
|
|
171
146
|
=begin rdoc
|
172
147
|
|
@@ -181,129 +156,129 @@ inherited from a parent, and incremented whenever a mutation or crossover is app
|
|
181
156
|
=end
|
182
157
|
|
183
158
|
|
184
|
-
class Tour
|
185
|
-
|
159
|
+
class Tour
|
160
|
+
attr_reader :id, :path, :cost, :matrix, :nm, :nx
|
186
161
|
|
187
|
-
|
162
|
+
@@id = 0
|
188
163
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
164
|
+
def initialize(m, s = nil, nm = 0, nx = 0)
|
165
|
+
if s == nil
|
166
|
+
@path = m.keys.to_s.permute!
|
167
|
+
else
|
168
|
+
skeys = Set.new(m.keys)
|
169
|
+
used = Set.new
|
170
|
+
s.each_byte do |x|
|
171
|
+
raise "Tour.new: invalid character in tour: #{x}" unless skeys.member?(x.chr)
|
172
|
+
raise "Tour.new: duplicate character in tour: #{x.chr}" if used.member?(x)
|
173
|
+
used.add(x)
|
174
|
+
end
|
175
|
+
@path = s.clone
|
199
176
|
end
|
200
|
-
@
|
177
|
+
@cost = pathcost(m,@path)
|
178
|
+
@matrix = m # need matrix to update cost after mutation...
|
179
|
+
@id = @@id
|
180
|
+
@nm = nm
|
181
|
+
@nx = nx
|
182
|
+
@@id += 1
|
201
183
|
end
|
202
|
-
@cost = pathcost(m,@path)
|
203
|
-
@matrix = m # need matrix to update cost after mutation...
|
204
|
-
@id = @@id
|
205
|
-
@nm = nm
|
206
|
-
@nx = nx
|
207
|
-
@@id += 1
|
208
|
-
end
|
209
184
|
|
210
|
-
|
211
|
-
|
185
|
+
# An alterntaive constructor -- either copy a single parent and add
|
186
|
+
# a point mutation, or cross two parents
|
212
187
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
188
|
+
def Tour.reproduce(x, y = nil, trace = nil)
|
189
|
+
if y.nil? || y == :trace
|
190
|
+
i = rand(x.path.length)
|
191
|
+
j = (i+1) % x.path.length
|
192
|
+
trace = :trace if y == :trace # handle calls like reproduce(x, :trace)
|
193
|
+
x.clone.mutate(i, j, trace)
|
194
|
+
else
|
195
|
+
i = rand(x.path.length)
|
196
|
+
loop { j = rand(x.path.length); break if j != i }
|
197
|
+
i, j = j, i if i > j
|
198
|
+
x.clone.cross(y, i, j, trace)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def to_s()
|
203
|
+
return "\##{@id}: #{@path} / #{@cost}"
|
204
|
+
end
|
230
205
|
|
231
|
-
|
232
|
-
|
233
|
-
|
206
|
+
def inspect
|
207
|
+
return to_s
|
208
|
+
end
|
234
209
|
|
235
|
-
|
210
|
+
# Reset the id to 0 (e.g. to count number of objects)
|
236
211
|
|
237
|
-
|
238
|
-
|
239
|
-
|
212
|
+
def Tour.reset
|
213
|
+
@@id = 0
|
214
|
+
end
|
240
215
|
|
241
|
-
|
242
|
-
|
243
|
-
|
216
|
+
def Tour.count
|
217
|
+
@@id
|
218
|
+
end
|
244
219
|
|
245
|
-
|
220
|
+
# A copy of a tour needs its own new id and copy of the path and pedigree
|
246
221
|
|
247
|
-
|
248
|
-
|
249
|
-
|
222
|
+
def clone
|
223
|
+
return Tour.new(@matrix, @path, @nm, @nx)
|
224
|
+
end
|
250
225
|
|
251
|
-
|
252
|
-
|
226
|
+
# Exchange mutation (called EM by Larranaga et al). Simply swaps two
|
227
|
+
# cities at the specified locations.
|
253
228
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
229
|
+
def mutate(i, j, trace = nil)
|
230
|
+
puts "mutate: #{i} <-> #{j}" if ! trace.nil?
|
231
|
+
@path[i], @path[j] = @path[j], @path[i]
|
232
|
+
@cost = pathcost(@matrix,@path)
|
233
|
+
@nm += 1
|
234
|
+
self
|
235
|
+
end
|
261
236
|
|
262
|
-
|
263
|
-
|
264
|
-
|
237
|
+
# Order cross-over (called OX1 by Larranaga et al). Save a chunk of the
|
238
|
+
# current tour, then copy the remaining cities in the order they occur in
|
239
|
+
# tour t.
|
265
240
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
241
|
+
def cross(t, i, j, trace = nil)
|
242
|
+
puts "cross: copy #{i}..#{j}" if ! trace.nil?
|
243
|
+
@path = @path[i..j]
|
244
|
+
t.path.each_byte do |c|
|
245
|
+
@path << c unless @path.include?(c)
|
246
|
+
end
|
247
|
+
@cost = pathcost(@matrix,@path)
|
248
|
+
@nx += 1 + t.nx
|
249
|
+
@nm += t.nm
|
250
|
+
self
|
271
251
|
end
|
272
|
-
@cost = pathcost(@matrix,@path)
|
273
|
-
@nx += 1 + t.nx
|
274
|
-
@nm += t.nm
|
275
|
-
self
|
276
|
-
end
|
277
252
|
|
278
|
-
|
279
|
-
|
253
|
+
# private methods -- compute the cost of a path, given a matrix; apply
|
254
|
+
# mutations (point mutations, cross-overs)
|
280
255
|
|
281
|
-
|
256
|
+
private
|
282
257
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
258
|
+
def pathcost(m,s)
|
259
|
+
sum = m.distance(s[0],s[-1]) # link from end to start
|
260
|
+
for i in 0..s.length-2
|
261
|
+
sum += m.distance(s[i],s[i+1])
|
262
|
+
end
|
263
|
+
return sum
|
287
264
|
end
|
288
|
-
return sum
|
289
|
-
end
|
290
265
|
|
291
|
-
end
|
266
|
+
end # class Tour
|
292
267
|
|
293
268
|
# Make a population of size n -- n instances of tours made from a set
|
294
269
|
# of cities
|
295
270
|
|
296
|
-
def initPopulation(n,m)
|
297
|
-
|
271
|
+
def initPopulation(n,m)
|
272
|
+
a = Array.new
|
298
273
|
|
299
|
-
|
300
|
-
|
301
|
-
|
274
|
+
n.times do
|
275
|
+
a.push(Tour.new(m))
|
276
|
+
end
|
302
277
|
|
303
|
-
|
278
|
+
a.sort! { |x,y| x.cost <=> y.cost }
|
304
279
|
|
305
|
-
|
306
|
-
end
|
280
|
+
return a
|
281
|
+
end
|
307
282
|
|
308
283
|
# Evolve a population. Sort by fitness, then remove individuals with
|
309
284
|
# a probability based on their ranking (the ith individual survives
|
@@ -312,105 +287,162 @@ end
|
|
312
287
|
# argument is a collection of parameters that can override various
|
313
288
|
# options (e.g. the probability of survival).
|
314
289
|
|
315
|
-
def evolve(p, param={})
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
290
|
+
def evolve(p, param={})
|
291
|
+
debug = param[:trace] ? true : false
|
292
|
+
pcross = param[:pcross] ? param[:pcross] : 0.25
|
293
|
+
plotci = param[:plotci]
|
294
|
+
|
295
|
+
p.sort! { |x,y| x.cost <=> y.cost }
|
296
|
+
i = 0
|
297
|
+
n = p.length
|
298
|
+
|
299
|
+
if plotci
|
300
|
+
m = 0.0
|
301
|
+
p.each { |t| m += t.cost }
|
302
|
+
m = m / p.length
|
303
|
+
printf "%.2f %.2f %.2f\n", m, p[0].cost, p[-1].cost
|
304
|
+
end
|
305
|
+
|
306
|
+
# phase 1 -- delete random tours
|
307
|
+
|
308
|
+
while i < p.length
|
309
|
+
pk = (n-i).to_f / n
|
310
|
+
if rand < pk
|
311
|
+
puts "keep #{p[i]}" if debug
|
312
|
+
i += 1 # keep this tour, skip to next one
|
313
|
+
else
|
314
|
+
puts "zap #{p[i]}" if debug
|
315
|
+
p.delete_at(i) # zap this one; keep i at same value
|
316
|
+
end
|
341
317
|
end
|
342
|
-
end
|
343
318
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
319
|
+
# phase 2 -- build back up to n tours; prev is the index of the last
|
320
|
+
# tour from the previous generation (candidates for cloning/crossing)
|
321
|
+
|
322
|
+
best = p[0]
|
323
|
+
prev = p.length
|
324
|
+
|
325
|
+
while p.length < n
|
326
|
+
mom = p[rand(prev)]
|
327
|
+
if rand < pcross
|
328
|
+
dad = p[rand(prev)]
|
329
|
+
kid = Tour.reproduce(mom,dad)
|
330
|
+
puts "#{mom} x #{dad} => #{kid}" if debug
|
331
|
+
else
|
332
|
+
kid = Tour.reproduce(mom)
|
333
|
+
puts "#{mom} => #{kid}" if debug
|
334
|
+
end
|
335
|
+
p.push(kid)
|
336
|
+
best = kid if kid.cost < best.cost
|
359
337
|
end
|
360
|
-
p.push(kid)
|
361
|
-
best = kid if kid.cost < best.cost
|
362
|
-
end
|
363
338
|
|
364
|
-
|
339
|
+
return best
|
365
340
|
|
366
|
-
end
|
341
|
+
end
|
367
342
|
|
368
343
|
# High level interface -- make a population for a set of cities, then
|
369
344
|
# call evolve() until the best tour doesn't change after some number of
|
370
345
|
# iterations (also passed as a parameter). Print the tour when done.
|
371
346
|
|
372
|
-
def bestTour(matrix, param={})
|
373
|
-
|
374
|
-
|
375
|
-
|
347
|
+
def bestTour(matrix, param={})
|
348
|
+
popsize = param[:popsize] ? param[:popsize] : 10
|
349
|
+
maxgen = param[:maxgen] ? param[:maxgen] : 25
|
350
|
+
maxstatic = param[:maxstatic] ? param[:maxstatic] : maxgen/2
|
351
|
+
|
352
|
+
p = initPopulation(popsize,matrix)
|
353
|
+
best = p[0]
|
354
|
+
nstatic = 0
|
355
|
+
ngen = 0
|
376
356
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
357
|
+
if param[:verbose]
|
358
|
+
puts "popsize: #{popsize}"
|
359
|
+
puts "maxgen: #{maxgen}"
|
360
|
+
puts "maxstatic: #{maxstatic}"
|
361
|
+
puts "pcross: #{param[:pcross]}"
|
362
|
+
end
|
363
|
+
|
364
|
+
while ngen < maxgen && nstatic < maxstatic
|
365
|
+
puts "best tour: #{p[0]}" if param[:verbose]
|
366
|
+
evolve(p, param)
|
367
|
+
if p[0].cost < best.cost
|
368
|
+
best = p[0]
|
369
|
+
nstatic = 0
|
370
|
+
else
|
371
|
+
nstatic += 1
|
372
|
+
end
|
373
|
+
ngen += 1
|
374
|
+
end
|
375
|
+
|
376
|
+
# puts "#{ngen} generations (#{nstatic} at best value)"
|
377
|
+
puts "#{ngen} generations"
|
381
378
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
379
|
+
if param[:verbose]
|
380
|
+
puts "best cost: #{best.cost}"
|
381
|
+
puts "pedigree: #{best.nx} cross #{best.nm} mutate"
|
382
|
+
puts "best tour:"
|
383
|
+
best.path.each_byte do |x|
|
384
|
+
puts " " + matrix[x]
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
return best
|
387
389
|
end
|
388
390
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
if
|
393
|
-
|
394
|
-
|
395
|
-
else
|
396
|
-
nstatic += 1
|
391
|
+
def checkout(matrix, filename = nil)
|
392
|
+
matrixfilename = matrix.to_s + ".txt"
|
393
|
+
matrixfilename = File.join(@@dataDirectory, matrixfilename)
|
394
|
+
if !File.exists?(matrixfilename)
|
395
|
+
puts "Matrix not found: #{matrixfilename}"
|
396
|
+
return nil
|
397
397
|
end
|
398
|
-
|
398
|
+
outfilename = filename.nil? ? (matrix.to_s + ".txt") : filename
|
399
|
+
dest = File.open(outfilename, "w")
|
400
|
+
File.open(matrixfilename).each do |line|
|
401
|
+
dest.puts line.chomp
|
402
|
+
end
|
403
|
+
dest.close
|
404
|
+
puts "Copy of #{matrix} saved in #{outfilename}"
|
399
405
|
end
|
406
|
+
|
407
|
+
# Values accessible to all the methods in the module
|
408
|
+
|
409
|
+
@@dataDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'tsp')
|
410
|
+
|
411
|
+
end # module TSPLab
|
412
|
+
|
413
|
+
end # module RubyLabs
|
414
|
+
|
415
|
+
# Additions to the String class
|
400
416
|
|
401
|
-
|
402
|
-
puts "#{ngen} generations"
|
417
|
+
class String
|
403
418
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
419
|
+
=begin rdoc
|
420
|
+
Call <tt>s.permute</tt> to scramble the characters in string +s+.
|
421
|
+
=end
|
422
|
+
|
423
|
+
def permute!
|
424
|
+
for i in 0..length-2
|
425
|
+
r = rand(length-i) + i # i <= r < length
|
426
|
+
self[i],self[r] = self[r],self[i]
|
410
427
|
end
|
428
|
+
self
|
429
|
+
end
|
430
|
+
|
431
|
+
|
432
|
+
=begin rdoc
|
433
|
+
Call <tt>s.rotate</tt> to "rotate" the characters in string +s+, i.e. detach
|
434
|
+
the last character in +s+ and reinsert it at the front.
|
435
|
+
=end
|
436
|
+
|
437
|
+
def rotate!(n)
|
438
|
+
if n > 0
|
439
|
+
tail = self[n..-1]
|
440
|
+
self[n..-1] = ""
|
441
|
+
self.insert(0,tail)
|
442
|
+
end
|
443
|
+
self
|
411
444
|
end
|
412
445
|
|
413
|
-
return best
|
414
446
|
end
|
415
447
|
|
416
448
|
|