ruleby 0.2 → 0.3
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/basic_rules.rb +66 -0
- data/benchmarks/joined_rules.rb +73 -0
- data/benchmarks/miss_manners/data.rb +11 -0
- data/benchmarks/miss_manners/miss_manners.rb +11 -1
- data/benchmarks/miss_manners/model.rb +11 -0
- data/benchmarks/miss_manners/rules.rb +49 -110
- data/benchmarks/model.rb +36 -0
- data/examples/example_diagnosis.rb +35 -73
- data/examples/example_hello.rb +20 -22
- data/examples/example_politician.rb +22 -11
- data/examples/example_ticket.rb +40 -85
- data/examples/fibonacci_example1.rb +13 -2
- data/examples/fibonacci_example2.rb +11 -0
- data/examples/fibonacci_rulebook.rb +58 -111
- data/examples/test_self_reference.rb +51 -9
- data/lib/core/atoms.rb +53 -116
- data/lib/core/engine.rb +96 -96
- data/lib/core/nodes.rb +330 -298
- data/lib/core/patterns.rb +36 -39
- data/lib/core/utils.rb +141 -3
- data/lib/dsl/ferrari.rb +263 -0
- data/lib/dsl/letigre.rb +212 -0
- data/lib/dsl/steel.rb +313 -0
- data/lib/rulebook.rb +82 -265
- data/lib/ruleby.rb +13 -0
- metadata +21 -19
- data/benchmarks/50_joined_rules.rb +0 -78
- data/benchmarks/50_rules.rb +0 -57
- data/benchmarks/5_joined_rules.rb +0 -78
- data/benchmarks/5_rules.rb +0 -57
data/lib/core/patterns.rb
CHANGED
@@ -1,28 +1,37 @@
|
|
1
|
+
# This file is part of the Ruleby project (http://ruleby.org)
|
2
|
+
#
|
3
|
+
# This application is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the Ruby license defined in the
|
5
|
+
# LICENSE.txt file.
|
6
|
+
#
|
7
|
+
# Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
|
8
|
+
#
|
9
|
+
# * Authors: Joe Kutner
|
10
|
+
#
|
11
|
+
|
1
12
|
module Ruleby
|
2
13
|
module Core
|
3
14
|
|
4
15
|
class Pattern
|
5
|
-
# a list of AtomTag objects
|
6
|
-
attr_reader :atom_tags
|
7
16
|
end
|
8
17
|
|
18
|
+
# This class represents a pattern that is looking for the existence of some
|
19
|
+
# object. It contains a list of 'atoms' that represent the properties of
|
20
|
+
# the class that we are looking for.
|
9
21
|
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
22
|
|
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
23
|
attr_reader :head
|
24
24
|
attr_reader :atoms
|
25
|
-
|
25
|
+
|
26
|
+
# 'deftemplate' is a class type
|
27
|
+
def initialize(tag, deftemplate, atoms)
|
28
|
+
init_vars tag, deftemplate, atoms
|
29
|
+
end
|
30
|
+
|
31
|
+
def init_vars(tag,deftemplate,atoms)
|
32
|
+
@head = TypeAtom.new tag, deftemplate
|
33
|
+
@atoms = [@head] + atoms
|
34
|
+
end
|
26
35
|
|
27
36
|
def ==(pattern)
|
28
37
|
atoms = pattern.atoms
|
@@ -42,24 +51,27 @@ module Ruleby
|
|
42
51
|
end
|
43
52
|
end
|
44
53
|
|
54
|
+
# This class represents a pattern that is looking for the absence of some
|
55
|
+
# object (rather than the existence of). In all respects, it is the same as
|
56
|
+
# an ObjectPattern, but it is handled differently by the inference engine.
|
45
57
|
class NotPattern < ObjectPattern
|
46
|
-
def initialize(tag, head, atoms)
|
47
|
-
init_vars tag, head, atoms, false
|
48
|
-
end
|
49
|
-
|
50
58
|
def ==(pattern)
|
51
59
|
return pattern.kind_of?(NotPattern) && super==(pattern)
|
52
60
|
end
|
53
61
|
end
|
54
62
|
|
63
|
+
# A composite pattern represents a logical conjunction of two patterns. The
|
64
|
+
# inference engine interprets this differently from an ObjectPattern because
|
65
|
+
# it simply aggregates patterns.
|
55
66
|
class CompositePattern < Pattern
|
67
|
+
|
68
|
+
attr_reader :left_pattern
|
69
|
+
attr_reader :right_pattern
|
56
70
|
|
57
71
|
def initialize(left_pattern, right_pattern)
|
58
72
|
@left_pattern = left_pattern
|
59
73
|
@right_pattern = right_pattern
|
60
|
-
end
|
61
|
-
|
62
|
-
attr_reader :left_pattern, :right_pattern
|
74
|
+
end
|
63
75
|
|
64
76
|
def atoms
|
65
77
|
atoms = []
|
@@ -73,8 +85,7 @@ module Ruleby
|
|
73
85
|
|
74
86
|
def initialize(left_pattern, right_pattern)
|
75
87
|
super(left_pattern, right_pattern)
|
76
|
-
@head = :and
|
77
|
-
@atom_tags = left_pattern.atom_tags.concat(right_pattern.atom_tags)
|
88
|
+
@head = :and
|
78
89
|
end
|
79
90
|
|
80
91
|
end
|
@@ -84,24 +95,10 @@ module Ruleby
|
|
84
95
|
def initialize(left_pattern, right_pattern)
|
85
96
|
super(left_pattern, right_pattern)
|
86
97
|
@head = :or
|
87
|
-
@atom_tags = [[left_pattern.atom_tags, right_pattern.atom_tags]]
|
88
98
|
end
|
89
99
|
|
90
100
|
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
|
101
|
+
|
105
102
|
|
106
103
|
end
|
107
104
|
end
|
data/lib/core/utils.rb
CHANGED
@@ -1,9 +1,145 @@
|
|
1
|
+
# This file is part of the Ruleby project (http://ruleby.org)
|
2
|
+
#
|
3
|
+
# This application is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the Ruby license defined in the
|
5
|
+
# LICENSE.txt file.
|
6
|
+
#
|
7
|
+
# Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
|
8
|
+
#
|
9
|
+
# * Authors: Joe Kutner
|
10
|
+
#
|
11
|
+
|
1
12
|
module Ruleby
|
2
13
|
module Core
|
3
14
|
|
4
|
-
# This class is
|
5
|
-
#
|
6
|
-
#
|
15
|
+
# This class is a wrapper for the context under which the network executes for
|
16
|
+
# for a given fact. It is essentially a wrapper for a fact and a partial
|
17
|
+
# match.
|
18
|
+
class MatchContext
|
19
|
+
|
20
|
+
attr_reader:fact
|
21
|
+
attr_reader:match
|
22
|
+
|
23
|
+
def initialize(fact,mr)
|
24
|
+
@fact = fact
|
25
|
+
@match = mr
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
return @match.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(t)
|
33
|
+
return t && @fact == t.fact && @match == t.match
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# This class represents a partial match. It contains the variables, values,
|
38
|
+
# and some metadata about the match. For the most part, this metadata is used
|
39
|
+
# during conflict resolution.
|
40
|
+
class MatchResult
|
41
|
+
# TODO this class needs to be cleaned up so that we don't have a bunch of
|
42
|
+
# properties. Instead, maybe it sould have a list of facts.
|
43
|
+
|
44
|
+
attr :variables, true
|
45
|
+
attr :is_match, true
|
46
|
+
attr :fact_hash, true
|
47
|
+
attr :recency, true
|
48
|
+
|
49
|
+
def initialize(variables=Hash.new,is_match=false,fact_hash={},recency=[])
|
50
|
+
@variables = variables
|
51
|
+
|
52
|
+
# a list of recencies of the facts that this matchresult depends on.
|
53
|
+
@recency = recency
|
54
|
+
|
55
|
+
# notes where this match result is from a NotPattern or ObjectPattern
|
56
|
+
# TODO this isn't really needed anymore. how can we get rid of it?
|
57
|
+
@is_match = is_match
|
58
|
+
|
59
|
+
# a hash of fact.ids that each tag corresponds to
|
60
|
+
@fact_hash = fact_hash
|
61
|
+
end
|
62
|
+
|
63
|
+
def []=(sym, object)
|
64
|
+
@variables[sym] = object
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](sym)
|
68
|
+
return @variables[sym]
|
69
|
+
end
|
70
|
+
|
71
|
+
def fact_ids
|
72
|
+
return fact_hash.values.uniq
|
73
|
+
end
|
74
|
+
|
75
|
+
def ==(match)
|
76
|
+
return match != nil && @variables == match.variables && @is_match == match.is_match && @fact_hash == match.fact_hash
|
77
|
+
end
|
78
|
+
|
79
|
+
def key?(m)
|
80
|
+
return @variables.key?(m)
|
81
|
+
end
|
82
|
+
|
83
|
+
def keys
|
84
|
+
return @variables.keys
|
85
|
+
end
|
86
|
+
|
87
|
+
def update(mr)
|
88
|
+
@recency = @recency | mr.recency
|
89
|
+
@is_match = mr.is_match
|
90
|
+
@variables = @variables.update mr.variables
|
91
|
+
@fact_hash = @fact_hash.update mr.fact_hash
|
92
|
+
return self
|
93
|
+
end
|
94
|
+
|
95
|
+
def dup
|
96
|
+
dup_mr = MatchResult.new
|
97
|
+
dup_mr.recency = @recency.clone
|
98
|
+
dup_mr.is_match = @is_match
|
99
|
+
dup_mr.variables = @variables.clone
|
100
|
+
dup_mr.fact_hash = @fact_hash.clone
|
101
|
+
return dup_mr
|
102
|
+
end
|
103
|
+
|
104
|
+
def merge!(mr)
|
105
|
+
return update(mr)
|
106
|
+
end
|
107
|
+
|
108
|
+
def merge(mr)
|
109
|
+
new_mr = MatchResult.new
|
110
|
+
new_mr.recency = @recency | mr.recency
|
111
|
+
new_mr.is_match = mr.is_match
|
112
|
+
new_mr.variables = @variables.merge mr.variables
|
113
|
+
new_mr.fact_hash = @fact_hash.merge mr.fact_hash
|
114
|
+
return new_mr
|
115
|
+
end
|
116
|
+
|
117
|
+
def clear
|
118
|
+
@variables = {}
|
119
|
+
@fact_hash = {}
|
120
|
+
@recency = []
|
121
|
+
end
|
122
|
+
|
123
|
+
def delete(tag)
|
124
|
+
@variables.delete(tag)
|
125
|
+
@fact_hash.delete(tag)
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_s
|
129
|
+
s = '#MatchResult('
|
130
|
+
s = s + 'f)(' unless @is_match
|
131
|
+
s = s + object_id.to_s+')('
|
132
|
+
@variables.each do |key,value|
|
133
|
+
s += "#{key}=#{value}/#{@fact_hash[key]}, "
|
134
|
+
end
|
135
|
+
return s + ")"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# This class is used when we need to have a Hash where keys and values are
|
140
|
+
# mapped many-to-many. This class allows for quick access of both key and
|
141
|
+
# value. It is similar to Multimap in C++ standard lib.
|
142
|
+
# This thing is a mess (and barely works). It needs to be refactored.
|
7
143
|
class MultiHash
|
8
144
|
def initialize(key=nil, values=[])
|
9
145
|
@i = 0
|
@@ -87,6 +223,7 @@ module Ruleby
|
|
87
223
|
@backward_hash[xref] = ids
|
88
224
|
end
|
89
225
|
|
226
|
+
# DEPRECATED
|
90
227
|
# WARN this method adds a value to the MultiHash only if it is unique. It
|
91
228
|
# can be a fairly costly operation, and should be avoided. We only
|
92
229
|
# implemented this as part of a hack to get things working early on.
|
@@ -150,6 +287,7 @@ module Ruleby
|
|
150
287
|
end
|
151
288
|
end
|
152
289
|
|
290
|
+
# DEPRECATED
|
153
291
|
# WARN see comments in add_uniq
|
154
292
|
def concat_uniq(double_hash)
|
155
293
|
double_hash.each do |ids,val|
|
data/lib/dsl/ferrari.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
# This file is part of the Ruleby project (http://ruleby.org)
|
2
|
+
#
|
3
|
+
# This application is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the Ruby license defined in the
|
5
|
+
# LICENSE.txt file.
|
6
|
+
#
|
7
|
+
# Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
|
8
|
+
#
|
9
|
+
# * Authors: Joe Kutner
|
10
|
+
#
|
11
|
+
|
12
|
+
module Ruleby
|
13
|
+
module Ferrari
|
14
|
+
class RulebookHelper
|
15
|
+
def initialize(engine)
|
16
|
+
@engine = engine
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :engine
|
20
|
+
|
21
|
+
def rule(name, *args, &block)
|
22
|
+
options = args[0].kind_of?(Hash) ? args.shift : {}
|
23
|
+
|
24
|
+
r = RuleBuilder.new name
|
25
|
+
args.each do |arg|
|
26
|
+
if arg.kind_of? Array
|
27
|
+
r.when(*arg)
|
28
|
+
else
|
29
|
+
raise 'Invalid condition. All or none must be Arrays.'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
r.then(&block)
|
34
|
+
r.priority = options[:priority] if options[:priority]
|
35
|
+
|
36
|
+
@engine.assert_rule(r.build_rule)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class RuleBuilder
|
42
|
+
|
43
|
+
def initialize(name, pattern=nil, action=nil, priority=0)
|
44
|
+
@name = name
|
45
|
+
@pattern = pattern
|
46
|
+
@action = action
|
47
|
+
@priority = priority
|
48
|
+
|
49
|
+
@tags = {}
|
50
|
+
@methods = {}
|
51
|
+
@when_counter = 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def when(*args)
|
55
|
+
clazz = args.shift
|
56
|
+
is_not = false
|
57
|
+
if clazz == :not || clazz == :~
|
58
|
+
clazz = args.shift
|
59
|
+
is_not = true
|
60
|
+
elsif clazz == :exists
|
61
|
+
raise 'The \'exists\' quantifier is not yet supported.'
|
62
|
+
end
|
63
|
+
|
64
|
+
head_tag = nil
|
65
|
+
atoms = []
|
66
|
+
@when_counter += 1
|
67
|
+
|
68
|
+
if args[0].kind_of?(Symbol)
|
69
|
+
head_tag = args.shift
|
70
|
+
@tags[head_tag] = @when_counter
|
71
|
+
else
|
72
|
+
head_tag = GeneratedTag.new
|
73
|
+
@tags[head_tag] = @when_counter
|
74
|
+
end
|
75
|
+
|
76
|
+
args.each do |arg|
|
77
|
+
if arg.kind_of? Hash
|
78
|
+
arg.each do |ab,tag|
|
79
|
+
ab.tag = tag
|
80
|
+
ab.clazz = clazz
|
81
|
+
@tags[tag] = @when_counter
|
82
|
+
@methods[tag] = ab.name
|
83
|
+
atoms.push ab.build_atom(@tags, @methods, @when_counter)
|
84
|
+
end
|
85
|
+
elsif arg.kind_of? AtomBuilder
|
86
|
+
arg.tag = GeneratedTag.new
|
87
|
+
arg.clazz = clazz
|
88
|
+
@methods[arg.tag] = arg.name
|
89
|
+
atoms.push arg.build_atom(@tags, @methods, @when_counter)
|
90
|
+
elsif arg == false
|
91
|
+
raise 'The != operator is not allowed.'
|
92
|
+
else
|
93
|
+
raise "Invalid condition: #{arg}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
p = is_not ? Core::NotPattern.new(head_tag, clazz, atoms) :
|
98
|
+
Core::ObjectPattern.new(head_tag, clazz, atoms)
|
99
|
+
@pattern = @pattern ? Core::AndPattern.new(@pattern, p) : p
|
100
|
+
|
101
|
+
return nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def then(&block)
|
105
|
+
@action = Core::Action.new(&block)
|
106
|
+
@action.name = @name
|
107
|
+
@action.priority = @priority
|
108
|
+
end
|
109
|
+
|
110
|
+
def priority
|
111
|
+
return @priority
|
112
|
+
end
|
113
|
+
|
114
|
+
def priority=(p)
|
115
|
+
@priority = p
|
116
|
+
@action.priority = @priority
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_rule
|
120
|
+
Core::Rule.new @name, @pattern, @action, @priority
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class MethodBuilder
|
125
|
+
public_instance_methods.each do |m|
|
126
|
+
a = [:method_missing, :new, :public_instance_methods, :__send__, :__id__]
|
127
|
+
undef_method m.to_sym unless a.include? m.to_sym
|
128
|
+
end
|
129
|
+
|
130
|
+
def method_missing(method_id, *args, &block)
|
131
|
+
ab = AtomBuilder.new method_id
|
132
|
+
if block_given?
|
133
|
+
args.each do |arg|
|
134
|
+
ab.bindings.push BindingBuilder.new(arg, method_id)
|
135
|
+
end
|
136
|
+
ab.block = block
|
137
|
+
elsif args.size > 0
|
138
|
+
puts args.class.to_s + ' --- ' + args.to_s
|
139
|
+
raise 'Arguments not supported for short-hand conditions'
|
140
|
+
end
|
141
|
+
return ab
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class BindingBuilder
|
146
|
+
attr_accessor :tag, :method
|
147
|
+
def initialize(tag,method=nil)
|
148
|
+
@tag = tag
|
149
|
+
@method = method
|
150
|
+
end
|
151
|
+
|
152
|
+
def +(arg)
|
153
|
+
raise 'Cannot use operators in short-hand mode!'
|
154
|
+
end
|
155
|
+
|
156
|
+
def -(arg)
|
157
|
+
raise 'Cannot use operators in short-hand mode!'
|
158
|
+
end
|
159
|
+
|
160
|
+
def /(arg)
|
161
|
+
raise 'Cannot use operators in short-hand mode!'
|
162
|
+
end
|
163
|
+
|
164
|
+
def *(arg)
|
165
|
+
raise 'Cannot use operators in short-hand mode!'
|
166
|
+
end
|
167
|
+
|
168
|
+
def to_s
|
169
|
+
"BindingBuilder @tag=#{@tag}, @method=#{@method}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class AtomBuilder
|
174
|
+
attr_accessor :tag, :name, :bindings, :clazz, :block
|
175
|
+
|
176
|
+
def initialize(name)
|
177
|
+
@name = name
|
178
|
+
@clazz = nil
|
179
|
+
@tag = nil
|
180
|
+
@bindings = []
|
181
|
+
@block = lambda {|x| true}
|
182
|
+
end
|
183
|
+
|
184
|
+
def method_missing(method_id, *args, &block)
|
185
|
+
if method_id == :not
|
186
|
+
return NotOperatorBuilder.new(@name)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def ==(value)
|
191
|
+
create_block value, lambda {|x,y| x == y}, lambda {|x| x == value}; self
|
192
|
+
end
|
193
|
+
|
194
|
+
def >(value)
|
195
|
+
create_block value, lambda {|x,y| x > y}, lambda {|x| x > value}; self
|
196
|
+
end
|
197
|
+
|
198
|
+
def <(value)
|
199
|
+
create_block value, lambda {|x,y| x < y}, lambda {|x| x < value}; self
|
200
|
+
end
|
201
|
+
|
202
|
+
def =~(value)
|
203
|
+
create_block value, lambda {|x,y| x =~ y}, lambda {|x| x =~ value}; self
|
204
|
+
end
|
205
|
+
|
206
|
+
def <=(value)
|
207
|
+
create_block value, lambda {|x,y| x <= y}, lambda {|x| x <= value}; self
|
208
|
+
end
|
209
|
+
|
210
|
+
def >=(value)
|
211
|
+
create_block value, lambda {|x,y| x >= y}, lambda {|x| x >= value}; self
|
212
|
+
end
|
213
|
+
|
214
|
+
def build_atom(tags=nil,methods=nil,when_id=nil)
|
215
|
+
|
216
|
+
raise 'Syntax error in Rules: class not set' unless @clazz
|
217
|
+
|
218
|
+
return Core::TypeAtom.new(@tag, @clazz) unless tags || when_id
|
219
|
+
|
220
|
+
return Core::PropertyAtom.new(@tag, @name, @clazz, &@block) if @bindings.empty?
|
221
|
+
|
222
|
+
if references_self?(tags,when_id)
|
223
|
+
bind_methods = @bindings.collect{ |bb| methods[bb.tag] }
|
224
|
+
Core::SelfReferenceAtom.new(@tag,@name,bind_methods,@clazz,&@block)
|
225
|
+
else
|
226
|
+
bind_tags = @bindings.collect{ |bb| bb.tag }
|
227
|
+
Core::ReferenceAtom.new(@tag,@name,bind_tags,@clazz,&@block)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
def references_self?(tags,when_id)
|
233
|
+
ref_self = 0
|
234
|
+
@bindings.each do |bb|
|
235
|
+
if (tags[bb.tag] == when_id)
|
236
|
+
ref_self += 1
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
if ref_self > 0 and ref_self != @bindings.size
|
241
|
+
raise 'Binding to self and another pattern in the same condition is not yet supported.'
|
242
|
+
end
|
243
|
+
|
244
|
+
return ref_self > 0
|
245
|
+
end
|
246
|
+
|
247
|
+
def create_block(value, ref_block, basic_block)
|
248
|
+
if value && value.kind_of?(BindingBuilder)
|
249
|
+
@bindings = [value]
|
250
|
+
@block = ref_block
|
251
|
+
else
|
252
|
+
@block = basic_block
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class NotOperatorBuilder < AtomBuilder
|
258
|
+
def ==(value)
|
259
|
+
create_block value, lambda {|x,y| x != y}, lambda {|x| x != value}; self
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|