factbase 0.0.35 → 0.0.36
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/README.md +30 -5
- data/lib/factbase/fact.rb +1 -1
- data/lib/factbase/syntax.rb +2 -2
- data/lib/factbase/term.rb +77 -12
- data/lib/factbase.rb +1 -1
- data/test/factbase/test_fact.rb +2 -2
- data/test/factbase/test_query.rb +9 -1
- data/test/factbase/test_term.rb +17 -0
- metadata +1 -1
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
@@ -48,7 +48,7 @@ class Factbase::Fact
|
|
48
48
|
k = args[0].to_s
|
49
49
|
if k.end_with?('=')
|
50
50
|
kk = k[0..-2]
|
51
|
-
raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-
|
51
|
+
raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-z_][_a-zA-Z0-9]*$/)
|
52
52
|
raise "Prohibited prop name '#{kk}'" if kk == 'to_s'
|
53
53
|
v = args[1]
|
54
54
|
raise "Prop value can't be nil" if v.nil?
|
data/lib/factbase/syntax.rb
CHANGED
@@ -129,9 +129,9 @@ class Factbase::Syntax
|
|
129
129
|
elsif t.start_with?('\'', '"')
|
130
130
|
raise 'String literal can\'t be empty' if t.length <= 2
|
131
131
|
t[1..-2]
|
132
|
-
elsif t.match?(/^[0-9]+$/)
|
132
|
+
elsif t.match?(/^(\+|-)?[0-9]+$/)
|
133
133
|
t.to_i
|
134
|
-
elsif t.match?(/^[0-9]+\.[0-9]+(e\+[0-9]+)?$/)
|
134
|
+
elsif t.match?(/^(\+|-)?[0-9]+\.[0-9]+(e\+[0-9]+)?$/)
|
135
135
|
t.to_f
|
136
136
|
elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
|
137
137
|
Time.parse(t)
|
data/lib/factbase/term.rb
CHANGED
@@ -93,19 +93,19 @@ class Factbase::Term
|
|
93
93
|
|
94
94
|
def not(fact, maps)
|
95
95
|
assert_args(1)
|
96
|
-
!only_bool(
|
96
|
+
!only_bool(the_values(0, fact, maps))
|
97
97
|
end
|
98
98
|
|
99
99
|
def or(fact, maps)
|
100
100
|
(0..@operands.size - 1).each do |i|
|
101
|
-
return true if only_bool(
|
101
|
+
return true if only_bool(the_values(i, fact, maps))
|
102
102
|
end
|
103
103
|
false
|
104
104
|
end
|
105
105
|
|
106
106
|
def and(fact, maps)
|
107
107
|
(0..@operands.size - 1).each do |i|
|
108
|
-
return false unless only_bool(
|
108
|
+
return false unless only_bool(the_values(i, fact, maps))
|
109
109
|
end
|
110
110
|
true
|
111
111
|
end
|
@@ -149,16 +149,70 @@ class Factbase::Term
|
|
149
149
|
by_symbol(0, fact).nil?
|
150
150
|
end
|
151
151
|
|
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)
|
157
|
+
end
|
158
|
+
|
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
|
176
|
+
end
|
177
|
+
|
178
|
+
def many(fact, maps)
|
179
|
+
assert_args(1)
|
180
|
+
v = the_values(0, fact, maps)
|
181
|
+
!v.nil? && v.size > 1
|
182
|
+
end
|
183
|
+
|
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
|
+
|
152
206
|
def eq(fact, maps)
|
153
|
-
|
207
|
+
cmp(:==, fact, maps)
|
154
208
|
end
|
155
209
|
|
156
210
|
def lt(fact, maps)
|
157
|
-
|
211
|
+
cmp(:<, fact, maps)
|
158
212
|
end
|
159
213
|
|
160
214
|
def gt(fact, maps)
|
161
|
-
|
215
|
+
cmp(:>, fact, maps)
|
162
216
|
end
|
163
217
|
|
164
218
|
def size(fact, _maps)
|
@@ -178,20 +232,20 @@ class Factbase::Term
|
|
178
232
|
|
179
233
|
def matches(fact, maps)
|
180
234
|
assert_args(2)
|
181
|
-
str =
|
235
|
+
str = the_values(0, fact, maps)
|
182
236
|
return false if str.nil?
|
183
237
|
raise 'Exactly one string expected' unless str.size == 1
|
184
|
-
re =
|
238
|
+
re = the_values(1, fact, maps)
|
185
239
|
raise 'Regexp is nil' if re.nil?
|
186
240
|
raise 'Exactly one regexp expected' unless re.size == 1
|
187
241
|
str[0].to_s.match?(re[0])
|
188
242
|
end
|
189
243
|
|
190
|
-
def
|
244
|
+
def cmp(op, fact, maps)
|
191
245
|
assert_args(2)
|
192
|
-
lefts =
|
246
|
+
lefts = the_values(0, fact, maps)
|
193
247
|
return false if lefts.nil?
|
194
|
-
rights =
|
248
|
+
rights = the_values(1, fact, maps)
|
195
249
|
return false if rights.nil?
|
196
250
|
lefts.any? do |l|
|
197
251
|
l = l.floor if l.is_a?(Time) && op == :==
|
@@ -202,6 +256,17 @@ class Factbase::Term
|
|
202
256
|
end
|
203
257
|
end
|
204
258
|
|
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
|
+
|
205
270
|
def defn(_fact, _maps)
|
206
271
|
fn = @operands[0]
|
207
272
|
raise 'A symbol expected as first argument of defn' unless fn.is_a?(Symbol)
|
@@ -269,7 +334,7 @@ class Factbase::Term
|
|
269
334
|
fact[k]
|
270
335
|
end
|
271
336
|
|
272
|
-
def
|
337
|
+
def the_values(pos, fact, maps)
|
273
338
|
v = @operands[pos]
|
274
339
|
v = v.evaluate(fact, maps) if v.is_a?(Factbase::Term)
|
275
340
|
v = fact[v.to_s] if v.is_a?(Symbol)
|
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
@@ -53,9 +53,17 @@ 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,
|
58
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,
|
59
67
|
'(gt (size num) 2)' => 1,
|
60
68
|
'(matches name "^[a-z]+$")' => 1,
|
61
69
|
'(lt (size num) 2)' => 2,
|
@@ -68,7 +76,7 @@ class TestQuery < Minitest::Test
|
|
68
76
|
'(eq time (min time))' => 1,
|
69
77
|
'(and (absent time) (exists pi))' => 1,
|
70
78
|
"(and (exists time) (not (\t\texists pi)))" => 1,
|
71
|
-
"(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
|
72
80
|
}.each do |q, r|
|
73
81
|
assert_equal(r, Factbase::Query.new(maps, Mutex.new, q).each.to_a.size, q)
|
74
82
|
end
|
data/test/factbase/test_term.rb
CHANGED
@@ -166,6 +166,23 @@ class TestTerm < Minitest::Test
|
|
166
166
|
assert_equal('(foo \'hello, world!\')', t1.evaluate(fact, []))
|
167
167
|
end
|
168
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), []))
|
184
|
+
end
|
185
|
+
|
169
186
|
private
|
170
187
|
|
171
188
|
def fact(map = {})
|