omf_rete 0.5
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/.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
|