factbase 0.0.35 → 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 +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 = {})
|