ruleby 0.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.
@@ -0,0 +1,217 @@
1
+ module Ruleby
2
+ module Core
3
+
4
+ class Pattern
5
+ # a list of AtomTag objects
6
+ attr_reader :atom_tags
7
+ end
8
+
9
+ class ObjectPattern < Pattern
10
+
11
+ # 'head' is a class type
12
+ def initialize(tag, head, atoms)
13
+ init_vars tag, head, atoms
14
+ end
15
+
16
+ def init_vars(tag,head,atoms,match=true)
17
+ @head = head
18
+ @headatom = TypeAtom.new tag, :class do |t| t == head end
19
+ @atoms = [@headatom] + atoms
20
+ @atom_tags = @atoms.collect {|a| AtomTag.new a.tag, match}
21
+ end
22
+
23
+ attr_reader :head
24
+ attr_reader :atoms
25
+ attr_reader :atom_tags
26
+
27
+ def ==(pattern)
28
+ atoms = pattern.atoms
29
+ if(@atoms.size == atoms.size)
30
+ (0..@atoms.size).each do |i|
31
+ if !(@atoms[i] == atoms[i])
32
+ return false
33
+ end
34
+ end
35
+ return true
36
+ end
37
+ return false
38
+ end
39
+
40
+ def to_s
41
+ return '(' + @atoms.join('|') + ')'
42
+ end
43
+ end
44
+
45
+ class NotPattern < ObjectPattern
46
+ def initialize(tag, head, atoms)
47
+ init_vars tag, head, atoms, false
48
+ end
49
+
50
+ def ==(pattern)
51
+ return pattern.kind_of?(NotPattern) && super==(pattern)
52
+ end
53
+ end
54
+
55
+ class CompositePattern < Pattern
56
+
57
+ def initialize(left_pattern, right_pattern)
58
+ @left_pattern = left_pattern
59
+ @right_pattern = right_pattern
60
+ end
61
+
62
+ attr_reader :left_pattern, :right_pattern
63
+
64
+ def atoms
65
+ atoms = []
66
+ atoms.push @left_pattern.atoms
67
+ atoms.push @right_pattern.atoms
68
+ return atoms
69
+ end
70
+ end
71
+
72
+ class AndPattern < CompositePattern
73
+
74
+ def initialize(left_pattern, right_pattern)
75
+ super(left_pattern, right_pattern)
76
+ @head = :and
77
+ @atom_tags = left_pattern.atom_tags.concat(right_pattern.atom_tags)
78
+ end
79
+
80
+ end
81
+
82
+ class OrPattern < CompositePattern
83
+
84
+ def initialize(left_pattern, right_pattern)
85
+ super(left_pattern, right_pattern)
86
+ @head = :or
87
+ @atom_tags = [[left_pattern.atom_tags, right_pattern.atom_tags]]
88
+ end
89
+
90
+ end
91
+
92
+ class AtomTag
93
+ def initialize(tag, exists=true)
94
+ @tag = tag
95
+
96
+ # QUESTION can we get rid of this property? if so, we may not need this
97
+ # class at all.
98
+ @exists = exists
99
+ end
100
+ attr_reader :tag,:exists
101
+ def to_s
102
+ return "[#{@tag.to_s}]"
103
+ end
104
+ end
105
+
106
+ class MatchContext
107
+ def initialize(fact_id=nil, match_results=[])
108
+ @match_hash = DoubleHash.new fact_id, match_results
109
+ end
110
+
111
+ def clear
112
+ @match_hash = DoubleHash.new
113
+ end
114
+
115
+ def add(ids,mr)
116
+ @match_hash.add ids, mr
117
+ end
118
+
119
+ def add_uniq(ids,mr)
120
+ @match_hash.add_uniq ids, mr
121
+ end
122
+
123
+ def each_match_result
124
+ @match_hash.each do |ids,mr|
125
+ yield(ids,mr)
126
+ end
127
+ end
128
+
129
+ def each_fact_id
130
+ @match_hash.each_id do |id|
131
+ yield(id)
132
+ end
133
+ end
134
+
135
+ def contains?(mr)
136
+ return @match_hash.value?(mr)
137
+ end
138
+
139
+ def +(mc)
140
+ # TODO make better
141
+ new_mc = MatchContext.new
142
+ new_mc.match_hash = @match_hash + mc.match_hash
143
+ return new_mc
144
+ end
145
+
146
+ def concat(match_context)
147
+ @match_hash.concat match_context.match_hash
148
+ return self
149
+ end
150
+
151
+ def concat_uniq(match_context)
152
+ @match_hash.concat_uniq match_context.match_hash
153
+ return self
154
+ end
155
+
156
+ def default
157
+ return @match_hash.default
158
+ end
159
+
160
+ def remove(id)
161
+ return @match_hash.remove(id)
162
+ end
163
+
164
+ def delete_if
165
+ @match_hash.delete_if do |mr|
166
+ yield(mr)
167
+ end
168
+ end
169
+
170
+ def match_results
171
+ v = @match_hash.values
172
+ if v.kind_of?(Hash)
173
+ # QUESTION What the heck is going on here? Why did this return a Hash
174
+ # instead of an Array?
175
+ return v.values
176
+ end
177
+ return v
178
+ end
179
+
180
+ def dup
181
+ dup_mc = MatchContext.new
182
+ dup_mc.match_hash = @match_hash.dup
183
+ return dup_mc
184
+ end
185
+
186
+ def subset(fact_id)
187
+ mrs = @match_hash.values_by_id(fact_id)
188
+ return MatchContext.new(fact_id, mrs.dup)
189
+ end
190
+
191
+ def satisfied?
192
+ return !@match_hash.values.empty?
193
+ end
194
+
195
+ def to_s
196
+ s = '[MatchContext('
197
+ each_match_result do |ids,mr|
198
+ s = s + "[#{ids.join(',')}->#{mr}]"
199
+ end
200
+ s = s + ')]'
201
+ end
202
+
203
+ def ==(mc)
204
+ return false if mc == nil
205
+ mc.match_hash.keys.each do |id|
206
+ return false if @match_hash.keys.index(id) == nil
207
+ end
208
+ mc.match_results.values.each do |mr|
209
+ return false if match_results.values.index(mr) == nil
210
+ end
211
+ return true
212
+ end
213
+
214
+ attr :match_hash, true
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,390 @@
1
+ module Ruleby
2
+ module Core
3
+
4
+ # We use this custom structure for our data so that we can hash the nodes by
5
+ # the type they represent. This allows for faster results because we can limit
6
+ # the number of nodes we compare against. In addition, this class does some of
7
+ # the compiling of the rules when a node is added.
8
+ class NodeNetworkArray
9
+ def initialize
10
+ @nodes = {}
11
+ end
12
+
13
+ def each(clazz)
14
+ nodes = @nodes[clazz]
15
+ if nodes
16
+ nodes.each do |n|
17
+ yield n
18
+ end
19
+ end
20
+ end
21
+
22
+ def each_internal
23
+ @nodes.values.flatten.each do |n|
24
+ yield n
25
+ end
26
+ end
27
+ private:each_internal
28
+
29
+ def has_node(node)
30
+ @nodes[node.pattern.head].each do |node2|
31
+ if node2 == node
32
+ return true
33
+ end
34
+ end
35
+ return false
36
+ end
37
+
38
+ def add_internal(node)
39
+ nodes = @nodes[node.pattern.head]
40
+ nodes = [] unless nodes
41
+ nodes.push node
42
+ @nodes[node.pattern.head] = nodes
43
+ end
44
+ private:add_internal
45
+
46
+ # This method adds a node to our list. In addition, it creates bindings between
47
+ # this node and any nodes that it references or is referenced by
48
+ def add(object_node)
49
+ object_node.pattern.atoms.each do |atom|
50
+ if (atom.kind_of?(ReferenceAtom))
51
+ each_internal do |local_node|
52
+ added = false
53
+ atom.vars.each do |var|
54
+ local_node.pattern.atoms.each do |local_atom|
55
+ if (var == local_atom.tag)
56
+ atom.var_atoms[var] = local_atom
57
+ unless added
58
+ local_node.referenced_by.push object_node
59
+ added = true
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ unless atom
67
+ # TODO it would be better to throw this exception before the
68
+ # the rule is asserted
69
+ raise "Reference not found for: " + atom.tag.to_s
70
+ end
71
+ end
72
+ add_internal(object_node);
73
+ end
74
+
75
+ end
76
+
77
+ # This class is used when we need to have a Hash where keys and values are mapped
78
+ # many-to-many. This class allows for quick access of both key and value.
79
+ class DoubleHash
80
+ def initialize(fact_id=nil, values=[])
81
+ @i = 0
82
+ clear
83
+ if fact_id
84
+ @fact_ids = {fact_id => []}
85
+ values.each do |v|
86
+ key = generate_key()
87
+ key_list = @fact_ids[fact_id]
88
+ key_list.push key
89
+ @fact_ids[fact_id] = key_list
90
+ @values = {key => v}
91
+ @backward_hash = {key => [fact_id]}
92
+ end
93
+ end
94
+ end
95
+
96
+ def rehash
97
+ @fact_ids.rehash
98
+ @values.rehash
99
+ @backward_hash.rehash
100
+ end
101
+
102
+ def value?(mr)
103
+ @values.value?(mr)
104
+ end
105
+
106
+ def clear
107
+ @fact_ids = {}
108
+ @values = {}
109
+ @backward_hash = {}
110
+ end
111
+
112
+ def values_by_id(id)
113
+ keys = @fact_ids[id]
114
+ values = []
115
+ if keys
116
+ keys.each do |k|
117
+ values.push @values[k]
118
+ end
119
+ else
120
+ #???
121
+ end
122
+ return values
123
+ end
124
+
125
+ def each_id
126
+ @fact_ids.each_key do |id|
127
+ yield(id)
128
+ end
129
+ end
130
+
131
+ def has_id?(id)
132
+ return @fact_ids.has_key?(id)
133
+ end
134
+
135
+ def +(dh)
136
+ # TODO this can be much faster
137
+ new_dh = dh.dup
138
+ dh.concat self.dup
139
+ return new_dh
140
+ end
141
+
142
+ def add(ids,val)
143
+ key = generate_key()
144
+ ids.each do |id|
145
+ key_list = @fact_ids[id]
146
+ key_list = [] if key_list == @fact_ids.default
147
+ key_list.push key
148
+ @fact_ids[id] = key_list
149
+ end
150
+ @values[key] = val
151
+ @backward_hash[key] = ids
152
+ end
153
+
154
+ def add_uniq(ids,val)
155
+ key = generate_key()
156
+ exist_list = []
157
+ ids.each do |id|
158
+ key_list = @fact_ids[id]
159
+ if key_list != @fact_ids.default
160
+ key_list.each do |existing_key|
161
+ existing_val = @values[existing_key]
162
+ if existing_val
163
+ if val == existing_val
164
+ key = existing_key
165
+ exist_list.push id
166
+ break
167
+ end
168
+ else
169
+ # HACK there shouldn't be any keys like this in the
170
+ # hash to being with. Why are they there?
171
+ key_list.delete(existing_key)
172
+ @fact_ids[id] = key_list
173
+ end
174
+ end
175
+ end
176
+ end
177
+ add_list = ids - exist_list
178
+ add_list.each do |id|
179
+ key_list = @fact_ids[id]
180
+ key_list = [] if key_list == @fact_ids.default
181
+ key_list.push key
182
+ @fact_ids[id] = key_list
183
+ end
184
+ @values[key] = val if exist_list.empty?
185
+ b_list = @backward_hash[key]
186
+ if b_list
187
+ @backward_hash[key] = b_list | ids
188
+ else
189
+ @backward_hash[key] = ids
190
+ end
191
+ end
192
+
193
+ def each
194
+ @values.each do |key,val|
195
+ ids = @backward_hash[key]
196
+ yield(ids,val)
197
+ end
198
+ end
199
+
200
+ def each_internal
201
+ @values.each do |key,val|
202
+ ids = @backward_hash[key]
203
+ yield(ids,key,val)
204
+ end
205
+ end
206
+ private:each_internal
207
+
208
+ def concat(double_hash)
209
+ double_hash.each do |ids,val|
210
+ add(ids,val)
211
+ end
212
+ end
213
+
214
+ def concat_uniq(double_hash)
215
+ double_hash.each do |ids,val|
216
+ add_uniq(ids,val)
217
+ end
218
+ end
219
+
220
+ def default
221
+ return @values.default
222
+ end
223
+
224
+ def remove(id)
225
+ key_list = @fact_ids.delete(id)
226
+ if key_list != @fact_ids.default
227
+ removed_values = []
228
+ key_list.each do |key|
229
+ value = @values.delete(key)
230
+ removed_values.push value
231
+ id_list = @backward_hash.delete(key)
232
+ id_list.each do |next_id|
233
+ remove_internal(next_id,key) if next_id != id
234
+ end
235
+ end
236
+ return removed_values
237
+ else
238
+ # puts 'WARN: tried to remove from DoubleHash where id does not exist'
239
+ return default
240
+ end
241
+ end
242
+
243
+ def remove_internal(id,key)
244
+ key_list = @fact_ids[id]
245
+ if key_list # BUG this shouldn't be nil!
246
+ key_list.delete(key)
247
+ if key_list.empty?
248
+ @fact_ids.delete(id)
249
+ else
250
+ @fact_ids[id] = key_list
251
+ end
252
+ end
253
+ end
254
+ private:remove_internal
255
+
256
+ def remove_by_key(ids,key)
257
+ ids.each do |id|
258
+ key_list = @fact_ids[id]
259
+ key_list.delete(key)
260
+ if key_list.empty?
261
+ @fact_ids.delete(id)
262
+ else
263
+ @fact_ids[id] = key_list
264
+ end
265
+ end
266
+ @values.delete(key)
267
+ @backward_hash.delete(key)
268
+ end
269
+ private:remove_by_key
270
+
271
+ def delete_if
272
+ @values.delete_if do |key,v|
273
+ if yield(v)
274
+ id_list = @backward_hash.delete(key)
275
+ id_list.each do |next_id|
276
+ remove_internal(next_id,key)
277
+ end
278
+ true
279
+ else
280
+ false
281
+ end
282
+ end
283
+ end
284
+
285
+ def values
286
+ return @values.values
287
+ end
288
+
289
+ def keys
290
+ return @fact_ids.keys
291
+ end
292
+
293
+ def dup
294
+ dup_mc = DoubleHash.new
295
+ each do |ids,v|
296
+ dup_mc.add ids, v.dup
297
+ end
298
+ return dup_mc
299
+ end
300
+
301
+ def generate_key()
302
+ @i = @i + 1
303
+ return @i
304
+ end
305
+ private:generate_key
306
+
307
+ # This method is for testing. It ensures that all the Hash's
308
+ # and Array's are in order, and not corrupted (ex. some fact_id points
309
+ # to a key that does not exist in the match_results Hash).
310
+ def valid?
311
+ @fact_ids.each do |id,keys|
312
+ # key_list = @fact_ids[id]
313
+ # if key_list != @fact_ids.default
314
+ # key_list.each do |key|
315
+ # id_list = @backward_hash[key]
316
+ # unless id_list
317
+ # puts 'yup'
318
+ # return false
319
+ # end
320
+ # end
321
+ # end
322
+ keys.each do |key|
323
+ count = 0
324
+ keys.each do |key2|
325
+ if key == key2
326
+ count = count + 1
327
+ if count > 1
328
+ puts '(0) Duplicate keys in entry for fact_ids'
329
+ return false
330
+ end
331
+ end
332
+ end
333
+
334
+ mr = @match_results[key]
335
+ if mr == @match_results.default
336
+ puts '(1) Missing entry in @match_results for key'
337
+ return false
338
+ end
339
+
340
+ # @match_results.each do |mr_key,other_mr|
341
+ # if other_mr == mr && mr_key != key
342
+ # puts '(1a) Duplicate entry in @match_results'
343
+ # return false
344
+ # end
345
+ # end
346
+
347
+ id_list = @backward_hash[key]
348
+ if id_list == @backward_hash.default
349
+ puts '(2) Missing entry in backward_hash for key'
350
+ return false
351
+ end
352
+
353
+ if id_list.index(id) == nil
354
+ puts '(3) Entry in backward_hash is missing id'
355
+ return false
356
+ end
357
+
358
+ id_list.each do |ref_id|
359
+ unless ref_id == id
360
+ ref_key_list = @fact_ids[ref_id]
361
+ if ref_key_list == @fact_ids.default
362
+ puts '(4) Missing entry in fact_ids for backward_hash id'
363
+ puts "#{id},#{mr},#{key},#{ref_id}"
364
+ return false
365
+ end
366
+
367
+ if ref_key_list.index(key) == nil
368
+ puts '(5) Entry in fact_ids is missing key'
369
+ puts "#{id},#{mr},#{key},#{ref_id}"
370
+ return false
371
+ end
372
+ end
373
+ end
374
+ end
375
+ end
376
+ return true
377
+ end
378
+ private:valid?
379
+
380
+ def ==(dh)
381
+ # TODO need to implement this
382
+ return super
383
+ end
384
+
385
+ attr_reader :fact_ids
386
+ attr_reader :values
387
+ attr_reader :backward_hash
388
+ end
389
+ end
390
+ end