rubylabs 0.6.4 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|