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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20c84ac9e67311e6333da8c6da3d69ae445aad64686017a69bc702f50d703223
4
- data.tar.gz: c275ca48a224b42f09e71551b563585036b866f99acf8dd58e13311bdaa0beec
3
+ metadata.gz: c801d7ec256766a03b081de5949367aef74dc991b67207ae30c6e6229f11c425
4
+ data.tar.gz: 2cfc12996cc7e4d85fad86377ddc9b87916d5d2a1cbe5542ba8128488c1e0049
5
5
  SHA512:
6
- metadata.gz: 1ef21c5e33b3cf5f67e7f5edb56110e9f4d62c970cbc52158ca3922661d9a3d3ed411c26b95b2be97c1d6b6a4a5ebb5c521c87401325da42e585bd61f31b403f
7
- data.tar.gz: 9f02e0addedcff827c76f7c5a58cae70c84b73c02522ab429ba0607bf967f0412c8a82ba1b8caa49c0c0329498f0f440dc95e861fed563362bd1f7295bea52ba
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
@@ -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.to_json
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-z][_a-zA-Z0-9]*$/)
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
@@ -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
@@ -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.on(@maps)
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
- next unless term.evaluate(f)
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
@@ -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
@@ -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 => _e
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(the_value(0, fact))
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(the_value(i, fact))
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(the_value(i, fact))
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 eq(fact)
161
- arithmetic(:==, fact)
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 lt(fact)
165
- arithmetic(:<, fact)
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 gt(fact)
169
- arithmetic(:>, fact)
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 size(fact)
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 = the_value(0, fact)
190
- raise 'String is nil' if str.nil?
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 = the_value(1, fact)
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 arithmetic(op, fact)
244
+ def cmp(op, fact, maps)
199
245
  assert_args(2)
200
- lefts = the_value(0, fact)
246
+ lefts = the_values(0, fact, maps)
201
247
  return false if lefts.nil?
202
- rights = the_value(1, fact)
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 defn(_fact)
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(fact)
224
- vv = the_value(0, fact)
225
- return nil if vv.nil?
226
- vv.any? { |v| v == @min }
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 max(fact)
230
- vv = the_value(0, fact)
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 max_on(maps)
236
- @max = best(maps) { |v, b| v > b }
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 min_on(maps)
240
- @min = best(maps) { |v, b| v < b }
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 the_value(pos, fact)
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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.34'
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
@@ -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
- '(max num)' => 1,
64
- '(and (exists time) (max num))' => 0,
65
- '(and (exists pi) (max num))' => 1,
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
 
@@ -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.34
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-25 00:00:00.000000000 Z
11
+ date: 2024-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json