wongi-engine 0.4.0.pre.alpha3 → 0.4.0.pre.alpha4
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.
- checksums.yaml +4 -4
- data/lib/wongi-engine/beta/aggregate_node.rb +44 -53
- data/lib/wongi-engine/compiler.rb +3 -6
- data/lib/wongi-engine/dsl/clause/aggregate.rb +8 -10
- data/lib/wongi-engine/dsl.rb +18 -8
- data/lib/wongi-engine/token.rb +4 -0
- data/lib/wongi-engine/version.rb +1 -1
- data/spec/aggregate_spec.rb +95 -112
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 566bc82ac763603c9e6a5556677364af9e57449bcb839c00865c3dd8cefbc1dc
|
4
|
+
data.tar.gz: d8285247fbe76bcf603740f7e6a6dd603226ea5c1e47564223b851c087a148e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a88bbad2800cf1701d66277cbffc646fe92e0a283bb6da8f3bc828fbfdea001d3f4040be35a039372d7db4680c7acb763ccfe91e94f233be90b5182697f24602
|
7
|
+
data.tar.gz: b00126ee326301079e7c257548b6ce650682f0d8637056445760a3f0cc25da96411d4be8fc50edea8f343cdf1523f9ab82ec8d719a20a232128e7622f99ae331
|
@@ -1,41 +1,37 @@
|
|
1
1
|
module Wongi::Engine
|
2
2
|
class AggregateNode < BetaNode
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :var, :over, :partition, :aggregate, :map
|
4
4
|
|
5
|
-
def initialize(parent,
|
5
|
+
def initialize(parent, var, over, partition, aggregate, map)
|
6
6
|
super(parent)
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@assign = assign
|
7
|
+
@var = var
|
8
|
+
@over = over
|
9
|
+
@partition = make_partition_fn(partition)
|
10
|
+
@aggregate = make_aggregate_fn(aggregate)
|
11
|
+
@map = make_map_fn(map)
|
13
12
|
end
|
14
13
|
|
15
|
-
def
|
16
|
-
return
|
17
|
-
return false if self.assignment_pattern != assignment_pattern
|
18
|
-
return true if self.tests.empty? && tests.empty?
|
19
|
-
return false if self.tests.length != tests.length
|
14
|
+
def make_partition_fn(partition)
|
15
|
+
return nil if partition.nil?
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
}
|
25
|
-
|
17
|
+
if Template.variable?(partition)
|
18
|
+
->(token) { token[partition] }
|
19
|
+
elsif partition.is_a?(Array) && partition.all? { Template.variable?(_1) }
|
20
|
+
->(token) { token.values_at(*partition) }
|
21
|
+
else
|
22
|
+
partition
|
23
|
+
end
|
26
24
|
end
|
27
25
|
|
28
|
-
def
|
29
|
-
|
30
|
-
tokens.each do |token|
|
31
|
-
evaluate(wme: wme, token: token)
|
32
|
-
end
|
26
|
+
def make_aggregate_fn(agg)
|
27
|
+
agg
|
33
28
|
end
|
34
29
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
30
|
+
def make_map_fn(map)
|
31
|
+
if map.nil?
|
32
|
+
->(token) { token[over] }
|
33
|
+
else
|
34
|
+
map
|
39
35
|
end
|
40
36
|
end
|
41
37
|
|
@@ -43,42 +39,37 @@ module Wongi::Engine
|
|
43
39
|
return if tokens.find { |t| t.duplicate? token }
|
44
40
|
|
45
41
|
overlay.add_token(token)
|
46
|
-
evaluate
|
42
|
+
evaluate
|
47
43
|
end
|
48
44
|
|
49
45
|
def beta_deactivate(token)
|
50
46
|
overlay.remove_token(token)
|
51
|
-
beta_deactivate_children(token:
|
47
|
+
beta_deactivate_children(token:)
|
48
|
+
evaluate
|
52
49
|
end
|
53
50
|
|
54
51
|
def refresh_child(child)
|
55
|
-
|
56
|
-
evaluate(wme: nil, token: token, child: child)
|
57
|
-
end
|
52
|
+
evaluate(child: child)
|
58
53
|
end
|
59
54
|
|
60
|
-
def evaluate(
|
61
|
-
|
62
|
-
|
63
|
-
beta_deactivate_children(token: token)
|
64
|
-
|
65
|
-
template = specialize(alpha.template, tests, token)
|
66
|
-
candidates = select_wmes(template) { |asserted_wme| matches?(token, asserted_wme) }
|
67
|
-
|
68
|
-
return if candidates.empty?
|
69
|
-
|
70
|
-
mapped = candidates.map(&map)
|
71
|
-
value = if function.is_a?(Symbol) && mapped.respond_to?(function)
|
72
|
-
mapped.send(function)
|
73
|
-
else
|
74
|
-
function.call(mapped)
|
75
|
-
end
|
76
|
-
assignments = { assign => value }
|
77
|
-
if child
|
78
|
-
child.beta_activate(Token.new(child, token, wme, assignments))
|
55
|
+
def evaluate(child: nil)
|
56
|
+
groups = if partition
|
57
|
+
tokens.group_by(&partition).values
|
79
58
|
else
|
80
|
-
|
81
|
-
|
59
|
+
# just a single group of everything
|
60
|
+
[tokens]
|
61
|
+
end
|
62
|
+
|
63
|
+
groups.each do |tokens|
|
64
|
+
aggregated = self.aggregate.call(tokens.map(&self.map))
|
65
|
+
assignment = { var => aggregated }
|
66
|
+
children = child ? [child] : self.children
|
67
|
+
tokens.each do |token|
|
68
|
+
# TODO: optimize this to work with a diff of actual changes
|
69
|
+
beta_deactivate_children(token:, children:)
|
70
|
+
children.each do |beta|
|
71
|
+
beta.beta_activate(Token.new(beta, token, nil, assignment))
|
72
|
+
end
|
82
73
|
end
|
83
74
|
end
|
84
75
|
end
|
@@ -58,12 +58,9 @@ module Wongi::Engine
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
-
def aggregate_node(
|
62
|
-
declare(
|
63
|
-
|
64
|
-
self.node = AggregateNode.new(node, alpha, tests, assignment, map, function, assign).tap do |node|
|
65
|
-
alpha.betas << node unless alpha_deaf
|
66
|
-
end.tap(&:refresh)
|
61
|
+
def aggregate_node(var, over, partition, aggregate, map)
|
62
|
+
declare(var)
|
63
|
+
self.node = AggregateNode.new(node, var, over, partition, aggregate, map).tap(&:refresh)
|
67
64
|
end
|
68
65
|
|
69
66
|
def or_node(variants)
|
@@ -1,20 +1,18 @@
|
|
1
1
|
module Wongi::Engine
|
2
2
|
module DSL::Clause
|
3
|
-
class Aggregate
|
4
|
-
attr_reader :
|
3
|
+
class Aggregate
|
4
|
+
attr_reader :var, :over, :partition, :aggregate, :map
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
|
6
|
+
def initialize(var, options = {})
|
7
|
+
@var = var
|
8
|
+
@over = options[:over]
|
9
|
+
@partition = options[:partition]
|
10
|
+
@aggregate = options[:using]
|
8
11
|
@map = options[:map]
|
9
|
-
@function = options[:function]
|
10
|
-
@assign = options[:assign]
|
11
|
-
@map ||= ->(wme) { wme.send(member) }
|
12
|
-
super
|
13
12
|
end
|
14
13
|
|
15
14
|
def compile(context)
|
16
|
-
|
17
|
-
context.tap { |c| c.aggregate_node(self, tests, assignment, map, function, assign) }
|
15
|
+
context.tap { |c| c.aggregate_node(var, over, partition, aggregate, map) }
|
18
16
|
end
|
19
17
|
end
|
20
18
|
end
|
data/lib/wongi-engine/dsl.rb
CHANGED
@@ -89,19 +89,29 @@ module Wongi::Engine::DSL
|
|
89
89
|
clause :aggregate
|
90
90
|
accept Clause::Aggregate
|
91
91
|
|
92
|
-
clause :
|
93
|
-
body { |
|
94
|
-
aggregate
|
92
|
+
clause :min
|
93
|
+
body { |var, opts|
|
94
|
+
aggregate var, opts.merge(using: ->(collection) { collection.min })
|
95
95
|
}
|
96
96
|
|
97
|
-
clause :
|
98
|
-
body { |
|
99
|
-
aggregate
|
97
|
+
clause :max
|
98
|
+
body { |var, opts|
|
99
|
+
aggregate var, opts.merge(using: ->(collection) { collection.max })
|
100
100
|
}
|
101
101
|
|
102
102
|
clause :count
|
103
|
-
body { |
|
104
|
-
aggregate
|
103
|
+
body { |var, opts = {}|
|
104
|
+
aggregate var, opts.merge(using: ->(collection) { collection.count })
|
105
|
+
}
|
106
|
+
|
107
|
+
clause :sum
|
108
|
+
body { |var, opts|
|
109
|
+
aggregate var, opts.merge(using: ->(collection) { collection.inject(:+) })
|
110
|
+
}
|
111
|
+
|
112
|
+
clause :product
|
113
|
+
body { |var, opts|
|
114
|
+
aggregate var, opts.merge(using: ->(collection) { collection.inject(:*) })
|
105
115
|
}
|
106
116
|
|
107
117
|
clause :assert, :dynamic
|
data/lib/wongi-engine/token.rb
CHANGED
data/lib/wongi-engine/version.rb
CHANGED
data/spec/aggregate_spec.rb
CHANGED
@@ -8,190 +8,173 @@ describe 'aggregate' do
|
|
8
8
|
let(:rule_name) { SecureRandom.alphanumeric(16) }
|
9
9
|
let(:production) { engine.productions[rule_name] }
|
10
10
|
|
11
|
-
context '
|
12
|
-
it '
|
11
|
+
context 'min' do
|
12
|
+
it 'works' do
|
13
13
|
engine << rule(rule_name) do
|
14
14
|
forall {
|
15
|
-
|
15
|
+
has :_, :weight, :Weight
|
16
|
+
min :X, over: :Weight
|
17
|
+
has :Fruit, :weight, :X
|
16
18
|
}
|
17
19
|
end
|
18
20
|
|
19
|
-
expect(production.size).to be == 0
|
20
|
-
|
21
21
|
engine << [:apple, :weight, 5]
|
22
22
|
expect(production.size).to be == 1
|
23
|
-
|
23
|
+
production.tokens.each do |token|
|
24
|
+
expect(token[:X]).to be == 5
|
25
|
+
expect(token[:Fruit]).to be == :apple
|
26
|
+
end
|
24
27
|
|
25
28
|
engine << [:pea, :weight, 2]
|
26
|
-
expect(production.size).to be ==
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
expect(production.tokens.first[:X]).to be == 2
|
29
|
+
expect(production.size).to be == 2
|
30
|
+
production.tokens.each do |token|
|
31
|
+
expect(token[:X]).to be == 2
|
32
|
+
expect(token[:Fruit]).to be == :pea
|
33
|
+
end
|
32
34
|
|
33
35
|
engine.retract [:pea, :weight, 2]
|
34
36
|
expect(production.size).to be == 1
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
expect(production.tokens.first[:X]).to be == 15
|
40
|
-
|
41
|
-
engine.retract [:melon, :weight, 15]
|
42
|
-
expect(production.size).to be == 0
|
37
|
+
production.tokens.each do |token|
|
38
|
+
expect(token[:X]).to be == 5
|
39
|
+
expect(token[:Fruit]).to be == :apple
|
40
|
+
end
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
46
|
-
context '
|
44
|
+
context 'max' do
|
47
45
|
it 'works' do
|
48
46
|
engine << rule(rule_name) do
|
49
47
|
forall {
|
50
|
-
|
48
|
+
has :_, :weight, :Weight
|
49
|
+
max :X, over: :Weight
|
51
50
|
has :Fruit, :weight, :X
|
52
51
|
}
|
53
52
|
end
|
54
53
|
|
55
|
-
engine << [:apple, :weight, 5]
|
56
|
-
expect(production.size).to be == 1
|
57
|
-
expect(production.tokens.first[:X]).to be == 5
|
58
|
-
expect(production.tokens.first[:Fruit]).to be == :apple
|
59
|
-
|
60
54
|
engine << [:pea, :weight, 2]
|
61
55
|
expect(production.size).to be == 1
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
engine.retract [:pea, :weight, 2]
|
66
|
-
expect(production.size).to be == 1
|
67
|
-
expect(production.tokens.first[:X]).to be == 5
|
68
|
-
expect(production.tokens.first[:Fruit]).to be == :apple
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
context 'min' do
|
73
|
-
it 'works' do
|
74
|
-
engine << rule(rule_name) do
|
75
|
-
forall {
|
76
|
-
min :_, :weight, :_, on: :object, assign: :X
|
77
|
-
has :Fruit, :weight, :X
|
78
|
-
}
|
56
|
+
production.tokens.each do |token|
|
57
|
+
expect(token[:X]).to be == 2
|
58
|
+
expect(token[:Fruit]).to be == :pea
|
79
59
|
end
|
80
60
|
|
81
61
|
engine << [:apple, :weight, 5]
|
82
|
-
expect(production.size).to be ==
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
expect(production.size).to be == 1
|
88
|
-
expect(production.tokens.first[:X]).to be == 2
|
89
|
-
expect(production.tokens.first[:Fruit]).to be == :pea
|
62
|
+
expect(production.size).to be == 2
|
63
|
+
production.tokens.each do |token|
|
64
|
+
expect(token[:X]).to be == 5
|
65
|
+
expect(token[:Fruit]).to be == :apple
|
66
|
+
end
|
90
67
|
|
91
|
-
engine.retract [:
|
68
|
+
engine.retract [:apple, :weight, 5]
|
92
69
|
expect(production.size).to be == 1
|
93
|
-
|
94
|
-
|
70
|
+
production.tokens.each do |token|
|
71
|
+
expect(token[:X]).to be == 2
|
72
|
+
expect(token[:Fruit]).to be == :pea
|
73
|
+
end
|
95
74
|
end
|
96
75
|
end
|
97
76
|
|
98
|
-
context '
|
77
|
+
context 'count' do
|
99
78
|
it 'works' do
|
100
79
|
engine << rule(rule_name) do
|
101
80
|
forall {
|
102
|
-
|
103
|
-
|
81
|
+
has :_, :weight, :Weight
|
82
|
+
count :Count
|
104
83
|
}
|
105
84
|
end
|
106
85
|
|
107
|
-
engine << [:pea, :weight,
|
86
|
+
engine << [:pea, :weight, 1]
|
108
87
|
expect(production.size).to be == 1
|
109
|
-
|
110
|
-
|
88
|
+
production.tokens.each do |token|
|
89
|
+
expect(token[:Count]).to be == 1
|
90
|
+
end
|
111
91
|
|
112
92
|
engine << [:apple, :weight, 5]
|
113
|
-
expect(production.size).to be ==
|
114
|
-
|
115
|
-
|
93
|
+
expect(production.size).to be == 2
|
94
|
+
production.tokens.each do |token|
|
95
|
+
expect(token[:Count]).to be == 2
|
96
|
+
end
|
97
|
+
|
98
|
+
engine << [:watermelon, :weight, 15]
|
99
|
+
expect(production.size).to be == 3
|
100
|
+
production.tokens.each do |token|
|
101
|
+
expect(token[:Count]).to be == 3
|
102
|
+
end
|
116
103
|
|
117
104
|
engine.retract [:apple, :weight, 5]
|
118
|
-
expect(production.size).to be ==
|
119
|
-
|
120
|
-
|
105
|
+
expect(production.size).to be == 2
|
106
|
+
production.tokens.each do |token|
|
107
|
+
expect(token[:Count]).to be == 2
|
108
|
+
end
|
121
109
|
end
|
122
|
-
end
|
123
110
|
|
124
|
-
|
125
|
-
it 'works' do
|
111
|
+
it 'works with a post-filter' do
|
126
112
|
engine << rule(rule_name) do
|
127
113
|
forall {
|
128
|
-
|
129
|
-
|
114
|
+
has :_, :weight, :Weight
|
115
|
+
count :Count
|
116
|
+
gte :Count, 3 # pass if at least 3 matching facts exist
|
130
117
|
}
|
131
118
|
end
|
132
119
|
|
133
|
-
engine << [:pea, :weight,
|
134
|
-
expect(production.size).to be ==
|
135
|
-
expect(production.tokens.first[:X]).to be == 2
|
136
|
-
expect(production.tokens.first[:Fruit]).to be == :pea
|
120
|
+
engine << [:pea, :weight, 1]
|
121
|
+
expect(production.size).to be == 0
|
137
122
|
|
138
123
|
engine << [:apple, :weight, 5]
|
139
|
-
expect(production.size).to be ==
|
140
|
-
|
141
|
-
|
124
|
+
expect(production.size).to be == 0
|
125
|
+
|
126
|
+
engine << [:watermelon, :weight, 15]
|
127
|
+
expect(production.size).to be == 3
|
128
|
+
production.tokens.each do |token|
|
129
|
+
expect(token[:Count]).to be == 3
|
130
|
+
end
|
142
131
|
|
143
132
|
engine.retract [:apple, :weight, 5]
|
144
|
-
expect(production.size).to be ==
|
145
|
-
expect(production.tokens.first[:X]).to be == 2
|
146
|
-
expect(production.tokens.first[:Fruit]).to be == :pea
|
133
|
+
expect(production.size).to be == 0
|
147
134
|
end
|
148
135
|
end
|
149
136
|
|
150
|
-
context '
|
137
|
+
context 'partitioning by a single var' do
|
151
138
|
it 'works' do
|
152
139
|
engine << rule(rule_name) do
|
153
140
|
forall {
|
154
|
-
|
141
|
+
has :factor, :Number, :Factor
|
142
|
+
product :Product, over: :Factor, partition: :Number
|
155
143
|
}
|
156
144
|
end
|
157
145
|
|
158
|
-
engine << [:
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
engine << [:apple, :weight, 5]
|
163
|
-
expect(production.size).to be == 1
|
164
|
-
expect(production.tokens.first[:Count]).to be == 2
|
165
|
-
|
166
|
-
engine << [:watermelon, :weight, 15]
|
167
|
-
expect(production.size).to be == 1
|
168
|
-
expect(production.tokens.first[:Count]).to be == 3
|
146
|
+
engine << [:factor, 10, 2]
|
147
|
+
engine << [:factor, 10, 5]
|
148
|
+
engine << [:factor, 12, 3]
|
149
|
+
engine << [:factor, 12, 4]
|
169
150
|
|
170
|
-
|
171
|
-
|
172
|
-
|
151
|
+
expect(production).to have(4).tokens
|
152
|
+
production.tokens.each do |token|
|
153
|
+
expect(token[:Product]).to be_a(Integer)
|
154
|
+
expect(token[:Product]).to eq(token[:Number])
|
155
|
+
end
|
173
156
|
end
|
157
|
+
end
|
174
158
|
|
175
|
-
|
159
|
+
context 'partitioning by a list' do
|
160
|
+
it 'works' do
|
176
161
|
engine << rule(rule_name) do
|
177
162
|
forall {
|
178
|
-
|
179
|
-
|
163
|
+
has :factor, :Number, :Factor
|
164
|
+
product :Product, over: :Factor, partition: [:Number]
|
180
165
|
}
|
181
166
|
end
|
182
167
|
|
183
|
-
engine << [:
|
184
|
-
|
185
|
-
|
186
|
-
engine << [:
|
187
|
-
expect(production.size).to be == 0
|
188
|
-
|
189
|
-
engine << [:watermelon, :weight, 15]
|
190
|
-
expect(production.size).to be == 1
|
191
|
-
expect(production.tokens.first[:Count]).to be == 3
|
168
|
+
engine << [:factor, 10, 2]
|
169
|
+
engine << [:factor, 10, 5]
|
170
|
+
engine << [:factor, 12, 3]
|
171
|
+
engine << [:factor, 12, 4]
|
192
172
|
|
193
|
-
|
194
|
-
|
173
|
+
expect(production).to have(4).tokens
|
174
|
+
production.tokens.each do |token|
|
175
|
+
expect(token[:Product]).to be_a(Integer)
|
176
|
+
expect(token[:Product]).to eq(token[:Number])
|
177
|
+
end
|
195
178
|
end
|
196
179
|
end
|
197
180
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wongi-engine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.0.pre.
|
4
|
+
version: 0.4.0.pre.alpha4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Valeri Sokolov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-11-
|
11
|
+
date: 2022-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry
|