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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc97bfbf967aa07fa189c2d3c696eb8d963f4647c06fb5d97ca4b0e2b4edb4db
4
- data.tar.gz: b38987779b663f76cbf9876b4e117cd1fabb3e1c2ef07bd6ab433be0dede9a44
3
+ metadata.gz: c801d7ec256766a03b081de5949367aef74dc991b67207ae30c6e6229f11c425
4
+ data.tar.gz: 2cfc12996cc7e4d85fad86377ddc9b87916d5d2a1cbe5542ba8128488c1e0049
5
5
  SHA512:
6
- metadata.gz: 97cdc3269d4fbfea8d7246f6cee197be5bdf8b1e463573940b9be2384cbc2718a454e7f1291c5efc11554fc59c84a97c9349def6413d0ee54192118820cdb252
7
- data.tar.gz: '049a1b2ae928bae38e1079f4e67d4f29a3efe3201e27c28c92f58bf4a2eda519a62a9a18095d857792337b19775e319921f8494643718a406bf6c377e36eefa6'
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
- All terms available in a query:
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
- There are also terms that match the entire factbase:
78
+ Also, some simple arithmetic:
75
79
 
76
- * `(max k)` returns true if the value of `k` property
77
- is the largest in the entire factbase
78
- * `(min k)` returns true if the value of `k` is the smallest
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-z][_a-zA-Z0-9]*$/)
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?
@@ -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(the_value(0, fact, maps))
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(the_value(i, fact, maps))
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(the_value(i, fact, maps))
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
- arithmetic(:==, fact, maps)
207
+ cmp(:==, fact, maps)
154
208
  end
155
209
 
156
210
  def lt(fact, maps)
157
- arithmetic(:<, fact, maps)
211
+ cmp(:<, fact, maps)
158
212
  end
159
213
 
160
214
  def gt(fact, maps)
161
- arithmetic(:>, fact, maps)
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 = the_value(0, fact, maps)
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 = the_value(1, fact, maps)
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 arithmetic(op, fact, maps)
244
+ def cmp(op, fact, maps)
191
245
  assert_args(2)
192
- lefts = the_value(0, fact, maps)
246
+ lefts = the_values(0, fact, maps)
193
247
  return false if lefts.nil?
194
- rights = the_value(1, fact, maps)
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 the_value(pos, fact, maps)
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
@@ -29,7 +29,7 @@ require 'yaml'
29
29
  # License:: MIT
30
30
  class Factbase
31
31
  # Current version of the gem (changed by .rultor.yml on every release)
32
- VERSION = '0.0.35'
32
+ VERSION = '0.0.36'
33
33
 
34
34
  # Constructor.
35
35
  def initialize(facts = [])
@@ -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('foo_bar=', 42)
84
- assert_equal(42, f.foo_bar, f.to_s)
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
@@ -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
@@ -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 = {})
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factbase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.35
4
+ version: 0.0.36
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko