gratr 0.4.2 → 0.4.3
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/README +7 -0
- data/lib/gratr.rb +9 -0
- data/lib/gratr/base.rb +1 -1
- data/lib/gratr/digraph.rb +5 -3
- data/lib/gratr/rdot.rb +6 -0
- data/lib/gratr/search.rb +37 -24
- data/lib/priority-queue/benchmark/dijkstra.rb +171 -0
- data/lib/priority-queue/compare_comments.rb +49 -0
- data/lib/priority-queue/ext/priority_queue/CPriorityQueue/extconf.rb +2 -0
- data/lib/priority-queue/lib/priority_queue.rb +14 -0
- data/lib/priority-queue/lib/priority_queue/c_priority_queue.rb +1 -0
- data/lib/priority-queue/lib/priority_queue/poor_priority_queue.rb +46 -0
- data/lib/priority-queue/lib/priority_queue/ruby_priority_queue.rb +525 -0
- data/lib/priority-queue/setup.rb +1551 -0
- data/lib/priority-queue/test/priority_queue_test.rb +371 -0
- data/tests/TestDigraph.rb +3 -3
- data/tests/TestDot.rb +75 -0
- data/tests/TestSearch.rb +36 -16
- metadata +12 -2
data/README
CHANGED
|
@@ -269,6 +269,13 @@ Rick Bradley who reworked the library and added many graph theoretic constructs.
|
|
|
269
269
|
The core interface is about to be updated to allow for cleaner dependency injects. Luke Kanies has also contributed
|
|
270
270
|
several optimizations for speed and logged some bugs. This will result in a 0.5 release coming soon.
|
|
271
271
|
|
|
272
|
+
=== 0.4.3
|
|
273
|
+
|
|
274
|
+
* Fixed bug in dot output dependency.
|
|
275
|
+
* Changed method.call to send in a couple places.
|
|
276
|
+
* Fixed bug in unconnected vertices for reversal. Ticket #7237
|
|
277
|
+
* Fixed bugs in A* Search. Ticket #7250.
|
|
278
|
+
|
|
272
279
|
=== 0.4.2
|
|
273
280
|
|
|
274
281
|
* Fixed bug in parallel edge adjacency detection.
|
data/lib/gratr.rb
CHANGED
|
@@ -31,3 +31,12 @@ require 'gratr/base'
|
|
|
31
31
|
require 'gratr/digraph'
|
|
32
32
|
require 'gratr/undirected_graph'
|
|
33
33
|
require 'gratr/common'
|
|
34
|
+
|
|
35
|
+
# Load priority queue classes
|
|
36
|
+
begin
|
|
37
|
+
# Use installed Gem
|
|
38
|
+
require 'priority_queue'
|
|
39
|
+
rescue LoadError # Use local copy
|
|
40
|
+
require 'priority-queue/lib/priority_queue/ruby_priority_queue'
|
|
41
|
+
PriorityQueue = RubyPriorityQueue
|
|
42
|
+
end
|
data/lib/gratr/base.rb
CHANGED
data/lib/gratr/digraph.rb
CHANGED
|
@@ -64,11 +64,13 @@ module GRATR
|
|
|
64
64
|
|
|
65
65
|
# A digraph uses the Arc class for edges
|
|
66
66
|
def edge_class() @parallel_edges ? GRATR::MultiArc : GRATR::Arc; end
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
# Reverse all edges in a graph
|
|
69
69
|
def reversal
|
|
70
|
-
|
|
71
|
-
edges.inject(
|
|
70
|
+
result = self.class.new
|
|
71
|
+
edges.inject(result) {|a,e| a << e.reverse}
|
|
72
|
+
vertices.each { |v| result.add_vertex!(v) unless result.vertex?(v) }
|
|
73
|
+
result
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
# Return true if the Graph is oriented.
|
data/lib/gratr/rdot.rb
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
#
|
|
8
8
|
# It also supports undirected edges.
|
|
9
9
|
|
|
10
|
+
class Hash
|
|
11
|
+
def stringify_keys
|
|
12
|
+
inject({}) {|options, (key, value)| options[key.to_s] = value; options}
|
|
13
|
+
end
|
|
14
|
+
end unless Hash.respond_to? :stringify_keys
|
|
15
|
+
|
|
10
16
|
module DOT
|
|
11
17
|
|
|
12
18
|
# These glogal vars are used to make nice graph source.
|
data/lib/gratr/search.rb
CHANGED
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
27
27
|
#++
|
|
28
28
|
|
|
29
|
-
|
|
30
29
|
module GRATR
|
|
31
30
|
module Graph
|
|
32
31
|
module Search
|
|
@@ -102,7 +101,7 @@ module GRATR
|
|
|
102
101
|
roots = []
|
|
103
102
|
te = Proc.new {|e| predecessor[e.target] = e.source}
|
|
104
103
|
rv = Proc.new {|v| roots << v}
|
|
105
|
-
|
|
104
|
+
send routine, :start => start, :tree_edge => te, :root_vertex => rv
|
|
106
105
|
[predecessor, roots]
|
|
107
106
|
end
|
|
108
107
|
|
|
@@ -120,7 +119,7 @@ module GRATR
|
|
|
120
119
|
correct_tree = false
|
|
121
120
|
te = Proc.new {|e| predecessor[e.target] = e.source if correct_tree}
|
|
122
121
|
rv = Proc.new {|v| correct_tree = (v == start)}
|
|
123
|
-
|
|
122
|
+
send routine, :start => start, :tree_edge => te, :root_vertex => rv
|
|
124
123
|
predecessor
|
|
125
124
|
end
|
|
126
125
|
|
|
@@ -252,44 +251,58 @@ module GRATR
|
|
|
252
251
|
# Also see: http://en.wikipedia.org/wiki/A-star_search_algorithm
|
|
253
252
|
#
|
|
254
253
|
def astar(start, goal, func, options, &block)
|
|
255
|
-
options.instance_eval "def
|
|
256
|
-
|
|
257
|
-
|
|
254
|
+
options.instance_eval "def handle_callback(sym,u) self[sym].call(u) if self[sym]; end"
|
|
255
|
+
|
|
256
|
+
# Initialize
|
|
258
257
|
d = { start => 0 }
|
|
259
|
-
|
|
260
|
-
color = {start => :gray}
|
|
261
|
-
|
|
262
|
-
|
|
258
|
+
|
|
259
|
+
color = {start => :gray} # Open is :gray, Closed is :black
|
|
260
|
+
parent = Hash.new {|k| parent[k] = k}
|
|
261
|
+
f = {start => func.call(start)}
|
|
262
|
+
queue = PriorityQueue.new.push(start,f[start])
|
|
263
263
|
block.call(start) if block
|
|
264
|
+
|
|
265
|
+
# Process queue
|
|
264
266
|
until queue.empty?
|
|
265
|
-
u = queue.
|
|
266
|
-
options.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
u,dummy = queue.delete_min
|
|
268
|
+
options.handle_callback(:examine_vertex, u)
|
|
269
|
+
|
|
270
|
+
# Unravel solution if goal is reached.
|
|
271
|
+
if u == goal
|
|
272
|
+
solution = [goal]
|
|
273
|
+
while u != start
|
|
274
|
+
solution << parent[u]; u = parent[u]
|
|
275
|
+
end
|
|
276
|
+
return solution.reverse
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
adjacent(u, :type => :edges).each do |e|
|
|
280
|
+
v = e.source == u ? e.target : e.source
|
|
281
|
+
options.handle_callback(:examine_edge, e)
|
|
270
282
|
w = cost(e, options[:weight])
|
|
271
283
|
raise ArgumentError unless w
|
|
272
284
|
if d[v].nil? or (w + d[u]) < d[v]
|
|
273
|
-
options.
|
|
285
|
+
options.handle_callback(:edge_relaxed, e)
|
|
274
286
|
d[v] = w + d[u]
|
|
275
|
-
f[v] = d[v] + func.call(
|
|
276
|
-
|
|
287
|
+
f[v] = d[v] + func.call(v)
|
|
288
|
+
parent[v] = u
|
|
277
289
|
unless color[v] == :gray
|
|
278
|
-
options.
|
|
290
|
+
options.handle_callback(:black_target, v) if color[v] == :black
|
|
279
291
|
color[v] = :gray
|
|
280
|
-
options.
|
|
281
|
-
queue
|
|
292
|
+
options.handle_callback(:discover_vertex, v)
|
|
293
|
+
queue.push v, f[v]
|
|
282
294
|
block.call(v) if block
|
|
283
|
-
return [start]+queue if v == goal
|
|
284
295
|
end
|
|
285
296
|
else
|
|
286
|
-
options.
|
|
297
|
+
options.handle_callback(:edge_not_relaxed, e)
|
|
287
298
|
end
|
|
288
299
|
end # adjacent(u)
|
|
289
300
|
color[u] = :black
|
|
290
|
-
options.
|
|
301
|
+
options.handle_callback(:finish_vertex,u)
|
|
291
302
|
end # queue.empty?
|
|
303
|
+
|
|
292
304
|
nil # failure, on fall through
|
|
305
|
+
|
|
293
306
|
end # astar
|
|
294
307
|
|
|
295
308
|
# Best first has all the same options as astar with func set to h(v) = 0.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
$:.unshift "~/lib/ruby"
|
|
2
|
+
require 'priority_queue/ruby_priority_queue'
|
|
3
|
+
require 'priority_queue/poor_priority_queue'
|
|
4
|
+
require 'priority_queue/c_priority_queue'
|
|
5
|
+
require 'benchmark'
|
|
6
|
+
|
|
7
|
+
class Node
|
|
8
|
+
attr_reader :neighbours, :id
|
|
9
|
+
|
|
10
|
+
def initialize(id)
|
|
11
|
+
@neighbours = []
|
|
12
|
+
@id = id
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inspect
|
|
16
|
+
to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s
|
|
20
|
+
"(#{@id})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Build a graph by adding nodes with random connections
|
|
25
|
+
|
|
26
|
+
# Return a random graph with an average degree of degree
|
|
27
|
+
def make_graph(nodes, degree)
|
|
28
|
+
nodes = Array.new(nodes) { | i | Node.new(i.to_s) }
|
|
29
|
+
nodes.each do | n |
|
|
30
|
+
(degree / 2).times do
|
|
31
|
+
true while (n1 = nodes[rand(nodes.length)]) == n
|
|
32
|
+
n.neighbours << nodes[rand(nodes.length)]
|
|
33
|
+
n1.neighbours << n
|
|
34
|
+
n.neighbours << n1
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def draw_graph(nodes, out)
|
|
40
|
+
dot = [] << "graph g {"
|
|
41
|
+
nodes.each do | n1 |
|
|
42
|
+
dot << "N#{n1.id} [label='#{n1.id}'];"
|
|
43
|
+
n1.neighbours.each do | n2 |
|
|
44
|
+
dot << "N#{n1.id} -- N#{n2.id};" if n1.id <= n2.id
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
dot << "}"
|
|
48
|
+
|
|
49
|
+
# system "echo '#{dot}' | neato -Gepsilon=0.001 -Goverlap=scale -Gsplines=true -Gsep=.4 -Tps -o #{out}"
|
|
50
|
+
system "echo '#{dot}' | neato -Gepsilon=0.05 -Goverlap=scale -Gsep=.4 -Tps -o #{out}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def dijkstra(start_node, queue_klass)
|
|
54
|
+
# Priority Queue with unfinished nodes
|
|
55
|
+
active = queue_klass.new
|
|
56
|
+
# Distances for all nodes
|
|
57
|
+
distances = Hash.new { 1.0 / 0.0 }
|
|
58
|
+
# Parent pointers describing shortest paths for all nodes
|
|
59
|
+
parents = Hash.new
|
|
60
|
+
|
|
61
|
+
# Initialize with start node
|
|
62
|
+
active[start_node] = 0
|
|
63
|
+
until active.empty?
|
|
64
|
+
u, distance = active.delete_min
|
|
65
|
+
distances[u] = distance
|
|
66
|
+
d = distance + 1
|
|
67
|
+
u.neighbours.each do | v |
|
|
68
|
+
next unless d < distances[v] # we can't relax this one
|
|
69
|
+
active[v] = distances[v] = d
|
|
70
|
+
parents[v] = u
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
srand
|
|
76
|
+
|
|
77
|
+
sizes = Array.new(4) { | base | Array.new(9) { | mult | (mult+1) * 10**(base+2) } }.flatten
|
|
78
|
+
degrees = [2, 4, 16]
|
|
79
|
+
degrees = [4, 16]
|
|
80
|
+
degrees = [16]
|
|
81
|
+
queues = [ CPriorityQueue, PoorPriorityQueue, RubyPriorityQueue ]
|
|
82
|
+
queues = [ CPriorityQueue, RubyPriorityQueue ]
|
|
83
|
+
|
|
84
|
+
max_time = 400
|
|
85
|
+
ignore = Hash.new
|
|
86
|
+
|
|
87
|
+
repeats = 5
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
STDOUT.sync = true
|
|
91
|
+
|
|
92
|
+
results = Hash.new { | h, k | h[k] =
|
|
93
|
+
Hash.new { | h1, k1 | h1[k1] = Hash.new { 0 }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Benchmark.bm(30) do | b |
|
|
98
|
+
sizes.each do | size |
|
|
99
|
+
break if !ignore.empty? and ignore.values.inject(true) { | r, v | r and v }
|
|
100
|
+
puts
|
|
101
|
+
puts "Testing with graphs of size #{size}"
|
|
102
|
+
degrees.each do | degree |
|
|
103
|
+
repeats.times do | r |
|
|
104
|
+
nodes = make_graph(size, degree)
|
|
105
|
+
queues.each do | queue |
|
|
106
|
+
next if ignore[queue]
|
|
107
|
+
GC.start
|
|
108
|
+
results[queue][degree][size] += (b.report("#{queue}: #{size} (#{degree})") do dijkstra(nodes[1], queue) end).real
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
queues.each do | queue |
|
|
112
|
+
ignore[queue] ||= ((results[queue][degree][size] / repeats) > max_time)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
indices = queues.map { | q | degrees.map { | d | %&"#{q} (Graph of Degree: #{d})"& } }.flatten
|
|
117
|
+
File.open("results.csv", "wb") do | f |
|
|
118
|
+
f.puts "size\t" + indices.join("\t")
|
|
119
|
+
sizes.each do | size |
|
|
120
|
+
f.puts "#{size}\t" + queues.map { | q | degrees.map { | d |
|
|
121
|
+
(results[q][d].has_key?(size) and results[q][d][size] > 0.0) ? results[q][d][size] / repeats : "''"
|
|
122
|
+
} }.join("\t")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
File.open("results.gp", 'wb') do | f |
|
|
127
|
+
lines = []
|
|
128
|
+
indices.each_with_index do | t, i |
|
|
129
|
+
lines << " 'results.csv' using 1:#{i+2} with lines title #{t}"
|
|
130
|
+
end
|
|
131
|
+
f.puts "set term png"
|
|
132
|
+
f.puts "set out 'results.png'"
|
|
133
|
+
f.puts "set xlabel 'Number of nodes'"
|
|
134
|
+
f.puts "set ylabel 'Time in seconds (real)'"
|
|
135
|
+
f.puts "set logscale xy"
|
|
136
|
+
f.puts "set title 'Dijkstras Shortest Path Algorithm using different PQ Implementations'"
|
|
137
|
+
f.puts "plot \\"
|
|
138
|
+
f.puts lines.join(",\\\n")
|
|
139
|
+
end
|
|
140
|
+
system "gnuplot results.gp"
|
|
141
|
+
|
|
142
|
+
queues.each do | q |
|
|
143
|
+
File.open("result-#{q}.gp", 'wb') do | f |
|
|
144
|
+
lines = []
|
|
145
|
+
degrees.map { | d | %&"#{q} (Graph of Degree: #{d})"& }.flatten.each do | t |
|
|
146
|
+
lines << " 'results.csv' using 1:#{indices.index(t)+2} with lines title #{t}"
|
|
147
|
+
end
|
|
148
|
+
f.puts "set term png"
|
|
149
|
+
f.puts "set out 'result-#{q}.png'"
|
|
150
|
+
f.puts "set xlabel 'Number of nodes'"
|
|
151
|
+
f.puts "set ylabel 'Time in seconds (real)'"
|
|
152
|
+
f.puts "set logscale xy"
|
|
153
|
+
f.puts "set title 'Dijkstras Shortest Path Algorithm on Networks of different degrees'"
|
|
154
|
+
f.puts "plot \\"
|
|
155
|
+
f.puts lines.join(",\\\n")
|
|
156
|
+
end
|
|
157
|
+
system "gnuplot result-#{q}.gp"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
__END__
|
|
163
|
+
|
|
164
|
+
nodes = make_graph(100, 4)
|
|
165
|
+
draw_graph(nodes, "100-4.ps")
|
|
166
|
+
nodes = make_graph(100, 10)
|
|
167
|
+
draw_graph(nodes, "100-10.ps")
|
|
168
|
+
nodes = make_graph(10, 10)
|
|
169
|
+
draw_graph(nodes, "10-10.ps")
|
|
170
|
+
nodes = make_graph(1000, 2)
|
|
171
|
+
draw_graph(nodes, "1000-2.ps")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
c_file = File.read("ext/priority_queue/CPriorityQueue/priority_queue.c")
|
|
2
|
+
rb_file = File.read("lib/priority_queue/ruby_priority_queue.rb")
|
|
3
|
+
|
|
4
|
+
c_comments = Hash.new { "" }
|
|
5
|
+
|
|
6
|
+
c_file.scan(%r(/\*(.*?)\*/\s*static\s+\w+\s*pq_(\w+)\(.*?\))m).each do | match |
|
|
7
|
+
c_comments[match[1]] = match[0].gsub(%r(\n\s*\* {0,1})m, "\n").strip
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
rb_comments = Hash.new { "" }
|
|
11
|
+
|
|
12
|
+
rb_file.scan(%r(((?:\n\s*#[^\n]*)*)\s*def\s+(\w+))m).each do | match |
|
|
13
|
+
rb_comments[match[1]] = match[0].gsub(%r(\n\s*# {0,1})m, "\n").strip
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_comments = Hash.new
|
|
17
|
+
|
|
18
|
+
(rb_comments.keys + c_comments.keys).uniq.each do | key |
|
|
19
|
+
#next if rb_comments[key].gsub(/\s+/m, " ") == c_comments[key].gsub(/\s+/m, " ")
|
|
20
|
+
if c_comments[key].empty?
|
|
21
|
+
add_comments[key] = rb_comments[key]
|
|
22
|
+
elsif rb_comments[key].empty?
|
|
23
|
+
add_comments[key] = c_comments[key]
|
|
24
|
+
elsif rb_comments[key] != c_comments[key]
|
|
25
|
+
|
|
26
|
+
puts key
|
|
27
|
+
puts "Ruby"
|
|
28
|
+
puts rb_comments[key]
|
|
29
|
+
puts "C"
|
|
30
|
+
puts c_comments[key]
|
|
31
|
+
puts
|
|
32
|
+
puts "Choose [c,r]"
|
|
33
|
+
1 until /^([cr])/ =~ gets
|
|
34
|
+
add_comments[key] = ($1 == "c" ? c_comments : rb_comments)[key]
|
|
35
|
+
puts "-" * 80
|
|
36
|
+
puts
|
|
37
|
+
else
|
|
38
|
+
add_comments[key] = rb_comments[key]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
File.open("lib/priority_queue/ruby_priority_queue.new.rb", "wb") do | o |
|
|
44
|
+
o <<
|
|
45
|
+
rb_file.gsub(%r(((?:\n\s*#[^\n]*)*)(\s*def\s+(\w+)))m) do | match |
|
|
46
|
+
name, all = $3, $2
|
|
47
|
+
"\n" + (add_comments[name].gsub(/^/, "#")) + all
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# A priority queue implementation.
|
|
2
|
+
# This extension contains two implementations, a c extension and a pure ruby
|
|
3
|
+
# implementation. When the compiled extension can not be found, it falls back
|
|
4
|
+
# to the pure ruby extension.
|
|
5
|
+
#
|
|
6
|
+
# See CPriorityQueue and RubyPriorityQueue for more information.
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
require 'priority_queue/CPriorityQueue'
|
|
10
|
+
PriorityQueue = CPriorityQueue
|
|
11
|
+
rescue LoadError # C Version could not be found, try ruby version
|
|
12
|
+
require 'priority_queue/ruby_priority_queue'
|
|
13
|
+
PriorityQueue = RubyPriorityQueue
|
|
14
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "priority_queue/CPriorityQueue"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# A Poor mans Priority Queue. (Very inefficent but minimal implemention).
|
|
2
|
+
class PoorPriorityQueue < Hash
|
|
3
|
+
def push(object, priority)
|
|
4
|
+
self[object] = priority
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def min
|
|
8
|
+
return nil if self.empty?
|
|
9
|
+
min_k = self.keys.first
|
|
10
|
+
min_p = self[min_k]
|
|
11
|
+
self.each do | k, p |
|
|
12
|
+
min_k, min_p = k, p if p < min_p
|
|
13
|
+
end
|
|
14
|
+
[min_k, min_p]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def min_key
|
|
18
|
+
min[0] rescue nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def min_priority
|
|
22
|
+
min[1] rescue nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete_min
|
|
26
|
+
return nil if self.empty?
|
|
27
|
+
min_k, min_p = *min
|
|
28
|
+
self.delete(min_k)
|
|
29
|
+
[min_k, min_p]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete_min_return_key
|
|
33
|
+
delete_min[0] rescue nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete_min_return_priority
|
|
37
|
+
delete_min[1] rescue nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def delete(object)
|
|
41
|
+
return nil unless self.has_key?(object)
|
|
42
|
+
result = [object, self[object]]
|
|
43
|
+
super
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
end
|