fsm 0.0.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/gemspec.rb +23 -0
- data/install.rb +206 -0
- data/instance_exec.rb +26 -0
- data/lib/fsm-0.0.0.rb +78 -0
- data/lib/fsm-0.0.0/dsl.rb +71 -0
- data/lib/fsm-0.0.0/event.rb +68 -0
- data/lib/fsm-0.0.0/fsm.rb +211 -0
- data/lib/fsm-0.0.0/fsm.rb.bak +366 -0
- data/lib/fsm-0.0.0/graph/base_extensions.rb +70 -0
- data/lib/fsm-0.0.0/graph/directed_graph.rb +481 -0
- data/lib/fsm-0.0.0/graph/graphviz_dot.rb +188 -0
- data/lib/fsm-0.0.0/observer.rb +194 -0
- data/lib/fsm-0.0.0/system.rb +65 -0
- data/lib/fsm-0.0.0/util.rb +172 -0
- data/lib/fsm.rb +78 -0
- data/sample/a.rb +81 -0
- metadata +58 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
class Array
|
2
|
+
def equality_uniq
|
3
|
+
uniq_elements = []
|
4
|
+
self.each {|e| uniq_elements.push(e) unless uniq_elements.index(e)}
|
5
|
+
uniq_elements
|
6
|
+
end
|
7
|
+
|
8
|
+
def delete_at_indices(indices = [])
|
9
|
+
not_deleted = Array.new
|
10
|
+
self.each_with_index {|e,i| not_deleted.push(e) if !indices.include?(i)}
|
11
|
+
not_deleted
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class DefaultInitArray < Array
|
16
|
+
def initialize(*args, &initblock)
|
17
|
+
super(*args)
|
18
|
+
@initblock = initblock
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](index)
|
22
|
+
super(index) || (self[index] = @initblock.call(index))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class ArrayOfArrays < DefaultInitArray
|
27
|
+
@@create_array = proc{|i| Array.new}
|
28
|
+
def initialize(*args)
|
29
|
+
super(*args, &@@create_array)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class ArrayOfHashes < DefaultInitArray
|
34
|
+
@@create_hash = proc{|i| Hash.new}
|
35
|
+
def initialize(*args)
|
36
|
+
super(*args, &@@create_hash)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Hash which takes a block that is called to give a default value when a key
|
41
|
+
# has the value nil in the hash.
|
42
|
+
class DefaultInitHash < Hash
|
43
|
+
def initialize(*args, &initblock)
|
44
|
+
super(*args)
|
45
|
+
@initblock = initblock
|
46
|
+
end
|
47
|
+
|
48
|
+
def [](key)
|
49
|
+
super(key) || (self[key] = @initblock.call(key))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
unless Object.constants.include?("TimesClass")
|
54
|
+
TimesClass = (RUBY_VERSION < "1.7") ? Time : Process
|
55
|
+
end
|
56
|
+
|
57
|
+
def time_and_puts(string, &block)
|
58
|
+
if $TIME_AND_PUTS_VERBOSE
|
59
|
+
print string; STDOUT.flush
|
60
|
+
end
|
61
|
+
starttime = [Time.new, TimesClass.times]
|
62
|
+
block.call
|
63
|
+
endtime = [Time.new, TimesClass.times]
|
64
|
+
duration = endtime[0] - starttime[0]
|
65
|
+
begin
|
66
|
+
load = [((endtime[1].utime+endtime[1].stime)-(starttime[1].utime+starttime[1].stime))/duration*100.0, 100.0].min
|
67
|
+
puts " (%.2f s %.2f%%)" % [duration, load] if $TIME_AND_PUTS_VERBOSE
|
68
|
+
rescue FloatDomainError
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,481 @@
|
|
1
|
+
require 'graph/base_extensions'
|
2
|
+
require 'graph/graphviz_dot'
|
3
|
+
|
4
|
+
class HashOfHash < DefaultInitHash
|
5
|
+
def initialize(&initBlock)
|
6
|
+
super do
|
7
|
+
if initBlock
|
8
|
+
DefaultInitHash.new(&initBlock)
|
9
|
+
else
|
10
|
+
Hash.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
GraphLink = Struct.new("GraphLink", :from, :to, :info)
|
17
|
+
class GraphLink
|
18
|
+
def inspect
|
19
|
+
info_str = info ? info.inspect + "-" : ""
|
20
|
+
"#{from.inspect}-#{info_str}>#{to.inspect}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class GraphTraversalException < Exception
|
25
|
+
attr_reader :node, :links, :link_info
|
26
|
+
def initialize(node, links, linkInfo)
|
27
|
+
@node, @links, @link_info = node, links, linkInfo
|
28
|
+
super(message)
|
29
|
+
end
|
30
|
+
|
31
|
+
def message
|
32
|
+
"There is no link from #{@node.inspect} having info #{@link_info.inspect} (valid links are #{@links.inspect})"
|
33
|
+
end
|
34
|
+
alias inspect message
|
35
|
+
end
|
36
|
+
|
37
|
+
class DirectedGraph
|
38
|
+
# This is a memory expensive variant that manages several additional
|
39
|
+
# information data structures to cut down on processing when the graph
|
40
|
+
# has been built.
|
41
|
+
|
42
|
+
attr_reader :links
|
43
|
+
attr_reader :link_map
|
44
|
+
attr_reader :is_root
|
45
|
+
attr_reader :is_leaf
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
@link_map = HashOfHash.new {Array.new} # [from][to] -> array of links
|
49
|
+
@link_map = Hash.new{|h,from| h[from] = Hash.new{|h2,to| h2[to] = []}}
|
50
|
+
@links = Array.new # All links in one array
|
51
|
+
@is_root = Hash.new # true iff root node
|
52
|
+
@is_leaf = Hash.new # true iff leaf node
|
53
|
+
end
|
54
|
+
|
55
|
+
def nodes
|
56
|
+
@is_root.keys
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_node(node)
|
60
|
+
unless include_node?(node)
|
61
|
+
@is_root[node] = @is_leaf[node] = true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def root?(node)
|
66
|
+
@is_root[node]
|
67
|
+
end
|
68
|
+
|
69
|
+
def leaf?(node)
|
70
|
+
@is_leaf[node]
|
71
|
+
end
|
72
|
+
|
73
|
+
def include_node?(node)
|
74
|
+
@is_root.has_key?(node)
|
75
|
+
end
|
76
|
+
|
77
|
+
def links_from_to(from, to)
|
78
|
+
@link_map[from][to]
|
79
|
+
end
|
80
|
+
|
81
|
+
def links_from(node)
|
82
|
+
@link_map[node].map {|to, links| links}.flatten
|
83
|
+
end
|
84
|
+
|
85
|
+
def children(node)
|
86
|
+
@link_map[node].keys.select {|k| @link_map[node][k].length > 0}
|
87
|
+
end
|
88
|
+
|
89
|
+
# (Forced) add link will always add link even if there are already links
|
90
|
+
# between the nodes.
|
91
|
+
def add_link(from, to, informationOnLink = nil)
|
92
|
+
add_link_nodes(from, to)
|
93
|
+
link = GraphLink.new(from, to, informationOnLink)
|
94
|
+
links_from_to(from, to).push link
|
95
|
+
#@link_map[from][to].push link
|
96
|
+
add_to_links(link)
|
97
|
+
link
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_link_nodes(from, to)
|
101
|
+
add_node(from)
|
102
|
+
add_node(to)
|
103
|
+
@is_leaf[from] = @is_root[to] = false
|
104
|
+
end
|
105
|
+
|
106
|
+
# Add link if not already linked
|
107
|
+
def link_nodes(from, to, info = nil)
|
108
|
+
links_from_to?(from, to) ? nil : add_link(from, to, info)
|
109
|
+
end
|
110
|
+
|
111
|
+
def links_from_to?(from, to)
|
112
|
+
not links_from_to(from, to).empty?
|
113
|
+
end
|
114
|
+
alias linked? links_from_to?
|
115
|
+
|
116
|
+
def add_to_links(link)
|
117
|
+
@links.push link
|
118
|
+
end
|
119
|
+
private :add_to_links
|
120
|
+
|
121
|
+
def each_reachable_node_once_depth_first(node, inclusive = true, &block)
|
122
|
+
children(node).each do |c|
|
123
|
+
recurse_each_reachable_depth_first_visited(c, Hash.new, &block)
|
124
|
+
end
|
125
|
+
block.call(node) if inclusive
|
126
|
+
end
|
127
|
+
alias each_reachable_node each_reachable_node_once_depth_first
|
128
|
+
|
129
|
+
def recurse_each_reachable_depth_first_visited(node, visited, &block)
|
130
|
+
visited[node] = true
|
131
|
+
children(node).each do |c|
|
132
|
+
unless visited[c]
|
133
|
+
recurse_each_reachable_depth_first_visited(c, visited, &block)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
block.call(node)
|
137
|
+
end
|
138
|
+
|
139
|
+
def each_reachable_node_once_breadth_first(node, inclusive = true, &block)
|
140
|
+
block.call(node) if inclusive
|
141
|
+
children(node).each do |c|
|
142
|
+
recurse_each_reachable_breadth_first_visited(c, Hash.new, &block)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
alias each_reachable_node each_reachable_node_once_depth_first
|
146
|
+
|
147
|
+
def recurse_each_reachable_breadth_first_visited(node, visited, &block)
|
148
|
+
visited[node] = true
|
149
|
+
block.call(node)
|
150
|
+
children(node).each do |c|
|
151
|
+
unless visited[c]
|
152
|
+
recurse_each_reachable_breadth_first_visited(c, visited, &block)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def root_nodes
|
158
|
+
@is_root.reject {|key,val| val == false}.keys
|
159
|
+
end
|
160
|
+
alias_method :roots, :root_nodes
|
161
|
+
|
162
|
+
def leaf_nodes
|
163
|
+
@is_leaf.reject {|key,val| val == false}.keys
|
164
|
+
end
|
165
|
+
alias_method :leafs, :leaf_nodes
|
166
|
+
|
167
|
+
def internal_node?(node)
|
168
|
+
!root?(node) and !leaf?(node)
|
169
|
+
end
|
170
|
+
|
171
|
+
def internal_nodes
|
172
|
+
nodes.reject {|n| root?(n) or leaf?(n)}
|
173
|
+
end
|
174
|
+
|
175
|
+
def recurse_cyclic?(node, visited)
|
176
|
+
visited[node] = true
|
177
|
+
children(node).each do |c|
|
178
|
+
return true if visited[c] || recurse_cyclic?(c, visited)
|
179
|
+
end
|
180
|
+
false
|
181
|
+
end
|
182
|
+
|
183
|
+
def cyclic?
|
184
|
+
visited = Hash.new
|
185
|
+
root_nodes.each {|root| return true if recurse_cyclic?(root, visited)}
|
186
|
+
false
|
187
|
+
end
|
188
|
+
|
189
|
+
def acyclic?
|
190
|
+
not cyclic?
|
191
|
+
end
|
192
|
+
|
193
|
+
def transition(state, linkInfo)
|
194
|
+
link = links_from(state).detect {|l| l.info == linkInfo}
|
195
|
+
begin
|
196
|
+
link.to
|
197
|
+
rescue Exception
|
198
|
+
raise GraphTraversalException.new(state, links_from(state), linkInfo)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def traverse(fromState, alongLinksWithInfo = [])
|
203
|
+
state, len = fromState, alongLinksWithInfo.length
|
204
|
+
alongLinksWithInfo = alongLinksWithInfo.clone
|
205
|
+
while len > 0
|
206
|
+
state = transition(state, alongLinksWithInfo.shift)
|
207
|
+
len -= 1
|
208
|
+
end
|
209
|
+
state
|
210
|
+
end
|
211
|
+
|
212
|
+
def to_dot(nodeShaper = nil, nodeLabeler = nil, linkLabeler = nil)
|
213
|
+
dgp = DotGraphPrinter.new(links, nodes)
|
214
|
+
dgp.node_shaper = nodeShaper if nodeShaper
|
215
|
+
dgp.node_labeler = nodeLabeler if nodeLabeler
|
216
|
+
dgp.link_labeler = linkLabeler if linkLabeler
|
217
|
+
dgp
|
218
|
+
end
|
219
|
+
|
220
|
+
def to_postscript_file(filename, nodeShaper = nil, nodeLabeler = nil,
|
221
|
+
linkLabeler = nil)
|
222
|
+
to_dot(nodeShaper, nodeLabeler, linkLabeler).write_to_file(filename)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Floyd-Warshal algorithm which should be O(n^3) where n is the number of
|
226
|
+
# nodes. We can probably work a bit on the constant factors!
|
227
|
+
def transitive_closure_floyd_warshal
|
228
|
+
vertices = nodes
|
229
|
+
tcg = DirectedGraph.new
|
230
|
+
num_nodes = vertices.length
|
231
|
+
|
232
|
+
# Direct links
|
233
|
+
for k in (0...num_nodes)
|
234
|
+
for s in (0...num_nodes)
|
235
|
+
vk, vs = vertices[k], vertices[s]
|
236
|
+
if vk == vs
|
237
|
+
tcg.link_nodes(vk,vs)
|
238
|
+
elsif linked?(vk, vs)
|
239
|
+
tcg.link_nodes(vk,vs)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Indirect links
|
245
|
+
for i in (0...num_nodes)
|
246
|
+
for j in (0...num_nodes)
|
247
|
+
for k in (0...num_nodes)
|
248
|
+
vi, vj, vk = vertices[i], vertices[j], vertices[k]
|
249
|
+
if not tcg.linked?(vi,vj)
|
250
|
+
tcg.link_nodes(vi, vj) if linked?(vi,vk) and linked?(vk,vj)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
tcg
|
256
|
+
end
|
257
|
+
alias_method :transitive_closure, :transitive_closure_floyd_warshal
|
258
|
+
|
259
|
+
def num_vertices
|
260
|
+
@is_root.size
|
261
|
+
end
|
262
|
+
alias num_nodes num_vertices
|
263
|
+
|
264
|
+
# strongly_connected_components uses the algorithm described in
|
265
|
+
# following paper.
|
266
|
+
# @Article{Tarjan:1972:DFS,
|
267
|
+
# author = "R. E. Tarjan",
|
268
|
+
# key = "Tarjan",
|
269
|
+
# title = "Depth First Search and Linear Graph Algorithms",
|
270
|
+
# journal = "SIAM Journal on Computing",
|
271
|
+
# volume = "1",
|
272
|
+
# number = "2",
|
273
|
+
# pages = "146--160",
|
274
|
+
# month = jun,
|
275
|
+
# year = "1972",
|
276
|
+
# CODEN = "SMJCAT",
|
277
|
+
# ISSN = "0097-5397 (print), 1095-7111 (electronic)",
|
278
|
+
# bibdate = "Thu Jan 23 09:56:44 1997",
|
279
|
+
# bibsource = "Parallel/Multi.bib, Misc/Reverse.eng.bib",
|
280
|
+
# }
|
281
|
+
def strongly_connected_components
|
282
|
+
order_cell = [0]
|
283
|
+
order_hash = {}
|
284
|
+
node_stack = []
|
285
|
+
components = []
|
286
|
+
|
287
|
+
order_hash.default = -1
|
288
|
+
|
289
|
+
nodes.each {|node|
|
290
|
+
if order_hash[node] == -1
|
291
|
+
recurse_strongly_connected_components(node, order_cell, order_hash, node_stack, components)
|
292
|
+
end
|
293
|
+
}
|
294
|
+
|
295
|
+
components
|
296
|
+
end
|
297
|
+
|
298
|
+
def recurse_strongly_connected_components(node, order_cell, order_hash, node_stack, components)
|
299
|
+
order = (order_cell[0] += 1)
|
300
|
+
reachable_minimum_order = order
|
301
|
+
order_hash[node] = order
|
302
|
+
stack_length = node_stack.length
|
303
|
+
node_stack << node
|
304
|
+
|
305
|
+
links_from(node).each {|link|
|
306
|
+
nextnode = link.to
|
307
|
+
nextorder = order_hash[nextnode]
|
308
|
+
if nextorder != -1
|
309
|
+
if nextorder < reachable_minimum_order
|
310
|
+
reachable_minimum_order = nextorder
|
311
|
+
end
|
312
|
+
else
|
313
|
+
sub_minimum_order = recurse_strongly_connected_components(nextnode, order_cell, order_hash, node_stack, components)
|
314
|
+
if sub_minimum_order < reachable_minimum_order
|
315
|
+
reachable_minimum_order = sub_minimum_order
|
316
|
+
end
|
317
|
+
end
|
318
|
+
}
|
319
|
+
|
320
|
+
if order == reachable_minimum_order
|
321
|
+
scc = node_stack[stack_length .. -1]
|
322
|
+
node_stack[stack_length .. -1] = []
|
323
|
+
components << scc
|
324
|
+
scc.each {|n|
|
325
|
+
order_hash[n] = num_vertices
|
326
|
+
}
|
327
|
+
end
|
328
|
+
return reachable_minimum_order;
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
# Parallel propagation in directed acyclic graphs. Should be faster than
|
333
|
+
# traversing all links from each start node if the graph is dense so that
|
334
|
+
# many traversals can be merged.
|
335
|
+
class DagPropagator
|
336
|
+
def initialize(directedGraph, startNodes, &propagationBlock)
|
337
|
+
@graph, @block = directedGraph, propagationBlock
|
338
|
+
init_start_nodes(startNodes)
|
339
|
+
@visited = Hash.new
|
340
|
+
end
|
341
|
+
|
342
|
+
def init_start_nodes(startNodes)
|
343
|
+
@startnodes = startNodes
|
344
|
+
end
|
345
|
+
|
346
|
+
def propagate
|
347
|
+
@visited.clear
|
348
|
+
propagate_recursive
|
349
|
+
end
|
350
|
+
|
351
|
+
def propagate_recursive
|
352
|
+
next_start_nodes = Array.new
|
353
|
+
@startnodes.each do |parent|
|
354
|
+
@visited[parent] = true
|
355
|
+
@graph.children(parent).each do |child|
|
356
|
+
@block.call(parent, child)
|
357
|
+
unless @visited[child] or next_start_nodes.include?(child)
|
358
|
+
next_start_nodes.push(child)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
if next_start_nodes.length > 0
|
363
|
+
@startnodes = next_start_nodes
|
364
|
+
propagate_recursive
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Directed graph with fast traversal from children to parents (back)
|
370
|
+
class BackLinkedDirectedGraph < DirectedGraph
|
371
|
+
def initialize(*args)
|
372
|
+
super
|
373
|
+
@back_link_map = HashOfHash.new {Array.new} # [to][from] -> array of links
|
374
|
+
@incoming_links_info = DefaultInitHash.new {Array.new}
|
375
|
+
end
|
376
|
+
|
377
|
+
def add_link(from, to, informationOnLink = nil)
|
378
|
+
link = super
|
379
|
+
links_to_from(to, from).push link
|
380
|
+
if informationOnLink and
|
381
|
+
!@incoming_links_info[to].include?(informationOnLink)
|
382
|
+
@incoming_links_info[to].push informationOnLink
|
383
|
+
end
|
384
|
+
link
|
385
|
+
end
|
386
|
+
|
387
|
+
def incoming_links_info(node)
|
388
|
+
@incoming_links_info[node]
|
389
|
+
end
|
390
|
+
|
391
|
+
def back_transition(node, backLinkInfo)
|
392
|
+
link = links_to(node).detect {|l| l.info == backLinkInfo}
|
393
|
+
begin
|
394
|
+
link.from
|
395
|
+
rescue Exception
|
396
|
+
raise GraphTraversalException.new(node, links_to(node), backLinkInfo)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
def back_traverse(state, alongLinksWithInfo = [])
|
401
|
+
len = alongLinksWithInfo.length
|
402
|
+
alongLinksWithInfo = alongLinksWithInfo.clone
|
403
|
+
while len > 0
|
404
|
+
state = back_transition(state, alongLinksWithInfo.pop)
|
405
|
+
len -= 1
|
406
|
+
end
|
407
|
+
state
|
408
|
+
end
|
409
|
+
|
410
|
+
def links_to(node)
|
411
|
+
@back_link_map[node].map {|from, links| links}.flatten
|
412
|
+
end
|
413
|
+
|
414
|
+
protected
|
415
|
+
|
416
|
+
def links_to_from(to, from)
|
417
|
+
@back_link_map[to][from]
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def calc_masks(start, stop, masks = Array.new)
|
422
|
+
mask = 1 << start
|
423
|
+
(start..stop).each {|i| masks[i] = mask; mask <<= 1}
|
424
|
+
masks
|
425
|
+
end
|
426
|
+
|
427
|
+
class BooleanMatrix
|
428
|
+
def initialize(objects)
|
429
|
+
@index, @objects, @matrix = Hash.new, objects, Array.new
|
430
|
+
cnt = 0
|
431
|
+
objects.each do |o|
|
432
|
+
@index[o] = cnt
|
433
|
+
@matrix[cnt] = 0 # Use Integers to represent the booleans
|
434
|
+
cnt += 1
|
435
|
+
end
|
436
|
+
@num_obects = cnt
|
437
|
+
end
|
438
|
+
|
439
|
+
@@masks_max = 1000
|
440
|
+
@@masks = calc_masks(0,@@masks_max)
|
441
|
+
|
442
|
+
def mask(index)
|
443
|
+
mask = @@masks[index]
|
444
|
+
unless mask
|
445
|
+
calc_masks(@@masks_max+1, index, @@masks)
|
446
|
+
mask = @masks[index]
|
447
|
+
end
|
448
|
+
mask
|
449
|
+
end
|
450
|
+
|
451
|
+
def or(index1, index2)
|
452
|
+
@matrix[index1] |= @matrix[index2]
|
453
|
+
end
|
454
|
+
|
455
|
+
def indices(anInteger)
|
456
|
+
index = 0
|
457
|
+
while anInteger > 0
|
458
|
+
yeild(index) if anInteger & 1
|
459
|
+
anInteger >>= 1
|
460
|
+
index += 1
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def directed_graph
|
465
|
+
dg = Directedgraph.new
|
466
|
+
@matrix.each_with_index do |v,i|
|
467
|
+
indices(v) do |index|
|
468
|
+
dg.link_nodes(@objects[i], @objects[index])
|
469
|
+
end
|
470
|
+
end
|
471
|
+
dg
|
472
|
+
end
|
473
|
+
|
474
|
+
def transitive_closure
|
475
|
+
for i in (0..@num_obects)
|
476
|
+
for j in (0..@num_obects)
|
477
|
+
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|