omf_rete 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/README.md +182 -0
- data/Rakefile +14 -0
- data/lib/omf_rete/abstract_tuple_set.rb +68 -0
- data/lib/omf_rete/indexed_tuple_set.rb +129 -0
- data/lib/omf_rete/join_op.rb +113 -0
- data/lib/omf_rete/planner/abstract_plan.rb +57 -0
- data/lib/omf_rete/planner/filter_plan.rb +49 -0
- data/lib/omf_rete/planner/join_plan.rb +94 -0
- data/lib/omf_rete/planner/plan_builder.rb +302 -0
- data/lib/omf_rete/planner/plan_level_builder.rb +94 -0
- data/lib/omf_rete/planner/plan_set.rb +82 -0
- data/lib/omf_rete/planner/source_plan.rb +81 -0
- data/lib/omf_rete/store/alpha/alpha_element.rb +95 -0
- data/lib/omf_rete/store/alpha/alpha_inner_element.rb +96 -0
- data/lib/omf_rete/store/alpha/alpha_leaf_element.rb +41 -0
- data/lib/omf_rete/store/alpha/alpha_store.rb +197 -0
- data/lib/omf_rete/store.rb +57 -0
- data/lib/omf_rete/tuple_stream.rb +241 -0
- data/lib/omf_rete/version.rb +9 -0
- data/lib/omf_rete.rb +35 -0
- data/omf_rete.gemspec +24 -0
- data/tests/test.rb +8 -0
- data/tests/test_backtracking.rb +42 -0
- data/tests/test_filter.rb +77 -0
- data/tests/test_indexed_tuple_set.rb +58 -0
- data/tests/test_join_op.rb +50 -0
- data/tests/test_planner.rb +232 -0
- data/tests/test_store.rb +157 -0
- metadata +74 -0
@@ -0,0 +1,241 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module OMF::Rete
|
4
|
+
#
|
5
|
+
# This class provides functionality to process a
|
6
|
+
# stream of tuples.
|
7
|
+
#
|
8
|
+
class AbstractTupleStream
|
9
|
+
attr_accessor :source
|
10
|
+
attr_reader :description
|
11
|
+
|
12
|
+
def initialize(description, source = nil)
|
13
|
+
@description = description
|
14
|
+
@source = source
|
15
|
+
end
|
16
|
+
|
17
|
+
def index_for_binding(bname)
|
18
|
+
@description.find_index do |el|
|
19
|
+
el == bname
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return true if +tuple+ can be produced by this stream through the
|
24
|
+
# normal (+addTuple+) channels.
|
25
|
+
#
|
26
|
+
def check_for_tuple(tuple)
|
27
|
+
raise "Method 'check_for_tuple' is not implemented"
|
28
|
+
end
|
29
|
+
|
30
|
+
def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
|
31
|
+
out.write(" " * offset)
|
32
|
+
_describe(out, sep)
|
33
|
+
if @source
|
34
|
+
@source.describe(out, offset + incr, incr, sep)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# A processing tuple stream calls the associated processing block
|
41
|
+
# for every incoming tuple and forwards what is being returned by
|
42
|
+
# this block to the +receiver+. The return value of the block
|
43
|
+
# is assumed by a tuple as well. If the return value is nil,
|
44
|
+
# nothing is forwarded and the incoming tuple is essentially dropped.
|
45
|
+
#
|
46
|
+
class ProcessingTupleStream < AbstractTupleStream
|
47
|
+
attr_accessor :receiver
|
48
|
+
|
49
|
+
def initialize(project_pattern, out_description = project_pattern, in_description = nil, receiver = nil, &block)
|
50
|
+
@project_pattern = project_pattern
|
51
|
+
super out_description
|
52
|
+
@result_size = out_description.size
|
53
|
+
if in_description
|
54
|
+
self.inDescription = in_description
|
55
|
+
end
|
56
|
+
@receiver = receiver
|
57
|
+
@block = block
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_add(&block)
|
61
|
+
@block = block
|
62
|
+
end
|
63
|
+
|
64
|
+
def addTuple(tuple)
|
65
|
+
if @result_map
|
66
|
+
rtuple = @result_map.collect do |i| tuple[i] end
|
67
|
+
else
|
68
|
+
rtuple = tuple
|
69
|
+
end
|
70
|
+
result = @block ? @block.call(*rtuple) : rtuple
|
71
|
+
# if @block
|
72
|
+
# if (out = @block.call(*rtuple))
|
73
|
+
# unless out.kind_of?(Array) && out.size == @result_size
|
74
|
+
# raise "Expected block to return an array of size '#{@result_size}', but got '#{out.inspect}'"
|
75
|
+
# end
|
76
|
+
# @receiver.addTuple(out)
|
77
|
+
# end
|
78
|
+
# else
|
79
|
+
# @receiver.addTuple(rtuple)
|
80
|
+
# end
|
81
|
+
process_result(result, tuple)
|
82
|
+
end
|
83
|
+
|
84
|
+
def source=(source)
|
85
|
+
super
|
86
|
+
if source
|
87
|
+
self.inDescription = source.description
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def inDescription=(in_description)
|
92
|
+
if in_description
|
93
|
+
@result_map = @project_pattern.collect do |name|
|
94
|
+
index = in_description.find_index do |n2| name == n2 end
|
95
|
+
if index.nil?
|
96
|
+
raise "Unknown selector '#{name}'"
|
97
|
+
end
|
98
|
+
index
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def process_result(result, original_tuple)
|
106
|
+
if (result)
|
107
|
+
unless result.kind_of?(Array) && result.size == @result_size
|
108
|
+
raise "Expected block to return an array of size '#{@result_size}', but got '#{result.inspect}'"
|
109
|
+
end
|
110
|
+
@receiver.addTuple(result)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def _describe(out, sep )
|
115
|
+
out.write("processing#{sep}")
|
116
|
+
end
|
117
|
+
|
118
|
+
end # ProcessingTupleStream
|
119
|
+
|
120
|
+
# A result tuple stream calls the associated processing block
|
121
|
+
# for every incoming tuple.
|
122
|
+
#
|
123
|
+
# TODO: This should really be a subclass of +ProcessingTupleStream+, but
|
124
|
+
# we have supress_duplicates in this class which may be useful for
|
125
|
+
# +ProcessingTupleStream+ as well.
|
126
|
+
#
|
127
|
+
class ResultTupleStream < AbstractTupleStream
|
128
|
+
|
129
|
+
def initialize(description, supress_duplicates = true, &block)
|
130
|
+
super description
|
131
|
+
@block = block
|
132
|
+
if supress_duplicates
|
133
|
+
@results = Set.new
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def source=(source)
|
138
|
+
@source = source
|
139
|
+
if @source.description != @description
|
140
|
+
@result_map = @description.collect do |name|
|
141
|
+
index = @source.description.find_index do |n2| name == n2 end
|
142
|
+
if index.nil?
|
143
|
+
raise "Unknown selector '#{name}'"
|
144
|
+
end
|
145
|
+
index
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def addTuple(tuple)
|
151
|
+
if @result_map
|
152
|
+
rtuple = @result_map.collect do |i| tuple[i] end
|
153
|
+
else
|
154
|
+
rtuple = tuple
|
155
|
+
end
|
156
|
+
if @results
|
157
|
+
if @results.add?(rtuple)
|
158
|
+
@block.call(rtuple)
|
159
|
+
end
|
160
|
+
else
|
161
|
+
@block.call(rtuple)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Return true if +tuple+ can be produced by this stream. A
|
166
|
+
# +ResultStream+ only narrows a stream, so we need to
|
167
|
+
# potentially expand it (with nil) and pass it up to the
|
168
|
+
# +source+ of this stream.
|
169
|
+
#
|
170
|
+
def check_for_tuple(tuple)
|
171
|
+
if @sourcce
|
172
|
+
# should check if +tuple+ has the same size as description
|
173
|
+
if @result_map
|
174
|
+
# need to expand
|
175
|
+
unless @expand_map
|
176
|
+
@expand_map = @source.description.collect do |name|
|
177
|
+
index = @description.find_index do |n2| name == n2 end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
up_tuple = @expand_map.collect do |i| i nil? ? nil : tuple[i] end
|
181
|
+
else
|
182
|
+
up_tuple = tuple
|
183
|
+
end
|
184
|
+
@source.check_for_tuple(up_tuple)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
def _describe(out, sep )
|
191
|
+
out.write("out: [#{@description.join(', ')}]#{sep}")
|
192
|
+
end
|
193
|
+
end # ResultTupleStream
|
194
|
+
|
195
|
+
# A filtering tuple stream calls the associated processing block
|
196
|
+
# for every incoming tuple and forwards the incoming tuple if the
|
197
|
+
# the block returns true, otherwise it drops the tuple.
|
198
|
+
#
|
199
|
+
class FilterTupleStream < ProcessingTupleStream
|
200
|
+
|
201
|
+
def initialize(project_pattern, description = project_pattern, receiver = nil, &block)
|
202
|
+
super project_pattern, description, description, receiver, &block
|
203
|
+
end
|
204
|
+
|
205
|
+
# Return true if +tuple+ can be produced by this stream. For
|
206
|
+
# this we need to check first if it would pass this filter
|
207
|
+
# before we check if the source for this filter is being
|
208
|
+
# able to produce the tuple in question.
|
209
|
+
#
|
210
|
+
# TODO: This currently doesn't work for tuples with wild cards.
|
211
|
+
#
|
212
|
+
def check_for_tuple(tuple)
|
213
|
+
if @sourcce
|
214
|
+
# should check if +tuple+ has the same size as description
|
215
|
+
if @result_map
|
216
|
+
rtuple = @result_map.collect do |i| tuple[i] end
|
217
|
+
else
|
218
|
+
rtuple = tuple
|
219
|
+
end
|
220
|
+
if @block.call(*rtuple)
|
221
|
+
@source.check_for_tuple(tuple)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
def process_result(result, original_tuple)
|
229
|
+
if (result)
|
230
|
+
@receiver.addTuple(original_tuple)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
def _describe(out, sep )
|
236
|
+
out.write("filtering#{sep}")
|
237
|
+
end
|
238
|
+
|
239
|
+
end # class
|
240
|
+
end # module
|
241
|
+
|
data/lib/omf_rete.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
module OMF
|
5
|
+
module Rete
|
6
|
+
|
7
|
+
# Defines a filter on a tuple stream. The argument is either a variable
|
8
|
+
# number of binding variables with which the associated block is called.
|
9
|
+
# If the argument are two arrays, the first one holds the above described
|
10
|
+
# bindings for the block, while the second one describes the tuple returned
|
11
|
+
# by the block.
|
12
|
+
#
|
13
|
+
def self.filter(*projectPattern, &block)
|
14
|
+
require 'omf_rete/planner/filter_plan'
|
15
|
+
if projectPattern[0].kind_of? Array
|
16
|
+
if projectPattern.size != 2
|
17
|
+
raise "Wrong arguments for 'filter'. See documentation."
|
18
|
+
end
|
19
|
+
outDescription = projectPattern[1]
|
20
|
+
projectPattern = projectPattern[0]
|
21
|
+
else
|
22
|
+
outDescription = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
FilterPlan.new(projectPattern, outDescription, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.differ(binding1, binding2)
|
29
|
+
filter(binding1, binding2) do |b1, b2|
|
30
|
+
b1 != b2
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end # Filter
|
35
|
+
end # Moana
|
data/omf_rete.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "omf_rete/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "omf_rete"
|
7
|
+
# s.version = OmfWeb::VERSION
|
8
|
+
s.version = OMF::Rete::VERSION
|
9
|
+
s.authors = ["NICTA"]
|
10
|
+
s.email = ["omf-user@lists.nicta.com.au"]
|
11
|
+
s.homepage = "https://www.mytestbed.net"
|
12
|
+
s.summary = %q{A Rete implementation.}
|
13
|
+
s.description = %q{Tuple store with query and filter functionality.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "omf_rete"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- {bin,sbin}/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
# specify any dependencies here; for example:
|
23
|
+
# s.add_development_dependency "minitest", "~> 2.11.3"
|
24
|
+
end
|
data/tests/test.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
class TestBacktracking < Test::Unit::TestCase
|
3
|
+
|
4
|
+
def _test_plan(plan, storeSize, inTuples = nil, outTuples = nil, outPattern = nil, &requestProc)
|
5
|
+
store = Store.create(storeSize)
|
6
|
+
store.on_query &requestProc # proc to call if store gets a request for a tuple which doesn't exist
|
7
|
+
pb = PlanBuilder.new(plan, store, :backtracking => true)
|
8
|
+
pb.build
|
9
|
+
|
10
|
+
# pb.describe
|
11
|
+
|
12
|
+
resT = []
|
13
|
+
result = pb.materialize(outPattern) do |t|
|
14
|
+
resT << t
|
15
|
+
end
|
16
|
+
|
17
|
+
if (inTuples)
|
18
|
+
inTuples.each do |t|
|
19
|
+
store.addTuple(t)
|
20
|
+
end
|
21
|
+
assert_equal(outTuples, resT)
|
22
|
+
end
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
def x_test_request_one
|
27
|
+
plan = [
|
28
|
+
[:user?, :do, :action?],
|
29
|
+
[:pi, :endorses, :user?]
|
30
|
+
]
|
31
|
+
inT = [[:u1, :do, :start]]
|
32
|
+
resT = inT
|
33
|
+
_test_plan plan, 3, inT, resT do |*t|
|
34
|
+
puts ">>>>>>>>>>>"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_dummy
|
40
|
+
assert_equal(1, 1)
|
41
|
+
end
|
42
|
+
end # TestBacktracking
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'omf_rete'
|
2
|
+
require 'omf_rete/store'
|
3
|
+
require 'omf_rete/indexed_tuple_set'
|
4
|
+
require 'omf_rete/join_op'
|
5
|
+
require 'omf_rete/planner/plan_builder'
|
6
|
+
require 'stringio'
|
7
|
+
|
8
|
+
include OMF::Rete
|
9
|
+
include OMF::Rete::Planner
|
10
|
+
|
11
|
+
class TestFilter < Test::Unit::TestCase
|
12
|
+
|
13
|
+
|
14
|
+
def _test_plan(plan, storeSize, expected = nil, inTuples = nil, outTuples = nil, outPattern = nil)
|
15
|
+
store = Store.create(storeSize)
|
16
|
+
pb = PlanBuilder.new(plan, store)
|
17
|
+
pb.build
|
18
|
+
|
19
|
+
# pb.describe
|
20
|
+
|
21
|
+
resT = []
|
22
|
+
result = pb.materialize(outPattern) do |t|
|
23
|
+
resT << t
|
24
|
+
end
|
25
|
+
|
26
|
+
out = StringIO.new
|
27
|
+
#result.describe(out, 0, 0, '|')
|
28
|
+
result.describe(out)
|
29
|
+
assert_equal(expected, out.string) if expected
|
30
|
+
|
31
|
+
if (inTuples)
|
32
|
+
inTuples.each do |t|
|
33
|
+
store.addTuple(t)
|
34
|
+
end
|
35
|
+
assert_equal(outTuples, resT)
|
36
|
+
end
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_theshold_test
|
41
|
+
plan = [
|
42
|
+
[:x?],
|
43
|
+
OMF::Rete.filter(:x?) do |x|
|
44
|
+
x > 2
|
45
|
+
end
|
46
|
+
]
|
47
|
+
exp = %{\
|
48
|
+
out: [x?]
|
49
|
+
filtering
|
50
|
+
processing
|
51
|
+
}
|
52
|
+
inT = [[1], [2], [3], [4]]
|
53
|
+
resT = [[3], [4]]
|
54
|
+
_test_plan plan, 1, exp, inT, resT
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_theshold_test2
|
58
|
+
plan = [
|
59
|
+
[:x?, :y?],
|
60
|
+
OMF::Rete::filter(:x?) do |x|
|
61
|
+
x > 2
|
62
|
+
end,
|
63
|
+
OMF::Rete.filter(:y?) do |y|
|
64
|
+
y > 13
|
65
|
+
end
|
66
|
+
]
|
67
|
+
exp = %{\
|
68
|
+
out: [x?, y?]
|
69
|
+
filtering
|
70
|
+
filtering
|
71
|
+
processing
|
72
|
+
}
|
73
|
+
inT = [[1, 11], [2, 12], [3, 13], [4, 14]]
|
74
|
+
resT = [[4, 14]]
|
75
|
+
_test_plan plan, 2, exp, inT, resT
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'omf_rete/indexed_tuple_set'
|
2
|
+
|
3
|
+
include OMF::Rete
|
4
|
+
|
5
|
+
class TestIndexedTupleSet < Test::Unit::TestCase
|
6
|
+
def test_create_tset
|
7
|
+
IndexedTupleSet.new([:x?], [:x?])
|
8
|
+
IndexedTupleSet.new([:x?, nil, :y?], [:x?])
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_add_tuple
|
12
|
+
t = [:a, :b, :c]
|
13
|
+
ts = IndexedTupleSet.new([:x?, nil, nil], [:x?])
|
14
|
+
ts.addTuple(t)
|
15
|
+
assert_equal [t], ts.to_a
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_index0
|
19
|
+
t = [:a, :b, :c]
|
20
|
+
ts = IndexedTupleSet.new([:x?, nil, nil], [:x?])
|
21
|
+
ts.addTuple(t)
|
22
|
+
assert_equal [t], ts[[t[0]]].to_a
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def test_add_tuple_def_ts2
|
27
|
+
t1 = ['a', 'b', 'c'] # use strings as we need to sort tuple arrays
|
28
|
+
t2 = ['a', 'b', 'd']
|
29
|
+
ts = IndexedTupleSet.new([:x?], [:x?])
|
30
|
+
ts.addTuple(t1)
|
31
|
+
ts.addTuple(t2)
|
32
|
+
assert_equal [t1, t2].sort, ts.to_a.sort
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_index_pattern
|
36
|
+
t = [:a, :b, :c]
|
37
|
+
ts = IndexedTupleSet.new([:x?, :y?, :z?], [:y?, :x?])
|
38
|
+
ts.addTuple(t)
|
39
|
+
assert_equal [t], ts[[t[1], t[0]]].to_a
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_add_tuple_each
|
43
|
+
t1 = ['a', 'b', 'c'] # use strings as we need to sort tuple arrays
|
44
|
+
t2 = ['a', 'b', 'd']
|
45
|
+
ts = IndexedTupleSet.new([:x?, :y?, :z?], [:x?])
|
46
|
+
ts.addTuple(t1)
|
47
|
+
a = []
|
48
|
+
ts.on_add do |t|
|
49
|
+
a << t
|
50
|
+
end
|
51
|
+
assert_equal [t1], a
|
52
|
+
ts.addTuple(t2)
|
53
|
+
assert_equal [t1, t2], a
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'omf_rete/join_op'
|
2
|
+
|
3
|
+
include OMF::Rete
|
4
|
+
|
5
|
+
class TestJoinOP < Test::Unit::TestCase
|
6
|
+
def test_create_joinop
|
7
|
+
l = IndexedTupleSet.new([:x?], [:x?])
|
8
|
+
r = IndexedTupleSet.new([:x?], [:x?])
|
9
|
+
out = IndexedTupleSet.new([:x?], [:x?])
|
10
|
+
JoinOP.new(l, r, out)
|
11
|
+
end
|
12
|
+
|
13
|
+
# [:a :x?], [?x :c]
|
14
|
+
#
|
15
|
+
def test_join1
|
16
|
+
t1 = ['a', 'b']
|
17
|
+
t2 = ['b', 'd']
|
18
|
+
l = OMF::Rete::IndexedTupleSet.new([:a?, :x?], [:x?])
|
19
|
+
r = OMF::Rete::IndexedTupleSet.new([:x?, :b?], [:x?])
|
20
|
+
out = IndexedTupleSet.new([:a?, :b?], [:a?])
|
21
|
+
JoinOP.new(l, r, out)
|
22
|
+
l.addTuple(t1)
|
23
|
+
r.addTuple(t2)
|
24
|
+
assert_equal [[t1[0], t2[1]]], out.to_a
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_three_result_set
|
28
|
+
t1 = ['y', 'z', 'b', 'x']
|
29
|
+
t2 = ['c', 'd', 'x', 'b']
|
30
|
+
l = OMF::Rete::IndexedTupleSet.new([:y?, :z?, :b, :x?], [:x?])
|
31
|
+
r = OMF::Rete::IndexedTupleSet.new([:c, :d, :x?, :b], [:x?])
|
32
|
+
out = IndexedTupleSet.new([:x?, :y?, :z?], [:x?])
|
33
|
+
JoinOP.new(l, r, out)
|
34
|
+
l.addTuple(t1)
|
35
|
+
r.addTuple(t2)
|
36
|
+
assert_equal [['x', 'y', 'z']], out.to_a
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_join2
|
40
|
+
t1 = ['y', 'z', 'b', 'x']
|
41
|
+
t2 = ['c', 'd', 'x', 'y']
|
42
|
+
l = OMF::Rete::IndexedTupleSet.new([:y?, :z?, :b, :x?], [:x?, :y?])
|
43
|
+
r = OMF::Rete::IndexedTupleSet.new([:c, :d, :x?, :y?], [:x?, :y?])
|
44
|
+
out = IndexedTupleSet.new([:x?, :y?, :z?], [:x?])
|
45
|
+
JoinOP.new(l, r, out)
|
46
|
+
l.addTuple(t1)
|
47
|
+
r.addTuple(t2)
|
48
|
+
assert_equal [['x', 'y', 'z']], out.to_a
|
49
|
+
end
|
50
|
+
end
|