factbase 0.0.39 → 0.0.41

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: 8ff1db55d5a25e5036d3a98423caedc960f8323681e2227c82e0a397727067c0
4
- data.tar.gz: b3000b708bd2eb69005998a722011a296384d5cea66641b975471e611b93e4d0
3
+ metadata.gz: 0ea2fe8670484d4eb3eac59caa947b547863a2106fa6ae771b8e9447b30573dc
4
+ data.tar.gz: b45d41378d4b61f0b3d8d01a3454819a4eca7724bd81adda7fe083c9594250ef
5
5
  SHA512:
6
- metadata.gz: a377a359a877b1f50c83f13c451e095036b4087cf88e39c04a0e5d64e86337bfef458883646009daff4b2b90446abbdbd563a4921b3ed275b88b7408b69ef0de
7
- data.tar.gz: 26a5ca088df353e0c526080213aa4d87b23805dd522617d52d2d738b1881c40ae4c659fa886db4fbe19a30a131ff6b5a7e19aadf5dbafcbd0f6b81954386bd65
6
+ metadata.gz: 40d6ca4cce2c4d64fe5e31b609c2fd15d05041358b8ae3be266c991f943ce242217378a8e83f96d8be83c50068fb65e04dff8c8c24708eeea8f79490e283c817
7
+ data.tar.gz: d09b19ac55775c511c0619b0a23be1260496f33d623927153e0b5b373b6919570a9f47104c0e2b1125299799f561e2b6fb39b2cb6ea5486b48ab76a05e15bfd1
data/Gemfile CHANGED
@@ -26,7 +26,7 @@ gemspec
26
26
  gem 'minitest', '5.23.1', require: false
27
27
  gem 'rake', '13.2.1', require: false
28
28
  gem 'rspec-rails', '6.1.2', require: false
29
- gem 'rubocop', '1.64.0', require: false
29
+ gem 'rubocop', '1.64.1', require: false
30
30
  gem 'rubocop-performance', '1.21.0', require: false
31
31
  gem 'rubocop-rspec', '2.29.2', require: false
32
32
  gem 'simplecov', '0.22.0', require: false
data/Gemfile.lock CHANGED
@@ -70,7 +70,7 @@ GEM
70
70
  nokogiri (1.16.5-x86_64-linux)
71
71
  racc (~> 1.4)
72
72
  parallel (1.24.0)
73
- parser (3.3.1.0)
73
+ parser (3.3.2.0)
74
74
  ast (~> 2.4.1)
75
75
  racc
76
76
  psych (5.1.2)
@@ -125,7 +125,7 @@ GEM
125
125
  rspec-mocks (~> 3.13)
126
126
  rspec-support (~> 3.13)
127
127
  rspec-support (3.13.1)
128
- rubocop (1.64.0)
128
+ rubocop (1.64.1)
129
129
  json (~> 2.3)
130
130
  language_server-protocol (>= 3.17.0)
131
131
  parallel (~> 1.10)
@@ -184,7 +184,7 @@ DEPENDENCIES
184
184
  minitest (= 5.23.1)
185
185
  rake (= 13.2.1)
186
186
  rspec-rails (= 6.1.2)
187
- rubocop (= 1.64.0)
187
+ rubocop (= 1.64.1)
188
188
  rubocop-performance (= 1.21.0)
189
189
  rubocop-rspec (= 2.29.2)
190
190
  simplecov (= 0.22.0)
data/README.md CHANGED
@@ -92,6 +92,7 @@ One term is for meta-programming:
92
92
  There are terms that are history of search aware:
93
93
 
94
94
  * `(prev a)` returns the value of `a` in the previously seen fact
95
+ * `(unique k)` returns true if the value of `k` property hasn't been seen yet
95
96
 
96
97
  There are also terms that match the entire factbase
97
98
  and must be used inside the `(agg ..)` term:
data/lib/factbase/fact.rb CHANGED
@@ -24,17 +24,20 @@ require 'json'
24
24
  require 'time'
25
25
  require_relative '../factbase'
26
26
 
27
- # Fact.
27
+ # A single fact in a factbase.
28
28
  #
29
- # This is an internal class, it is not supposed to be instantiated directly.
30
- #
31
- # It is possible to use for testing directly, for example to make a
29
+ # This is an internal class, it is not supposed to be instantiated directly,
30
+ # by the +Factbase+ class.
31
+ # However, it is possible to use it for testing directly, for example to make a
32
32
  # fact with a single key/value pair inside:
33
33
  #
34
34
  # require 'factbase/fact'
35
35
  # f = Factbase::Fact.new(Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
36
36
  # assert_equal(42, f.foo)
37
37
  #
38
+ # A fact is basically a key/value hash map, where values are non-empty
39
+ # sets of values.
40
+ #
38
41
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
39
42
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
40
43
  # License:: MIT
@@ -20,6 +20,7 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
22
 
23
+ require 'time'
23
24
  require 'loog'
24
25
 
25
26
  # A decorator of a Factbase, that logs all operations.
@@ -114,39 +115,51 @@ class Factbase::Looged
114
115
  def each(&)
115
116
  q = Factbase::Syntax.new(@expr).to_term.to_s
116
117
  if block_given?
117
- r = @query.each(&)
118
+ r = nil
119
+ tail = Factbase::Looged.elapsed do
120
+ r = @query.each(&)
121
+ end
118
122
  raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
119
123
  if r.zero?
120
- @loog.debug("Nothing found by '#{q}'")
124
+ @loog.debug("Nothing found by '#{q}' #{tail}")
121
125
  else
122
- @loog.debug("Found #{r} fact(s) by '#{q}'")
126
+ @loog.debug("Found #{r} fact(s) by '#{q}' #{tail}")
123
127
  end
124
128
  r
125
129
  else
126
130
  array = []
127
- # rubocop:disable Style/MapIntoArray
128
- @query.each do |f|
129
- array << f
131
+ tail = Factbase::Looged.elapsed do
132
+ @query.each do |f|
133
+ array << f
134
+ end
130
135
  end
131
- # rubocop:enable Style/MapIntoArray
132
136
  if array.empty?
133
- @loog.debug("Nothing found by '#{q}'")
137
+ @loog.debug("Nothing found by '#{q}' #{tail}")
134
138
  else
135
- @loog.debug("Found #{array.size} fact(s) by '#{q}'")
139
+ @loog.debug("Found #{array.size} fact(s) by '#{q}' #{tail}")
136
140
  end
137
141
  array
138
142
  end
139
143
  end
140
144
 
141
145
  def delete!
142
- r = @query.delete!
146
+ r = nil
147
+ tail = Factbase::Looged.elapsed do
148
+ r = @query.delete!
149
+ end
143
150
  raise ".delete! of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
144
151
  if r.zero?
145
- @loog.debug("Nothing deleted by '#{@expr}'")
152
+ @loog.debug("Nothing deleted by '#{@expr}' #{tail}")
146
153
  else
147
- @loog.debug("Deleted #{r} fact(s) by '#{@expr}'")
154
+ @loog.debug("Deleted #{r} fact(s) by '#{@expr}' #{tail}")
148
155
  end
149
156
  r
150
157
  end
151
158
  end
159
+
160
+ def self.elapsed
161
+ start = Time.now
162
+ yield
163
+ "in #{format('%.2f', (Time.now - start) * 1000)}ms"
164
+ end
152
165
  end
@@ -26,12 +26,17 @@ require_relative 'fact'
26
26
 
27
27
  # Query.
28
28
  #
29
- # This is an internal class, it is not supposed to be instantiated directly.
29
+ # This is an internal class, it is not supposed to be instantiated directly. It
30
+ # is created by the +query()+ method of the +Factbase+ class.
30
31
  #
31
32
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
32
33
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
33
34
  # License:: MIT
34
35
  class Factbase::Query
36
+ # Constructor.
37
+ # @param [Array<Fact>] maps Array of facts to start with
38
+ # @param [Mutex] mutex Mutex to sync all modifications to the +maps+
39
+ # @param [String] query The query as a string
35
40
  def initialize(maps, mutex, query)
36
41
  @maps = maps
37
42
  @mutex = mutex
@@ -48,7 +53,9 @@ class Factbase::Query
48
53
  @maps.each do |m|
49
54
  f = Factbase::Fact.new(@mutex, m)
50
55
  r = term.evaluate(f, @maps)
51
- raise 'Unexpected evaluation result, must be boolean' unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
56
+ unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
57
+ raise "Unexpected evaluation result (#{r.class}), must be Boolean at #{@query}"
58
+ end
52
59
  next unless r
53
60
  yield f
54
61
  yielded += 1
data/lib/factbase/term.rb CHANGED
@@ -36,6 +36,15 @@ require_relative 'fact'
36
36
  # t = Factbase::Term.new(:lt, [:foo, 50])
37
37
  # assert(t.evaluate(f))
38
38
  #
39
+ # The design of this class may look ugly, since it has a large number of
40
+ # methods, each of which corresponds to a different type of a +Term+. A much
41
+ # better design would definitely involve many classes, one per each type
42
+ # of a term. It's not done this way because of an experimental nature of
43
+ # the project. Most probably we should keep current design intact, since it
44
+ # works well and is rather simple to extend (by adding new term types).
45
+ # Moreover, it looks like the number of possible term types is rather limited
46
+ # and currently we implement most of them.
47
+ #
39
48
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
40
49
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
41
50
  # License:: MIT
@@ -57,7 +66,9 @@ class Factbase::Term
57
66
  def evaluate(fact, maps)
58
67
  send(@op, fact, maps)
59
68
  rescue NoMethodError => e
60
- raise "Term '#{@op}' is not defined: #{e.message}"
69
+ raise "Term '#{@op}' is not defined at #{self}: #{e.message}"
70
+ rescue StandardError => e
71
+ raise "#{e.message} at #{self}"
61
72
  end
62
73
 
63
74
  # Simplify it if possible.
@@ -102,19 +113,19 @@ class Factbase::Term
102
113
 
103
114
  def not(fact, maps)
104
115
  assert_args(1)
105
- !only_bool(the_values(0, fact, maps))
116
+ !only_bool(the_values(0, fact, maps), 0)
106
117
  end
107
118
 
108
119
  def or(fact, maps)
109
120
  (0..@operands.size - 1).each do |i|
110
- return true if only_bool(the_values(i, fact, maps))
121
+ return true if only_bool(the_values(i, fact, maps), 0)
111
122
  end
112
123
  false
113
124
  end
114
125
 
115
126
  def and(fact, maps)
116
127
  (0..@operands.size - 1).each do |i|
117
- return false unless only_bool(the_values(i, fact, maps))
128
+ return false unless only_bool(the_values(i, fact, maps), 0)
118
129
  end
119
130
  true
120
131
  end
@@ -184,6 +195,19 @@ class Factbase::Term
184
195
  before
185
196
  end
186
197
 
198
+ def unique(fact, _maps)
199
+ @uniques = [] if @uniques.nil?
200
+ assert_args(1)
201
+ vv = by_symbol(0, fact)
202
+ return false if vv.nil?
203
+ vv = [vv] unless vv.is_a?(Array)
204
+ vv.each do |v|
205
+ return false if @uniques.include?(v)
206
+ @uniques << v
207
+ end
208
+ true
209
+ end
210
+
187
211
  def many(fact, maps)
188
212
  assert_args(1)
189
213
  v = the_values(0, fact, maps)
@@ -317,6 +341,7 @@ class Factbase::Term
317
341
  end
318
342
 
319
343
  def agg(_fact, maps)
344
+ assert_args(2)
320
345
  selector = @operands[0]
321
346
  raise "A term expected, but #{selector} provided" unless selector.is_a?(Factbase::Term)
322
347
  term = @operands[1]
@@ -352,10 +377,12 @@ class Factbase::Term
352
377
  v
353
378
  end
354
379
 
355
- def only_bool(val)
380
+ def only_bool(val, pos)
356
381
  val = val[0] if val.is_a?(Array)
357
382
  return false if val.nil?
358
- raise "Boolean expected, while #{val.class} received" unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
383
+ unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
384
+ raise "Boolean expected, while #{val.class} received from #{@operands[pos]}"
385
+ end
359
386
  val
360
387
  end
361
388
 
data/lib/factbase.rb CHANGED
@@ -79,12 +79,13 @@ require 'yaml'
79
79
  # License:: MIT
80
80
  class Factbase
81
81
  # Current version of the gem (changed by .rultor.yml on every release)
82
- VERSION = '0.0.39'
82
+ VERSION = '0.0.41'
83
83
 
84
84
  # An exception that may be thrown in a transaction, to roll it back.
85
85
  class Rollback < StandardError; end
86
86
 
87
87
  # Constructor.
88
+ # @param [Array<Hash>] facts Array of facts to start with
88
89
  def initialize(facts = [])
89
90
  @maps = facts
90
91
  @mutex = Mutex.new
@@ -94,7 +94,7 @@ class TestLooged < Minitest::Test
94
94
  'Found 1 fact(s) by \'(exists bar)\'',
95
95
  'Deleted 3 fact(s) by \'(not (exists bar))\''
96
96
  ].each do |s|
97
- assert(log.to_s.include?("#{s}\n"), "#{log}\n")
97
+ assert(log.to_s.include?(s), "#{log}\n")
98
98
  end
99
99
  end
100
100
  end
@@ -57,6 +57,9 @@ class TestQuery < Minitest::Test
57
57
  '(not (exists hello))' => 3,
58
58
  '(eq "Integer" (type num))' => 2,
59
59
  '(when (eq num 0) (exists time))' => 2,
60
+ '(unique num)' => 2,
61
+ '(unique name)' => 2,
62
+ '(unique pi)' => 1,
60
63
  '(many num)' => 1,
61
64
  '(one num)' => 2,
62
65
  '(gt num (minus 1 (either (at 0 (prev num)) 0)))' => 3,
@@ -183,6 +183,22 @@ class TestTerm < Minitest::Test
183
183
  assert_equal([42], t.evaluate(fact('foo' => 4), []))
184
184
  end
185
185
 
186
+ def test_report_missing_term
187
+ t = Factbase::Term.new(:something, [])
188
+ msg = assert_raises do
189
+ t.evaluate(fact, [])
190
+ end.message
191
+ assert(msg.include?('not defined at (something)'), msg)
192
+ end
193
+
194
+ def test_report_other_error
195
+ t = Factbase::Term.new(:at, [])
196
+ msg = assert_raises do
197
+ t.evaluate(fact, [])
198
+ end.message
199
+ assert(msg.include?('at (at)'), msg)
200
+ end
201
+
186
202
  private
187
203
 
188
204
  def fact(map = {})
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.39
4
+ version: 0.0.41
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-30 00:00:00.000000000 Z
11
+ date: 2024-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json