factbase 0.0.34 → 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: 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