factbase 0.0.34 → 0.0.36
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +30 -5
- data/lib/factbase/fact.rb +5 -2
- data/lib/factbase/inv.rb +6 -0
- data/lib/factbase/looged.rb +6 -0
- data/lib/factbase/query.rb +8 -3
- data/lib/factbase/rules.rb +10 -1
- data/lib/factbase/syntax.rb +5 -2
- data/lib/factbase/term.rb +140 -59
- data/lib/factbase/to_json.rb +7 -0
- data/lib/factbase/to_xml.rb +7 -0
- data/lib/factbase/to_yaml.rb +7 -0
- data/lib/factbase/tuples.rb +16 -0
- data/lib/factbase.rb +1 -1
- data/test/factbase/test_fact.rb +2 -2
- data/test/factbase/test_query.rb +17 -7
- data/test/factbase/test_syntax.rb +1 -1
- data/test/factbase/test_term.rb +69 -52
- 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: c801d7ec256766a03b081de5949367aef74dc991b67207ae30c6e6229f11c425
|
4
|
+
data.tar.gz: 2cfc12996cc7e4d85fad86377ddc9b87916d5d2a1cbe5542ba8128488c1e0049
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d2d1ec011eaf2d34f00068fa71809bf8bd3fe794b6097eeb1292ad25f1a6916e319490a137fa4c1908c00ce640f19c8efff27444ca277e72048daf5e688c3ef
|
7
|
+
data.tar.gz: 7f20423de58a836eb68261a8d7ad45ec00056a4f991b8f34c68dd4833a42bbb0009bca75a4a8092112cfb80599e2be04e5cca9a9bfe18b3ae18102622d98f9a6
|
data/README.md
CHANGED
@@ -54,7 +54,7 @@ f2.import(File.read(file))
|
|
54
54
|
assert(f2.query('(eq foo 42)').each.to_a.size == 1)
|
55
55
|
```
|
56
56
|
|
57
|
-
|
57
|
+
There are some terms available in a query:
|
58
58
|
|
59
59
|
* `(always)` and `(never)` are "true" and "false"
|
60
60
|
* `(not t)` inverses the `t` if it's boolean (exception otherwise)
|
@@ -68,14 +68,39 @@ All terms available in a query:
|
|
68
68
|
* `(gt a b)` returns true if `a` is greater than `b`
|
69
69
|
* `(size k)` returns cardinality of `k` property (zero if property is absent)
|
70
70
|
* `(type a)` returns type of `a` ("String", "Integer", "Float", or "Time")
|
71
|
+
* `(many a)` return true if there are many values in the `a` property
|
72
|
+
* `(one a)` returns true if there is only one value in the `a` property
|
73
|
+
* `(at i a)` returns the `i`-th value of the `a` property
|
74
|
+
* `(nonil a b)` returns `b` if `a` is `nil`
|
71
75
|
* `(matches a re)` returns true when `a` matches regular expression `re`
|
72
76
|
* `(defn foo "self.to_s")` defines a new term using Ruby syntax and returns true
|
73
77
|
|
74
|
-
|
78
|
+
Also, some simple arithmetic:
|
75
79
|
|
76
|
-
* `(
|
77
|
-
is
|
78
|
-
* `(
|
80
|
+
* `(plus a b)` is a sum of `a` and `b`
|
81
|
+
* `(minus a b)` is a deducation of `b` from `a`
|
82
|
+
* `(times a b)` is a multiplication of `a` and `b`
|
83
|
+
* `(div a b)` is a division of `a` by `b`
|
84
|
+
|
85
|
+
There are terms that are history of search aware:
|
86
|
+
|
87
|
+
* `(prev a)` returns the value of `a` in the previously seen fact
|
88
|
+
|
89
|
+
There are also terms that match the entire factbase
|
90
|
+
and must be used inside the `(agg ..)` term:
|
91
|
+
|
92
|
+
* `(count)` returns the tally of facts
|
93
|
+
* `(max k)` returns the maximum value of the `k` property in all facts
|
94
|
+
* `(min k)` returns the minimum
|
95
|
+
* `(sum k)` returns the arithmetic sum of all values of the `k` property
|
96
|
+
|
97
|
+
The `agg` term enables sub-queries by evaluating the first argument (term)
|
98
|
+
over all available facts, passing the entire subset to the second argument,
|
99
|
+
and then returning the result as an atomic value:
|
100
|
+
|
101
|
+
* `(lt age (agg (eq gender 'F') (max age)))` selects all facts where
|
102
|
+
the `age` is smaller than the maximum `age` of all women
|
103
|
+
* `(eq id (agg (always) (max id)))` selects the fact with the largest `id`
|
79
104
|
|
80
105
|
## How to contribute
|
81
106
|
|
data/lib/factbase/fact.rb
CHANGED
@@ -25,6 +25,9 @@ require 'time'
|
|
25
25
|
require_relative '../factbase'
|
26
26
|
|
27
27
|
# Fact.
|
28
|
+
#
|
29
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
30
|
+
#
|
28
31
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
29
32
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
30
33
|
# License:: MIT
|
@@ -37,7 +40,7 @@ class Factbase::Fact
|
|
37
40
|
# Convert it to a string.
|
38
41
|
# @return [String] String representation of it (in JSON)
|
39
42
|
def to_s
|
40
|
-
@map.
|
43
|
+
"[ #{@map.map { |k, v| "#{k}: #{v}" }.join(', ')} ]"
|
41
44
|
end
|
42
45
|
|
43
46
|
# When a method is missing, this method is called.
|
@@ -45,7 +48,7 @@ class Factbase::Fact
|
|
45
48
|
k = args[0].to_s
|
46
49
|
if k.end_with?('=')
|
47
50
|
kk = k[0..-2]
|
48
|
-
raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-
|
51
|
+
raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-z_][_a-zA-Z0-9]*$/)
|
49
52
|
raise "Prohibited prop name '#{kk}'" if kk == 'to_s'
|
50
53
|
v = args[1]
|
51
54
|
raise "Prop value can't be nil" if v.nil?
|
data/lib/factbase/inv.rb
CHANGED
@@ -61,6 +61,9 @@ class Factbase::Inv
|
|
61
61
|
end
|
62
62
|
|
63
63
|
# Fact decorator.
|
64
|
+
#
|
65
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
66
|
+
#
|
64
67
|
class Fact
|
65
68
|
def initialize(fact, block)
|
66
69
|
@fact = fact
|
@@ -89,6 +92,9 @@ class Factbase::Inv
|
|
89
92
|
end
|
90
93
|
|
91
94
|
# Query decorator.
|
95
|
+
#
|
96
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
97
|
+
#
|
92
98
|
class Query
|
93
99
|
def initialize(query, block)
|
94
100
|
@query = query
|
data/lib/factbase/looged.rb
CHANGED
@@ -63,6 +63,9 @@ class Factbase::Looged
|
|
63
63
|
end
|
64
64
|
|
65
65
|
# Fact decorator.
|
66
|
+
#
|
67
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
68
|
+
#
|
66
69
|
class Fact
|
67
70
|
MAX_LENGTH = 64
|
68
71
|
|
@@ -98,6 +101,9 @@ class Factbase::Looged
|
|
98
101
|
end
|
99
102
|
|
100
103
|
# Query decorator.
|
104
|
+
#
|
105
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
106
|
+
#
|
101
107
|
class Query
|
102
108
|
def initialize(query, expr, loog)
|
103
109
|
@query = query
|
data/lib/factbase/query.rb
CHANGED
@@ -25,6 +25,9 @@ require_relative 'syntax'
|
|
25
25
|
require_relative 'fact'
|
26
26
|
|
27
27
|
# Query.
|
28
|
+
#
|
29
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
30
|
+
#
|
28
31
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
29
32
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
30
33
|
# License:: MIT
|
@@ -40,11 +43,13 @@ class Factbase::Query
|
|
40
43
|
# @return [Integer] Total number of facts yielded
|
41
44
|
def each
|
42
45
|
return to_enum(__method__) unless block_given?
|
43
|
-
term = Factbase::Syntax.new(@query).to_term
|
46
|
+
term = Factbase::Syntax.new(@query).to_term
|
44
47
|
yielded = 0
|
45
48
|
@maps.each do |m|
|
46
49
|
f = Factbase::Fact.new(@mutex, m)
|
47
|
-
|
50
|
+
r = term.evaluate(f, @maps)
|
51
|
+
raise 'Unexpected evaluation result, must be boolean' unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
|
52
|
+
next unless r
|
48
53
|
yield f
|
49
54
|
yielded += 1
|
50
55
|
end
|
@@ -59,7 +64,7 @@ class Factbase::Query
|
|
59
64
|
@mutex.synchronize do
|
60
65
|
@maps.delete_if do |m|
|
61
66
|
f = Factbase::Fact.new(@mutex, m)
|
62
|
-
if term.evaluate(f)
|
67
|
+
if term.evaluate(f, @maps)
|
63
68
|
deleted += 1
|
64
69
|
true
|
65
70
|
else
|
data/lib/factbase/rules.rb
CHANGED
@@ -62,6 +62,9 @@ class Factbase::Rules
|
|
62
62
|
end
|
63
63
|
|
64
64
|
# Fact decorator.
|
65
|
+
#
|
66
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
67
|
+
#
|
65
68
|
class Fact
|
66
69
|
def initialize(fact, check)
|
67
70
|
@fact = fact
|
@@ -91,6 +94,9 @@ class Factbase::Rules
|
|
91
94
|
end
|
92
95
|
|
93
96
|
# Query decorator.
|
97
|
+
#
|
98
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
99
|
+
#
|
94
100
|
class Query
|
95
101
|
def initialize(query, check)
|
96
102
|
@query = query
|
@@ -110,6 +116,9 @@ class Factbase::Rules
|
|
110
116
|
end
|
111
117
|
|
112
118
|
# Check one fact.
|
119
|
+
#
|
120
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
121
|
+
#
|
113
122
|
class Check
|
114
123
|
def initialize(fb, expr)
|
115
124
|
@fb = fb
|
@@ -117,7 +126,7 @@ class Factbase::Rules
|
|
117
126
|
end
|
118
127
|
|
119
128
|
def it(fact)
|
120
|
-
return if Factbase::Syntax.new(@expr).to_term.evaluate(fact)
|
129
|
+
return if Factbase::Syntax.new(@expr).to_term.evaluate(fact, [])
|
121
130
|
raise "The fact is in invalid state: #{fact}"
|
122
131
|
end
|
123
132
|
end
|
data/lib/factbase/syntax.rb
CHANGED
@@ -26,6 +26,9 @@ require_relative 'fact'
|
|
26
26
|
require_relative 'term'
|
27
27
|
|
28
28
|
# Syntax.
|
29
|
+
#
|
30
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
31
|
+
#
|
29
32
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
30
33
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
31
34
|
# License:: MIT
|
@@ -126,9 +129,9 @@ class Factbase::Syntax
|
|
126
129
|
elsif t.start_with?('\'', '"')
|
127
130
|
raise 'String literal can\'t be empty' if t.length <= 2
|
128
131
|
t[1..-2]
|
129
|
-
elsif t.match?(/^[0-9]+$/)
|
132
|
+
elsif t.match?(/^(\+|-)?[0-9]+$/)
|
130
133
|
t.to_i
|
131
|
-
elsif t.match?(/^[0-9]+\.[0-9]+(e\+[0-9]+)?$/)
|
134
|
+
elsif t.match?(/^(\+|-)?[0-9]+\.[0-9]+(e\+[0-9]+)?$/)
|
132
135
|
t.to_f
|
133
136
|
elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
|
134
137
|
Time.parse(t)
|
data/lib/factbase/term.rb
CHANGED
@@ -24,6 +24,9 @@ require_relative '../factbase'
|
|
24
24
|
require_relative 'fact'
|
25
25
|
|
26
26
|
# Term.
|
27
|
+
#
|
28
|
+
# This is an internal class, it is not supposed to be instantiated directly.
|
29
|
+
#
|
27
30
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
28
31
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
29
32
|
# License:: MIT
|
@@ -40,23 +43,12 @@ class Factbase::Term
|
|
40
43
|
|
41
44
|
# Does it match the fact?
|
42
45
|
# @param [Factbase::Fact] fact The fact
|
46
|
+
# @param [Array<Factbase::Fact>] maps All maps available
|
43
47
|
# @return [bool] TRUE if matches
|
44
|
-
def evaluate(fact)
|
45
|
-
send(@op, fact)
|
46
|
-
rescue NoMethodError =>
|
47
|
-
raise "Term '#{@op}' is not defined"
|
48
|
-
end
|
49
|
-
|
50
|
-
# Put it into the context: let it see the entire array of maps.
|
51
|
-
# @param [Array] maps The maps
|
52
|
-
# @return [Factbase::Term] Itself
|
53
|
-
def on(maps)
|
54
|
-
m = "#{@op}_on"
|
55
|
-
send(m, maps) if respond_to?(m, true)
|
56
|
-
@operands.each do |o|
|
57
|
-
o.on(maps) if o.is_a?(Factbase::Term)
|
58
|
-
end
|
59
|
-
self
|
48
|
+
def evaluate(fact, maps)
|
49
|
+
send(@op, fact, maps)
|
50
|
+
rescue NoMethodError => e
|
51
|
+
raise "Term '#{@op}' is not defined: #{e.message}"
|
60
52
|
end
|
61
53
|
|
62
54
|
# Simplify it if possible.
|
@@ -89,31 +81,31 @@ class Factbase::Term
|
|
89
81
|
|
90
82
|
private
|
91
83
|
|
92
|
-
def always(_fact)
|
84
|
+
def always(_fact, _maps)
|
93
85
|
assert_args(0)
|
94
86
|
true
|
95
87
|
end
|
96
88
|
|
97
|
-
def never(_fact)
|
89
|
+
def never(_fact, _maps)
|
98
90
|
assert_args(0)
|
99
91
|
false
|
100
92
|
end
|
101
93
|
|
102
|
-
def not(fact)
|
94
|
+
def not(fact, maps)
|
103
95
|
assert_args(1)
|
104
|
-
!only_bool(
|
96
|
+
!only_bool(the_values(0, fact, maps))
|
105
97
|
end
|
106
98
|
|
107
|
-
def or(fact)
|
99
|
+
def or(fact, maps)
|
108
100
|
(0..@operands.size - 1).each do |i|
|
109
|
-
return true if only_bool(
|
101
|
+
return true if only_bool(the_values(i, fact, maps))
|
110
102
|
end
|
111
103
|
false
|
112
104
|
end
|
113
105
|
|
114
|
-
def and(fact)
|
106
|
+
def and(fact, maps)
|
115
107
|
(0..@operands.size - 1).each do |i|
|
116
|
-
return false unless only_bool(
|
108
|
+
return false unless only_bool(the_values(i, fact, maps))
|
117
109
|
end
|
118
110
|
true
|
119
111
|
end
|
@@ -140,36 +132,90 @@ class Factbase::Term
|
|
140
132
|
and_or_simplify
|
141
133
|
end
|
142
134
|
|
143
|
-
def when(fact)
|
135
|
+
def when(fact, maps)
|
144
136
|
assert_args(2)
|
145
137
|
a = @operands[0]
|
146
138
|
b = @operands[1]
|
147
|
-
!a.evaluate(fact) || (a.evaluate(fact) && b.evaluate(fact))
|
139
|
+
!a.evaluate(fact, maps) || (a.evaluate(fact, maps) && b.evaluate(fact, maps))
|
148
140
|
end
|
149
141
|
|
150
|
-
def exists(fact)
|
142
|
+
def exists(fact, _maps)
|
151
143
|
assert_args(1)
|
152
144
|
!by_symbol(0, fact).nil?
|
153
145
|
end
|
154
146
|
|
155
|
-
def absent(fact)
|
147
|
+
def absent(fact, _maps)
|
156
148
|
assert_args(1)
|
157
149
|
by_symbol(0, fact).nil?
|
158
150
|
end
|
159
151
|
|
160
|
-
def
|
161
|
-
|
152
|
+
def nonil(fact, maps)
|
153
|
+
assert_args(2)
|
154
|
+
v = the_values(0, fact, maps)
|
155
|
+
return v unless v.nil?
|
156
|
+
the_values(1, fact, maps)
|
162
157
|
end
|
163
158
|
|
164
|
-
def
|
165
|
-
|
159
|
+
def at(fact, maps)
|
160
|
+
assert_args(2)
|
161
|
+
i = the_values(0, fact, maps)
|
162
|
+
raise 'Too many values at first position, one expected' unless i.size == 1
|
163
|
+
i = i[0]
|
164
|
+
return nil if i.nil?
|
165
|
+
v = the_values(1, fact, maps)
|
166
|
+
return nil if v.nil?
|
167
|
+
v[i]
|
168
|
+
end
|
169
|
+
|
170
|
+
def prev(fact, maps)
|
171
|
+
assert_args(1)
|
172
|
+
before = @prev
|
173
|
+
v = the_values(0, fact, maps)
|
174
|
+
@prev = v
|
175
|
+
before
|
166
176
|
end
|
167
177
|
|
168
|
-
def
|
169
|
-
|
178
|
+
def many(fact, maps)
|
179
|
+
assert_args(1)
|
180
|
+
v = the_values(0, fact, maps)
|
181
|
+
!v.nil? && v.size > 1
|
170
182
|
end
|
171
183
|
|
172
|
-
def
|
184
|
+
def one(fact, maps)
|
185
|
+
assert_args(1)
|
186
|
+
v = the_values(0, fact, maps)
|
187
|
+
!v.nil? && v.size == 1
|
188
|
+
end
|
189
|
+
|
190
|
+
def plus(fact, maps)
|
191
|
+
arithmetic(:+, fact, maps)
|
192
|
+
end
|
193
|
+
|
194
|
+
def minus(fact, maps)
|
195
|
+
arithmetic(:-, fact, maps)
|
196
|
+
end
|
197
|
+
|
198
|
+
def times(fact, maps)
|
199
|
+
arithmetic(:*, fact, maps)
|
200
|
+
end
|
201
|
+
|
202
|
+
def div(fact, maps)
|
203
|
+
arithmetic(:/, fact, maps)
|
204
|
+
end
|
205
|
+
|
206
|
+
def eq(fact, maps)
|
207
|
+
cmp(:==, fact, maps)
|
208
|
+
end
|
209
|
+
|
210
|
+
def lt(fact, maps)
|
211
|
+
cmp(:<, fact, maps)
|
212
|
+
end
|
213
|
+
|
214
|
+
def gt(fact, maps)
|
215
|
+
cmp(:>, fact, maps)
|
216
|
+
end
|
217
|
+
|
218
|
+
def size(fact, _maps)
|
173
219
|
assert_args(1)
|
174
220
|
v = by_symbol(0, fact)
|
175
221
|
return 0 if v.nil?
|
@@ -177,29 +223,29 @@ class Factbase::Term
|
|
177
223
|
v.size
|
178
224
|
end
|
179
225
|
|
180
|
-
def type(fact)
|
226
|
+
def type(fact, _maps)
|
181
227
|
assert_args(1)
|
182
228
|
v = by_symbol(0, fact)
|
183
229
|
return 'nil' if v.nil?
|
184
230
|
v.class.to_s
|
185
231
|
end
|
186
232
|
|
187
|
-
def matches(fact)
|
233
|
+
def matches(fact, maps)
|
188
234
|
assert_args(2)
|
189
|
-
str =
|
190
|
-
|
235
|
+
str = the_values(0, fact, maps)
|
236
|
+
return false if str.nil?
|
191
237
|
raise 'Exactly one string expected' unless str.size == 1
|
192
|
-
re =
|
238
|
+
re = the_values(1, fact, maps)
|
193
239
|
raise 'Regexp is nil' if re.nil?
|
194
240
|
raise 'Exactly one regexp expected' unless re.size == 1
|
195
241
|
str[0].to_s.match?(re[0])
|
196
242
|
end
|
197
243
|
|
198
|
-
def
|
244
|
+
def cmp(op, fact, maps)
|
199
245
|
assert_args(2)
|
200
|
-
lefts =
|
246
|
+
lefts = the_values(0, fact, maps)
|
201
247
|
return false if lefts.nil?
|
202
|
-
rights =
|
248
|
+
rights = the_values(1, fact, maps)
|
203
249
|
return false if rights.nil?
|
204
250
|
lefts.any? do |l|
|
205
251
|
l = l.floor if l.is_a?(Time) && op == :==
|
@@ -210,34 +256,69 @@ class Factbase::Term
|
|
210
256
|
end
|
211
257
|
end
|
212
258
|
|
213
|
-
def
|
259
|
+
def arithmetic(op, fact, maps)
|
260
|
+
assert_args(2)
|
261
|
+
lefts = the_values(0, fact, maps)
|
262
|
+
raise 'The first argument is NIL, while literal expected' if lefts.nil?
|
263
|
+
raise 'Too many values at first position, one expected' unless lefts.size == 1
|
264
|
+
rights = the_values(1, fact, maps)
|
265
|
+
raise 'The second argument is NIL, while literal expected' if rights.nil?
|
266
|
+
raise 'Too many values at second position, one expected' unless rights.size == 1
|
267
|
+
lefts[0].send(op, rights[0])
|
268
|
+
end
|
269
|
+
|
270
|
+
def defn(_fact, _maps)
|
214
271
|
fn = @operands[0]
|
215
272
|
raise 'A symbol expected as first argument of defn' unless fn.is_a?(Symbol)
|
216
|
-
e = "class Factbase::Term\nprivate\ndef #{fn}(fact)\n#{@operands[1]}\nend\nend"
|
273
|
+
e = "class Factbase::Term\nprivate\ndef #{fn}(fact, maps)\n#{@operands[1]}\nend\nend"
|
217
274
|
# rubocop:disable Security/Eval
|
218
275
|
eval(e)
|
219
276
|
# rubocop:enable Security/Eval
|
220
277
|
true
|
221
278
|
end
|
222
279
|
|
223
|
-
def min(
|
224
|
-
|
225
|
-
|
226
|
-
|
280
|
+
def min(_fact, maps)
|
281
|
+
@min ||= best(maps) { |v, b| v < b }
|
282
|
+
end
|
283
|
+
|
284
|
+
def max(_fact, maps)
|
285
|
+
@max ||= best(maps) { |v, b| v > b }
|
227
286
|
end
|
228
287
|
|
229
|
-
def
|
230
|
-
|
231
|
-
return nil if vv.nil?
|
232
|
-
vv.any? { |v| v == @max }
|
288
|
+
def count(_fact, maps)
|
289
|
+
@count ||= maps.size
|
233
290
|
end
|
234
291
|
|
235
|
-
def
|
236
|
-
@
|
292
|
+
def sum(_fact, maps)
|
293
|
+
@sum ||=
|
294
|
+
begin
|
295
|
+
k = @operands[0]
|
296
|
+
raise "A symbol expected, but provided: #{k}" unless k.is_a?(Symbol)
|
297
|
+
sum = 0
|
298
|
+
maps.each do |m|
|
299
|
+
vv = m[k.to_s]
|
300
|
+
next if vv.nil?
|
301
|
+
vv = [vv] unless vv.is_a?(Array)
|
302
|
+
vv.each do |v|
|
303
|
+
sum += v
|
304
|
+
end
|
305
|
+
end
|
306
|
+
sum
|
307
|
+
end
|
237
308
|
end
|
238
309
|
|
239
|
-
def
|
240
|
-
|
310
|
+
def agg(_fact, maps)
|
311
|
+
selector = @operands[0]
|
312
|
+
raise "A term expected, but #{selector} provided" unless selector.is_a?(Factbase::Term)
|
313
|
+
term = @operands[1]
|
314
|
+
raise "A term expected, but #{term} provided" unless term.is_a?(Factbase::Term)
|
315
|
+
subset = maps.select { |m| selector.evaluate(m, maps) }
|
316
|
+
@agg ||=
|
317
|
+
if subset.empty?
|
318
|
+
term.evaluate(Factbase::Fact.new(Mutex.new, {}), subset)
|
319
|
+
else
|
320
|
+
term.evaluate(subset.first, subset)
|
321
|
+
end
|
241
322
|
end
|
242
323
|
|
243
324
|
def assert_args(num)
|
@@ -253,9 +334,9 @@ class Factbase::Term
|
|
253
334
|
fact[k]
|
254
335
|
end
|
255
336
|
|
256
|
-
def
|
337
|
+
def the_values(pos, fact, maps)
|
257
338
|
v = @operands[pos]
|
258
|
-
v = v.evaluate(fact) if v.is_a?(Factbase::Term)
|
339
|
+
v = v.evaluate(fact, maps) if v.is_a?(Factbase::Term)
|
259
340
|
v = fact[v.to_s] if v.is_a?(Symbol)
|
260
341
|
return v if v.nil?
|
261
342
|
v = [v] unless v.is_a?(Array)
|
data/lib/factbase/to_json.rb
CHANGED
@@ -25,6 +25,13 @@ require 'time'
|
|
25
25
|
require_relative '../factbase'
|
26
26
|
|
27
27
|
# Factbase to JSON converter.
|
28
|
+
#
|
29
|
+
# This class helps converting an entire Factbase to YAML format, for example:
|
30
|
+
#
|
31
|
+
# require 'factbase/to_json'
|
32
|
+
# fb = Factbase.new
|
33
|
+
# puts Factbase::ToJSON.new(fb).json
|
34
|
+
#
|
28
35
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
29
36
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
30
37
|
# License:: MIT
|
data/lib/factbase/to_xml.rb
CHANGED
@@ -25,6 +25,13 @@ require 'time'
|
|
25
25
|
require_relative '../factbase'
|
26
26
|
|
27
27
|
# Factbase to XML converter.
|
28
|
+
#
|
29
|
+
# This class helps converting an entire Factbase to YAML format, for example:
|
30
|
+
#
|
31
|
+
# require 'factbase/to_xml'
|
32
|
+
# fb = Factbase.new
|
33
|
+
# puts Factbase::ToXML.new(fb).xml
|
34
|
+
#
|
28
35
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
29
36
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
30
37
|
# License:: MIT
|
data/lib/factbase/to_yaml.rb
CHANGED
@@ -25,6 +25,13 @@ require 'time'
|
|
25
25
|
require_relative '../factbase'
|
26
26
|
|
27
27
|
# Factbase to YAML converter.
|
28
|
+
#
|
29
|
+
# This class helps converting an entire Factbase to YAML format, for example:
|
30
|
+
#
|
31
|
+
# require 'factbase/to_yaml'
|
32
|
+
# fb = Factbase.new
|
33
|
+
# puts Factbase::ToYAML.new(fb).yaml
|
34
|
+
#
|
28
35
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
29
36
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
30
37
|
# License:: MIT
|
data/lib/factbase/tuples.rb
CHANGED
@@ -23,6 +23,22 @@
|
|
23
23
|
require_relative '../factbase'
|
24
24
|
|
25
25
|
# Tuples.
|
26
|
+
#
|
27
|
+
# With the help of this class, it's possible to select a few facts
|
28
|
+
# from a factbase at a time, which depend on each other. For example,
|
29
|
+
# it's necessary to find a fact where the +name+ is set and then find
|
30
|
+
# another fact, where the salary is the +salary+ is the same as in the
|
31
|
+
# first found fact. Here is how:
|
32
|
+
#
|
33
|
+
# Factbase::Tuples.new(qt, ['(exists name)', '(eq salary, {f0.salary})']).each do |a, b|
|
34
|
+
# puts a.name
|
35
|
+
# puts b.salary
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# Here, the +{f0.salary}+ is a special substitution place, which is replaced
|
39
|
+
# by the +salary+ of the fact that is found by the previous query. The indexing
|
40
|
+
# of queries starts from zero.
|
41
|
+
#
|
26
42
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
27
43
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
28
44
|
# License:: MIT
|
data/lib/factbase.rb
CHANGED
data/test/factbase/test_fact.rb
CHANGED
@@ -80,8 +80,8 @@ class TestFact < Minitest::Test
|
|
80
80
|
|
81
81
|
def test_set_by_name
|
82
82
|
f = Factbase::Fact.new(Mutex.new, {})
|
83
|
-
f.send('
|
84
|
-
assert_equal(42, f.
|
83
|
+
f.send('_foo_bar=', 42)
|
84
|
+
assert_equal(42, f._foo_bar, f.to_s)
|
85
85
|
end
|
86
86
|
|
87
87
|
def test_set_twice_same_value
|
data/test/factbase/test_query.rb
CHANGED
@@ -44,8 +44,8 @@ class TestQuery < Minitest::Test
|
|
44
44
|
|
45
45
|
def test_complex_parsing
|
46
46
|
maps = []
|
47
|
-
maps << { 'num' => 42 }
|
48
|
-
maps << { 'pi' => 3.14, 'num' => [42, 66, 0] }
|
47
|
+
maps << { 'num' => 42, 'name' => 'Jeff' }
|
48
|
+
maps << { 'pi' => 3.14, 'num' => [42, 66, 0], 'name' => 'peter' }
|
49
49
|
maps << { 'time' => Time.now - 100, 'num' => 0 }
|
50
50
|
{
|
51
51
|
'(eq num 444)' => 0,
|
@@ -53,20 +53,30 @@ class TestQuery < Minitest::Test
|
|
53
53
|
'(gt num 60)' => 1,
|
54
54
|
"(and (lt pi 100) \n\n (gt num 1000))" => 0,
|
55
55
|
'(exists pi)' => 1,
|
56
|
+
'(eq pi +3.14)' => 1,
|
56
57
|
'(not (exists hello))' => 3,
|
57
58
|
'(eq "Integer" (type num))' => 2,
|
59
|
+
'(when (eq num 0) (exists time))' => 2,
|
60
|
+
'(many num)' => 1,
|
61
|
+
'(one num)' => 2,
|
62
|
+
'(gt num (minus 1 (nonil (at 0 (prev num)) 0)))' => 3,
|
63
|
+
'(and (not (many num)) (eq num (plus 21 +21)))' => 1,
|
64
|
+
'(and (not (many num)) (eq num (minus -100 -142)))' => 1,
|
65
|
+
'(and (one num) (eq num (times 7 6)))' => 1,
|
66
|
+
'(and (one pi) (eq pi (div -6.28 -2)))' => 1,
|
58
67
|
'(gt (size num) 2)' => 1,
|
68
|
+
'(matches name "^[a-z]+$")' => 1,
|
59
69
|
'(lt (size num) 2)' => 2,
|
60
70
|
'(eq (size hello) 0)' => 3,
|
61
71
|
'(eq num pi)' => 0,
|
62
72
|
'(absent time)' => 2,
|
63
|
-
'(
|
64
|
-
'(
|
65
|
-
'(
|
66
|
-
'(min time)' => 1,
|
73
|
+
'(eq pi (agg (eq num 0) (sum pi)))' => 1,
|
74
|
+
'(eq num (agg (exists oops) (count)))' => 2,
|
75
|
+
'(lt num (agg (eq num 0) (max pi)))' => 2,
|
76
|
+
'(eq time (min time))' => 1,
|
67
77
|
'(and (absent time) (exists pi))' => 1,
|
68
78
|
"(and (exists time) (not (\t\texists pi)))" => 1,
|
69
|
-
"(or (eq num 66) (lt time #{(Time.now - 200).utc.iso8601}))" => 1
|
79
|
+
"(or (eq num +66) (lt time #{(Time.now - 200).utc.iso8601}))" => 1
|
70
80
|
}.each do |q, r|
|
71
81
|
assert_equal(r, Factbase::Query.new(maps, Mutex.new, q).each.to_a.size, q)
|
72
82
|
end
|
@@ -83,7 +83,7 @@ class TestSyntax < Minitest::Test
|
|
83
83
|
'(or (eq bar 888) (eq z 1))' => true,
|
84
84
|
"(or (gt bar 100) (eq foo 'Hello, world!'))" => true
|
85
85
|
}.each do |k, v|
|
86
|
-
assert_equal(v, Factbase::Syntax.new(k).to_term.evaluate(m), k)
|
86
|
+
assert_equal(v, Factbase::Syntax.new(k).to_term.evaluate(m, []), k)
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
data/test/factbase/test_term.rb
CHANGED
@@ -30,107 +30,107 @@ require_relative '../../lib/factbase/term'
|
|
30
30
|
class TestTerm < Minitest::Test
|
31
31
|
def test_simple_matching
|
32
32
|
t = Factbase::Term.new(:eq, [:foo, 42])
|
33
|
-
assert(t.evaluate(fact('foo' => [42])))
|
34
|
-
assert(!t.evaluate(fact('foo' => 'Hello!')))
|
35
|
-
assert(!t.evaluate(fact('bar' => ['Hello!'])))
|
33
|
+
assert(t.evaluate(fact('foo' => [42]), []))
|
34
|
+
assert(!t.evaluate(fact('foo' => 'Hello!'), []))
|
35
|
+
assert(!t.evaluate(fact('bar' => ['Hello!']), []))
|
36
36
|
end
|
37
37
|
|
38
38
|
def test_eq_matching
|
39
39
|
t = Factbase::Term.new(:eq, [:foo, 42])
|
40
|
-
assert(t.evaluate(fact('foo' => 42)))
|
41
|
-
assert(t.evaluate(fact('foo' => [10, 5, 6, -8, 'hey', 42, 9, 'fdsf'])))
|
42
|
-
assert(!t.evaluate(fact('foo' => [100])))
|
43
|
-
assert(!t.evaluate(fact('foo' => [])))
|
44
|
-
assert(!t.evaluate(fact('bar' => [])))
|
40
|
+
assert(t.evaluate(fact('foo' => 42), []))
|
41
|
+
assert(t.evaluate(fact('foo' => [10, 5, 6, -8, 'hey', 42, 9, 'fdsf']), []))
|
42
|
+
assert(!t.evaluate(fact('foo' => [100]), []))
|
43
|
+
assert(!t.evaluate(fact('foo' => []), []))
|
44
|
+
assert(!t.evaluate(fact('bar' => []), []))
|
45
45
|
end
|
46
46
|
|
47
47
|
def test_eq_matching_time
|
48
48
|
now = Time.now
|
49
49
|
t = Factbase::Term.new(:eq, [:foo, Time.parse(now.iso8601)])
|
50
|
-
assert(t.evaluate(fact('foo' => now)))
|
51
|
-
assert(t.evaluate(fact('foo' => [now, Time.now])))
|
50
|
+
assert(t.evaluate(fact('foo' => now), []))
|
51
|
+
assert(t.evaluate(fact('foo' => [now, Time.now]), []))
|
52
52
|
end
|
53
53
|
|
54
54
|
def test_lt_matching
|
55
55
|
t = Factbase::Term.new(:lt, [:foo, 42])
|
56
|
-
assert(t.evaluate(fact('foo' => [10])))
|
57
|
-
assert(!t.evaluate(fact('foo' => [100])))
|
58
|
-
assert(!t.evaluate(fact('foo' => 100)))
|
59
|
-
assert(!t.evaluate(fact('bar' => 100)))
|
56
|
+
assert(t.evaluate(fact('foo' => [10]), []))
|
57
|
+
assert(!t.evaluate(fact('foo' => [100]), []))
|
58
|
+
assert(!t.evaluate(fact('foo' => 100), []))
|
59
|
+
assert(!t.evaluate(fact('bar' => 100), []))
|
60
60
|
end
|
61
61
|
|
62
62
|
def test_gt_matching
|
63
63
|
t = Factbase::Term.new(:gt, [:foo, 42])
|
64
|
-
assert(t.evaluate(fact('foo' => [100])))
|
65
|
-
assert(t.evaluate(fact('foo' => 100)))
|
66
|
-
assert(!t.evaluate(fact('foo' => [10])))
|
67
|
-
assert(!t.evaluate(fact('foo' => 10)))
|
68
|
-
assert(!t.evaluate(fact('bar' => 10)))
|
64
|
+
assert(t.evaluate(fact('foo' => [100]), []))
|
65
|
+
assert(t.evaluate(fact('foo' => 100), []))
|
66
|
+
assert(!t.evaluate(fact('foo' => [10]), []))
|
67
|
+
assert(!t.evaluate(fact('foo' => 10), []))
|
68
|
+
assert(!t.evaluate(fact('bar' => 10), []))
|
69
69
|
end
|
70
70
|
|
71
71
|
def test_lt_matching_time
|
72
72
|
t = Factbase::Term.new(:lt, [:foo, Time.now])
|
73
|
-
assert(t.evaluate(fact('foo' => [Time.now - 100])))
|
74
|
-
assert(!t.evaluate(fact('foo' => [Time.now + 100])))
|
75
|
-
assert(!t.evaluate(fact('bar' => [100])))
|
73
|
+
assert(t.evaluate(fact('foo' => [Time.now - 100]), []))
|
74
|
+
assert(!t.evaluate(fact('foo' => [Time.now + 100]), []))
|
75
|
+
assert(!t.evaluate(fact('bar' => [100]), []))
|
76
76
|
end
|
77
77
|
|
78
78
|
def test_gt_matching_time
|
79
79
|
t = Factbase::Term.new(:gt, [:foo, Time.now])
|
80
|
-
assert(t.evaluate(fact('foo' => [Time.now + 100])))
|
81
|
-
assert(!t.evaluate(fact('foo' => [Time.now - 100])))
|
82
|
-
assert(!t.evaluate(fact('bar' => [100])))
|
80
|
+
assert(t.evaluate(fact('foo' => [Time.now + 100]), []))
|
81
|
+
assert(!t.evaluate(fact('foo' => [Time.now - 100]), []))
|
82
|
+
assert(!t.evaluate(fact('bar' => [100]), []))
|
83
83
|
end
|
84
84
|
|
85
85
|
def test_false_matching
|
86
86
|
t = Factbase::Term.new(:never, [])
|
87
|
-
assert(!t.evaluate(fact('foo' => [100])))
|
87
|
+
assert(!t.evaluate(fact('foo' => [100]), []))
|
88
88
|
end
|
89
89
|
|
90
90
|
def test_not_matching
|
91
91
|
t = Factbase::Term.new(:not, [Factbase::Term.new(:always, [])])
|
92
|
-
assert(!t.evaluate(fact('foo' => [100])))
|
92
|
+
assert(!t.evaluate(fact('foo' => [100]), []))
|
93
93
|
end
|
94
94
|
|
95
95
|
def test_not_eq_matching
|
96
96
|
t = Factbase::Term.new(:not, [Factbase::Term.new(:eq, [:foo, 100])])
|
97
|
-
assert(t.evaluate(fact('foo' => [42, 12, -90])))
|
98
|
-
assert(!t.evaluate(fact('foo' => 100)))
|
97
|
+
assert(t.evaluate(fact('foo' => [42, 12, -90]), []))
|
98
|
+
assert(!t.evaluate(fact('foo' => 100), []))
|
99
99
|
end
|
100
100
|
|
101
101
|
def test_size_matching
|
102
102
|
t = Factbase::Term.new(:size, [:foo])
|
103
|
-
assert_equal(3, t.evaluate(fact('foo' => [42, 12, -90])))
|
104
|
-
assert_equal(0, t.evaluate(fact('bar' => 100)))
|
103
|
+
assert_equal(3, t.evaluate(fact('foo' => [42, 12, -90]), []))
|
104
|
+
assert_equal(0, t.evaluate(fact('bar' => 100), []))
|
105
105
|
end
|
106
106
|
|
107
107
|
def test_exists_matching
|
108
108
|
t = Factbase::Term.new(:exists, [:foo])
|
109
|
-
assert(t.evaluate(fact('foo' => [42, 12, -90])))
|
110
|
-
assert(!t.evaluate(fact('bar' => 100)))
|
109
|
+
assert(t.evaluate(fact('foo' => [42, 12, -90]), []))
|
110
|
+
assert(!t.evaluate(fact('bar' => 100), []))
|
111
111
|
end
|
112
112
|
|
113
113
|
def test_absent_matching
|
114
114
|
t = Factbase::Term.new(:absent, [:foo])
|
115
|
-
assert(t.evaluate(fact('z' => [42, 12, -90])))
|
116
|
-
assert(!t.evaluate(fact('foo' => 100)))
|
115
|
+
assert(t.evaluate(fact('z' => [42, 12, -90]), []))
|
116
|
+
assert(!t.evaluate(fact('foo' => 100), []))
|
117
117
|
end
|
118
118
|
|
119
119
|
def test_type_matching
|
120
120
|
t = Factbase::Term.new(:type, [:foo])
|
121
|
-
assert_equal('Integer', t.evaluate(fact('foo' => 42)))
|
122
|
-
assert_equal('Array', t.evaluate(fact('foo' => [1, 2, 3])))
|
123
|
-
assert_equal('String', t.evaluate(fact('foo' => 'Hello, world!')))
|
124
|
-
assert_equal('Float', t.evaluate(fact('foo' => 3.14)))
|
125
|
-
assert_equal('Time', t.evaluate(fact('foo' => Time.now)))
|
126
|
-
assert_equal('nil', t.evaluate(fact))
|
121
|
+
assert_equal('Integer', t.evaluate(fact('foo' => 42), []))
|
122
|
+
assert_equal('Array', t.evaluate(fact('foo' => [1, 2, 3]), []))
|
123
|
+
assert_equal('String', t.evaluate(fact('foo' => 'Hello, world!'), []))
|
124
|
+
assert_equal('Float', t.evaluate(fact('foo' => 3.14), []))
|
125
|
+
assert_equal('Time', t.evaluate(fact('foo' => Time.now), []))
|
126
|
+
assert_equal('nil', t.evaluate(fact, []))
|
127
127
|
end
|
128
128
|
|
129
129
|
def test_regexp_matching
|
130
130
|
t = Factbase::Term.new(:matches, [:foo, '[a-z]+'])
|
131
|
-
assert(t.evaluate(fact('foo' => 'hello')))
|
132
|
-
assert(t.evaluate(fact('foo' => 'hello 42')))
|
133
|
-
assert(!t.evaluate(fact('foo' => 42)))
|
131
|
+
assert(t.evaluate(fact('foo' => 'hello'), []))
|
132
|
+
assert(t.evaluate(fact('foo' => 'hello 42'), []))
|
133
|
+
assert(!t.evaluate(fact('foo' => 42), []))
|
134
134
|
end
|
135
135
|
|
136
136
|
def test_or_matching
|
@@ -141,9 +141,9 @@ class TestTerm < Minitest::Test
|
|
141
141
|
Factbase::Term.new(:eq, [:bar, 5])
|
142
142
|
]
|
143
143
|
)
|
144
|
-
assert(t.evaluate(fact('foo' => [4])))
|
145
|
-
assert(t.evaluate(fact('bar' => [5])))
|
146
|
-
assert(!t.evaluate(fact('bar' => [42])))
|
144
|
+
assert(t.evaluate(fact('foo' => [4]), []))
|
145
|
+
assert(t.evaluate(fact('bar' => [5]), []))
|
146
|
+
assert(!t.evaluate(fact('bar' => [42]), []))
|
147
147
|
end
|
148
148
|
|
149
149
|
def test_when_matching
|
@@ -154,16 +154,33 @@ class TestTerm < Minitest::Test
|
|
154
154
|
Factbase::Term.new(:eq, [:bar, 5])
|
155
155
|
]
|
156
156
|
)
|
157
|
-
assert(t.evaluate(fact('foo' => 4, 'bar' => 5)))
|
158
|
-
assert(!t.evaluate(fact('foo' => 4)))
|
159
|
-
assert(t.evaluate(fact('foo' => 5, 'bar' => 5)))
|
157
|
+
assert(t.evaluate(fact('foo' => 4, 'bar' => 5), []))
|
158
|
+
assert(!t.evaluate(fact('foo' => 4), []))
|
159
|
+
assert(t.evaluate(fact('foo' => 5, 'bar' => 5), []))
|
160
160
|
end
|
161
161
|
|
162
162
|
def test_defn_simple
|
163
163
|
t = Factbase::Term.new(:defn, [:foo, 'self.to_s'])
|
164
|
-
assert_equal(true, t.evaluate(fact('foo' => 4)))
|
164
|
+
assert_equal(true, t.evaluate(fact('foo' => 4), []))
|
165
165
|
t1 = Factbase::Term.new(:foo, ['hello, world!'])
|
166
|
-
assert_equal('(foo \'hello, world!\')', t1.evaluate(fact))
|
166
|
+
assert_equal('(foo \'hello, world!\')', t1.evaluate(fact, []))
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_past
|
170
|
+
t = Factbase::Term.new(:prev, [:foo])
|
171
|
+
assert_nil(t.evaluate(fact('foo' => 4), []))
|
172
|
+
assert_equal([4], t.evaluate(fact('foo' => 5), []))
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_at
|
176
|
+
t = Factbase::Term.new(:at, [1, :foo])
|
177
|
+
assert_nil(t.evaluate(fact('foo' => 4), []))
|
178
|
+
assert_equal(5, t.evaluate(fact('foo' => [4, 5]), []))
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_nonil
|
182
|
+
t = Factbase::Term.new(:nonil, [Factbase::Term.new(:at, [5, :foo]), 42])
|
183
|
+
assert_equal([42], t.evaluate(fact('foo' => 4), []))
|
167
184
|
end
|
168
185
|
|
169
186
|
private
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: factbase
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.36
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-05-
|
11
|
+
date: 2024-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|