ruleby 0.1 → 0.2
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/benchmarks/50_joined_rules.rb +78 -0
- data/benchmarks/50_rules.rb +57 -0
- data/benchmarks/5_joined_rules.rb +78 -0
- data/benchmarks/5_rules.rb +57 -0
- data/benchmarks/miss_manners/data.rb +135 -0
- data/benchmarks/miss_manners/miss_manners.rb +23 -0
- data/benchmarks/miss_manners/model.rb +182 -0
- data/benchmarks/miss_manners/rules.rb +165 -0
- data/examples/example_diagnosis.rb +155 -0
- data/examples/example_hello.rb +48 -0
- data/examples/example_politician.rb +86 -0
- data/examples/example_ticket.rb +158 -0
- data/examples/fibonacci_example1.rb +33 -0
- data/examples/fibonacci_example2.rb +29 -0
- data/examples/fibonacci_rulebook.rb +137 -0
- data/examples/test_self_reference.rb +35 -0
- data/lib/core/atoms.rb +13 -71
- data/lib/core/engine.rb +47 -9
- data/lib/core/nodes.rb +487 -548
- data/lib/core/patterns.rb +0 -110
- data/lib/core/utils.rb +120 -184
- data/lib/rulebook.rb +237 -157
- data/lib/ruleby.rb +0 -20
- metadata +28 -11
data/lib/core/patterns.rb
CHANGED
@@ -103,115 +103,5 @@ module Ruleby
|
|
103
103
|
end
|
104
104
|
end
|
105
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
106
|
end
|
217
107
|
end
|
data/lib/core/utils.rb
CHANGED
@@ -1,100 +1,32 @@
|
|
1
1
|
module Ruleby
|
2
2
|
module Core
|
3
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
4
|
# 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
|
-
|
80
|
-
|
5
|
+
# many-to-many. This class allows for quick access of both key and value. It
|
6
|
+
# is similar to Multimap in C++ standard lib.
|
7
|
+
class MultiHash
|
8
|
+
def initialize(key=nil, values=[])
|
81
9
|
@i = 0
|
82
10
|
clear
|
83
|
-
if
|
84
|
-
@
|
11
|
+
if key
|
12
|
+
@keys = {key => []}
|
85
13
|
values.each do |v|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
@
|
90
|
-
@values = {
|
91
|
-
@backward_hash = {
|
14
|
+
xref = generate_xref()
|
15
|
+
xref_list = @keys[key]
|
16
|
+
xref_list.push xref
|
17
|
+
@keys[key] = xref_list
|
18
|
+
@values = {xref => v}
|
19
|
+
@backward_hash = {xref => [key]}
|
92
20
|
end
|
93
21
|
end
|
94
22
|
end
|
95
23
|
|
24
|
+
def empty?
|
25
|
+
return @keys.empty?
|
26
|
+
end
|
27
|
+
|
96
28
|
def rehash
|
97
|
-
@
|
29
|
+
@keys.rehash
|
98
30
|
@values.rehash
|
99
31
|
@backward_hash.rehash
|
100
32
|
end
|
@@ -104,16 +36,16 @@ module Ruleby
|
|
104
36
|
end
|
105
37
|
|
106
38
|
def clear
|
107
|
-
@
|
39
|
+
@keys = {}
|
108
40
|
@values = {}
|
109
41
|
@backward_hash = {}
|
110
42
|
end
|
111
43
|
|
112
44
|
def values_by_id(id)
|
113
|
-
|
45
|
+
xrefs = @keys[id]
|
114
46
|
values = []
|
115
|
-
if
|
116
|
-
|
47
|
+
if xrefs
|
48
|
+
xrefs.each do |k|
|
117
49
|
values.push @values[k]
|
118
50
|
end
|
119
51
|
else
|
@@ -122,95 +54,103 @@ module Ruleby
|
|
122
54
|
return values
|
123
55
|
end
|
124
56
|
|
125
|
-
def
|
126
|
-
@
|
127
|
-
yield(
|
57
|
+
def each_key
|
58
|
+
@keys.each_key do |key|
|
59
|
+
yield(key)
|
128
60
|
end
|
129
61
|
end
|
130
62
|
|
131
|
-
def
|
132
|
-
return @
|
63
|
+
def has_key?(key)
|
64
|
+
return @keys.has_key?(key)
|
65
|
+
end
|
66
|
+
|
67
|
+
def key?(key)
|
68
|
+
return has_key?(key)
|
133
69
|
end
|
134
70
|
|
135
71
|
def +(dh)
|
136
|
-
# TODO this can be
|
72
|
+
# TODO this can be faster
|
137
73
|
new_dh = dh.dup
|
138
74
|
dh.concat self.dup
|
139
75
|
return new_dh
|
140
76
|
end
|
141
77
|
|
142
78
|
def add(ids,val)
|
143
|
-
|
79
|
+
xref = generate_xref()
|
144
80
|
ids.each do |id|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
@
|
81
|
+
xref_list = @keys[id]
|
82
|
+
xref_list = [] if xref_list == @keys.default
|
83
|
+
xref_list.push xref
|
84
|
+
@keys[id] = xref_list
|
149
85
|
end
|
150
|
-
@values[
|
151
|
-
@backward_hash[
|
86
|
+
@values[xref] = val
|
87
|
+
@backward_hash[xref] = ids
|
152
88
|
end
|
153
89
|
|
90
|
+
# WARN this method adds a value to the MultiHash only if it is unique. It
|
91
|
+
# can be a fairly costly operation, and should be avoided. We only
|
92
|
+
# implemented this as part of a hack to get things working early on.
|
154
93
|
def add_uniq(ids,val)
|
155
|
-
|
94
|
+
xref = generate_xref()
|
156
95
|
exist_list = []
|
157
96
|
ids.each do |id|
|
158
|
-
|
159
|
-
if
|
160
|
-
|
161
|
-
existing_val = @values[
|
97
|
+
xref_list = @keys[id]
|
98
|
+
if xref_list != @keys.default
|
99
|
+
xref_list.each do |existing_xref|
|
100
|
+
existing_val = @values[existing_xref]
|
162
101
|
if existing_val
|
163
102
|
if val == existing_val
|
164
|
-
|
103
|
+
xref = existing_xref
|
165
104
|
exist_list.push id
|
166
105
|
break
|
167
106
|
end
|
168
107
|
else
|
169
|
-
# HACK there shouldn't be any
|
108
|
+
# HACK there shouldn't be any xrefs like this in the
|
170
109
|
# hash to being with. Why are they there?
|
171
|
-
|
172
|
-
@
|
110
|
+
xref_list.delete(existing_xref)
|
111
|
+
@keys[id] = xref_list
|
173
112
|
end
|
174
113
|
end
|
175
114
|
end
|
176
115
|
end
|
177
116
|
add_list = ids - exist_list
|
178
117
|
add_list.each do |id|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
@
|
118
|
+
xref_list = @keys[id]
|
119
|
+
xref_list = [] if xref_list == @keys.default
|
120
|
+
xref_list.push xref
|
121
|
+
@keys[id] = xref_list
|
183
122
|
end
|
184
|
-
@values[
|
185
|
-
b_list = @backward_hash[
|
123
|
+
@values[xref] = val if exist_list.empty?
|
124
|
+
b_list = @backward_hash[xref]
|
186
125
|
if b_list
|
187
|
-
@backward_hash[
|
126
|
+
@backward_hash[xref] = b_list | ids
|
188
127
|
else
|
189
|
-
@backward_hash[
|
128
|
+
@backward_hash[xref] = ids
|
190
129
|
end
|
191
130
|
end
|
192
131
|
|
193
132
|
def each
|
194
|
-
@values.each do |
|
195
|
-
ids = @backward_hash[
|
133
|
+
@values.each do |xref,val|
|
134
|
+
ids = @backward_hash[xref]
|
196
135
|
yield(ids,val)
|
197
136
|
end
|
198
137
|
end
|
199
138
|
|
200
139
|
def each_internal
|
201
|
-
@values.each do |
|
202
|
-
ids = @backward_hash[
|
203
|
-
yield(ids,
|
140
|
+
@values.each do |xref,val|
|
141
|
+
ids = @backward_hash[xref]
|
142
|
+
yield(ids,xref,val)
|
204
143
|
end
|
205
144
|
end
|
206
145
|
private:each_internal
|
207
146
|
|
208
|
-
def concat(
|
209
|
-
|
147
|
+
def concat(multi_hash)
|
148
|
+
multi_hash.each do |ids,val|
|
210
149
|
add(ids,val)
|
211
150
|
end
|
212
151
|
end
|
213
152
|
|
153
|
+
# WARN see comments in add_uniq
|
214
154
|
def concat_uniq(double_hash)
|
215
155
|
double_hash.each do |ids,val|
|
216
156
|
add_uniq(ids,val)
|
@@ -222,58 +162,58 @@ module Ruleby
|
|
222
162
|
end
|
223
163
|
|
224
164
|
def remove(id)
|
225
|
-
|
226
|
-
if
|
165
|
+
xref_list = @keys.delete(id)
|
166
|
+
if xref_list != @keys.default
|
227
167
|
removed_values = []
|
228
|
-
|
229
|
-
value = @values.delete(
|
168
|
+
xref_list.each do |xref|
|
169
|
+
value = @values.delete(xref)
|
230
170
|
removed_values.push value
|
231
|
-
id_list = @backward_hash.delete(
|
171
|
+
id_list = @backward_hash.delete(xref)
|
232
172
|
id_list.each do |next_id|
|
233
|
-
remove_internal(next_id,
|
173
|
+
remove_internal(next_id,xref) if next_id != id
|
234
174
|
end
|
235
175
|
end
|
236
176
|
return removed_values
|
237
177
|
else
|
238
|
-
# puts 'WARN: tried to remove from
|
178
|
+
# puts 'WARN: tried to remove from MultiHash where id does not exist'
|
239
179
|
return default
|
240
180
|
end
|
241
181
|
end
|
242
182
|
|
243
|
-
def remove_internal(id,
|
244
|
-
|
245
|
-
if
|
246
|
-
|
247
|
-
if
|
248
|
-
@
|
183
|
+
def remove_internal(id,xref)
|
184
|
+
xref_list = @keys[id]
|
185
|
+
if xref_list # BUG this shouldn't be nil!
|
186
|
+
xref_list.delete(xref)
|
187
|
+
if xref_list.empty?
|
188
|
+
@keys.delete(id)
|
249
189
|
else
|
250
|
-
@
|
190
|
+
@keys[id] = xref_list
|
251
191
|
end
|
252
192
|
end
|
253
193
|
end
|
254
194
|
private:remove_internal
|
255
195
|
|
256
|
-
def
|
196
|
+
def remove_by_xref(ids,xref)
|
257
197
|
ids.each do |id|
|
258
|
-
|
259
|
-
|
260
|
-
if
|
261
|
-
@
|
198
|
+
xref_list = @keys[id]
|
199
|
+
xref_list.delete(xref)
|
200
|
+
if xref_list.empty?
|
201
|
+
@keys.delete(id)
|
262
202
|
else
|
263
|
-
@
|
203
|
+
@keys[id] = xref_list
|
264
204
|
end
|
265
205
|
end
|
266
|
-
@values.delete(
|
267
|
-
@backward_hash.delete(
|
206
|
+
@values.delete(xref)
|
207
|
+
@backward_hash.delete(xref)
|
268
208
|
end
|
269
|
-
private:
|
209
|
+
private:remove_by_xref
|
270
210
|
|
271
211
|
def delete_if
|
272
|
-
@values.delete_if do |
|
212
|
+
@values.delete_if do |xref,v|
|
273
213
|
if yield(v)
|
274
|
-
id_list = @backward_hash.delete(
|
214
|
+
id_list = @backward_hash.delete(xref)
|
275
215
|
id_list.each do |next_id|
|
276
|
-
remove_internal(next_id,
|
216
|
+
remove_internal(next_id,xref)
|
277
217
|
end
|
278
218
|
true
|
279
219
|
else
|
@@ -287,66 +227,66 @@ module Ruleby
|
|
287
227
|
end
|
288
228
|
|
289
229
|
def keys
|
290
|
-
return @
|
230
|
+
return @keys.keys
|
291
231
|
end
|
292
232
|
|
293
233
|
def dup
|
294
|
-
dup_mc =
|
234
|
+
dup_mc = MultiHash.new
|
295
235
|
each do |ids,v|
|
296
236
|
dup_mc.add ids, v.dup
|
297
237
|
end
|
298
238
|
return dup_mc
|
299
239
|
end
|
300
240
|
|
301
|
-
def
|
241
|
+
def generate_xref()
|
302
242
|
@i = @i + 1
|
303
243
|
return @i
|
304
244
|
end
|
305
|
-
private:
|
245
|
+
private:generate_xref
|
306
246
|
|
307
247
|
# This method is for testing. It ensures that all the Hash's
|
308
|
-
# and Array's are in order, and not corrupted (ex. some
|
309
|
-
# to a
|
248
|
+
# and Array's are in order, and not corrupted (ex. some key points
|
249
|
+
# to a xref that does not exist in the match_results Hash).
|
310
250
|
def valid?
|
311
|
-
@
|
312
|
-
#
|
313
|
-
# if
|
314
|
-
#
|
315
|
-
# id_list = @backward_hash[
|
251
|
+
@keys.each do |id,xrefs|
|
252
|
+
# xref_list = @keys[id]
|
253
|
+
# if xref_list != @keys.default
|
254
|
+
# xref_list.each do |xref|
|
255
|
+
# id_list = @backward_hash[xref]
|
316
256
|
# unless id_list
|
317
257
|
# puts 'yup'
|
318
258
|
# return false
|
319
259
|
# end
|
320
260
|
# end
|
321
261
|
# end
|
322
|
-
|
262
|
+
xrefs.each do |xref|
|
323
263
|
count = 0
|
324
|
-
|
325
|
-
if
|
264
|
+
xrefs.each do |xref2|
|
265
|
+
if xref == xref2
|
326
266
|
count = count + 1
|
327
267
|
if count > 1
|
328
|
-
puts '(0) Duplicate
|
268
|
+
puts '(0) Duplicate xrefs in entry for keys'
|
329
269
|
return false
|
330
270
|
end
|
331
271
|
end
|
332
272
|
end
|
333
273
|
|
334
|
-
mr = @match_results[
|
274
|
+
mr = @match_results[xref]
|
335
275
|
if mr == @match_results.default
|
336
|
-
puts '(1) Missing entry in @match_results for
|
276
|
+
puts '(1) Missing entry in @match_results for xref'
|
337
277
|
return false
|
338
278
|
end
|
339
279
|
|
340
|
-
# @match_results.each do |
|
341
|
-
# if other_mr == mr &&
|
280
|
+
# @match_results.each do |mr_xref,other_mr|
|
281
|
+
# if other_mr == mr && mr_xref != xref
|
342
282
|
# puts '(1a) Duplicate entry in @match_results'
|
343
283
|
# return false
|
344
284
|
# end
|
345
285
|
# end
|
346
286
|
|
347
|
-
id_list = @backward_hash[
|
287
|
+
id_list = @backward_hash[xref]
|
348
288
|
if id_list == @backward_hash.default
|
349
|
-
puts '(2) Missing entry in backward_hash for
|
289
|
+
puts '(2) Missing entry in backward_hash for xref'
|
350
290
|
return false
|
351
291
|
end
|
352
292
|
|
@@ -357,16 +297,16 @@ module Ruleby
|
|
357
297
|
|
358
298
|
id_list.each do |ref_id|
|
359
299
|
unless ref_id == id
|
360
|
-
|
361
|
-
if
|
362
|
-
puts '(4) Missing entry in
|
363
|
-
puts "#{id},#{mr},#{
|
300
|
+
ref_xref_list = @keys[ref_id]
|
301
|
+
if ref_xref_list == @keys.default
|
302
|
+
puts '(4) Missing entry in keys for backward_hash id'
|
303
|
+
puts "#{id},#{mr},#{xref},#{ref_id}"
|
364
304
|
return false
|
365
305
|
end
|
366
306
|
|
367
|
-
if
|
368
|
-
puts '(5) Entry in
|
369
|
-
puts "#{id},#{mr},#{
|
307
|
+
if ref_xref_list.index(xref) == nil
|
308
|
+
puts '(5) Entry in keys is missing xref'
|
309
|
+
puts "#{id},#{mr},#{xref},#{ref_id}"
|
370
310
|
return false
|
371
311
|
end
|
372
312
|
end
|
@@ -381,10 +321,6 @@ module Ruleby
|
|
381
321
|
# TODO need to implement this
|
382
322
|
return super
|
383
323
|
end
|
384
|
-
|
385
|
-
attr_reader :fact_ids
|
386
|
-
attr_reader :values
|
387
|
-
attr_reader :backward_hash
|
388
324
|
end
|
389
325
|
end
|
390
326
|
end
|