factbase 0.0.35 → 0.0.37

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: fc97bfbf967aa07fa189c2d3c696eb8d963f4647c06fb5d97ca4b0e2b4edb4db
4
- data.tar.gz: b38987779b663f76cbf9876b4e117cd1fabb3e1c2ef07bd6ab433be0dede9a44
3
+ metadata.gz: a464f6fa40a3aef098e4ab4a58b6f6905ac101421f22fb806b63c4fb624aa610
4
+ data.tar.gz: ad19a0147890d8b4ff8fff96f7e9f47b09ecb264b3cb52fa606be892278fa380
5
5
  SHA512:
6
- metadata.gz: 97cdc3269d4fbfea8d7246f6cee197be5bdf8b1e463573940b9be2384cbc2718a454e7f1291c5efc11554fc59c84a97c9349def6413d0ee54192118820cdb252
7
- data.tar.gz: '049a1b2ae928bae38e1079f4e67d4f29a3efe3201e27c28c92f58bf4a2eda519a62a9a18095d857792337b19775e319921f8494643718a406bf6c377e36eefa6'
6
+ metadata.gz: 6abb74b759ef7eea421e8a7f4f1ebdbf7ae46836dfefcd324286508fbb82e207a787265c0cf9f8931b43b00d50e5df983266fa9442f9c277111dfa9ea3a778de
7
+ data.tar.gz: b0ed97f83f59230bafce810488f3e720d098a748149f7fbd58b7f1c25425b2fba6b742ce16b18692df34607eb3c42f9f11196ca212bc0273d7bdffe15e759efe
@@ -32,7 +32,7 @@ jobs:
32
32
  strategy:
33
33
  matrix:
34
34
  os: [ubuntu-20.04, macos-12, windows-2022]
35
- ruby: [3.2]
35
+ ruby: [3.2, 3.3]
36
36
  runs-on: ${{ matrix.os }}
37
37
  steps:
38
38
  - uses: actions/checkout@v4
data/Gemfile.lock CHANGED
@@ -101,7 +101,7 @@ GEM
101
101
  zeitwerk (~> 2.6)
102
102
  rainbow (3.1.1)
103
103
  rake (13.2.1)
104
- rdoc (6.6.3.1)
104
+ rdoc (6.7.0)
105
105
  psych (>= 4.0.0)
106
106
  regexp_parser (2.9.2)
107
107
  reline (0.5.7)
@@ -171,7 +171,7 @@ GEM
171
171
  webrick (1.8.1)
172
172
  yaml (0.3.0)
173
173
  yard (0.9.36)
174
- zeitwerk (2.6.14)
174
+ zeitwerk (2.6.15)
175
175
 
176
176
  PLATFORMS
177
177
  arm64-darwin-22
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/factbase.gemspec CHANGED
@@ -25,17 +25,21 @@ require_relative 'lib/factbase'
25
25
 
26
26
  Gem::Specification.new do |s|
27
27
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
28
- s.required_ruby_version = '>=2.3'
28
+ s.required_ruby_version = '>=3.0'
29
29
  s.name = 'factbase'
30
30
  s.version = Factbase::VERSION
31
31
  s.license = 'MIT'
32
32
  s.summary = 'Factbase'
33
- s.description = 'Fact base in memory and on disc'
33
+ s.description =
34
+ 'A primitive in-memory collection of key-value records ' \
35
+ 'known as "facts," with an ability to insert facts, add properties ' \
36
+ 'to facts, and delete facts. There is no ability to modify facts. ' \
37
+ 'It is also possible to find facts using Lisp-alike query predicates. ' \
38
+ 'An entire factbase may be exported to a binary file and imported back.'
34
39
  s.authors = ['Yegor Bugayenko']
35
40
  s.email = 'yegor256@gmail.com'
36
41
  s.homepage = 'http://github.com/yegor256/factbase.rb'
37
42
  s.files = `git ls-files`.split($RS)
38
- s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
39
43
  s.rdoc_options = ['--charset=UTF-8']
40
44
  s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
41
45
  s.add_runtime_dependency 'json', '~> 2.7'
data/lib/factbase/fact.rb CHANGED
@@ -28,10 +28,20 @@ require_relative '../factbase'
28
28
  #
29
29
  # This is an internal class, it is not supposed to be instantiated directly.
30
30
  #
31
+ # It is possible to use for testing directly, for example to make a
32
+ # fact with a single key/value pair inside:
33
+ #
34
+ # require 'factbase/fact'
35
+ # f = Factbase::Fact.new(Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
36
+ # assert_equal(42, f.foo)
37
+ #
31
38
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
32
39
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
33
40
  # License:: MIT
34
41
  class Factbase::Fact
42
+ # Ctor.
43
+ # @param [Mutex] mutex A mutex to use for maps synchronization
44
+ # @param [Hash] map A map of key/value pairs
35
45
  def initialize(mutex, map)
36
46
  @mutex = mutex
37
47
  @map = map
@@ -48,7 +58,7 @@ class Factbase::Fact
48
58
  k = args[0].to_s
49
59
  if k.end_with?('=')
50
60
  kk = k[0..-2]
51
- raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-z][_a-zA-Z0-9]*$/)
61
+ raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-z_][_a-zA-Z0-9]*$/)
52
62
  raise "Prohibited prop name '#{kk}'" if kk == 'to_s'
53
63
  v = args[1]
54
64
  raise "Prop value can't be nil" if v.nil?
@@ -129,14 +129,14 @@ 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)
138
138
  else
139
- raise "Wrong symbol format (#{t})" unless t.match?(/^[a-z][a-zA-Z0-9_]*$/)
139
+ raise "Wrong symbol format (#{t})" unless t.match?(/^[_a-z][a-zA-Z0-9_]*$/)
140
140
  t.to_sym
141
141
  end
142
142
  end
data/lib/factbase/term.rb CHANGED
@@ -27,6 +27,15 @@ require_relative 'fact'
27
27
  #
28
28
  # This is an internal class, it is not supposed to be instantiated directly.
29
29
  #
30
+ # It is possible to use for testing directly, for example to make a
31
+ # term with two arguments:
32
+ #
33
+ # require 'factbase/fact'
34
+ # require 'factbase/term'
35
+ # f = Factbase::Fact.new(Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
36
+ # t = Factbase::Term.new(:lt, [:foo, 50])
37
+ # assert(t.evaluate(f))
38
+ #
30
39
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
31
40
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
32
41
  # License:: MIT
@@ -93,19 +102,19 @@ class Factbase::Term
93
102
 
94
103
  def not(fact, maps)
95
104
  assert_args(1)
96
- !only_bool(the_value(0, fact, maps))
105
+ !only_bool(the_values(0, fact, maps))
97
106
  end
98
107
 
99
108
  def or(fact, maps)
100
109
  (0..@operands.size - 1).each do |i|
101
- return true if only_bool(the_value(i, fact, maps))
110
+ return true if only_bool(the_values(i, fact, maps))
102
111
  end
103
112
  false
104
113
  end
105
114
 
106
115
  def and(fact, maps)
107
116
  (0..@operands.size - 1).each do |i|
108
- return false unless only_bool(the_value(i, fact, maps))
117
+ return false unless only_bool(the_values(i, fact, maps))
109
118
  end
110
119
  true
111
120
  end
@@ -149,16 +158,70 @@ class Factbase::Term
149
158
  by_symbol(0, fact).nil?
150
159
  end
151
160
 
161
+ def nonil(fact, maps)
162
+ assert_args(2)
163
+ v = the_values(0, fact, maps)
164
+ return v unless v.nil?
165
+ the_values(1, fact, maps)
166
+ end
167
+
168
+ def at(fact, maps)
169
+ assert_args(2)
170
+ i = the_values(0, fact, maps)
171
+ raise 'Too many values at first position, one expected' unless i.size == 1
172
+ i = i[0]
173
+ return nil if i.nil?
174
+ v = the_values(1, fact, maps)
175
+ return nil if v.nil?
176
+ v[i]
177
+ end
178
+
179
+ def prev(fact, maps)
180
+ assert_args(1)
181
+ before = @prev
182
+ v = the_values(0, fact, maps)
183
+ @prev = v
184
+ before
185
+ end
186
+
187
+ def many(fact, maps)
188
+ assert_args(1)
189
+ v = the_values(0, fact, maps)
190
+ !v.nil? && v.size > 1
191
+ end
192
+
193
+ def one(fact, maps)
194
+ assert_args(1)
195
+ v = the_values(0, fact, maps)
196
+ !v.nil? && v.size == 1
197
+ end
198
+
199
+ def plus(fact, maps)
200
+ arithmetic(:+, fact, maps)
201
+ end
202
+
203
+ def minus(fact, maps)
204
+ arithmetic(:-, fact, maps)
205
+ end
206
+
207
+ def times(fact, maps)
208
+ arithmetic(:*, fact, maps)
209
+ end
210
+
211
+ def div(fact, maps)
212
+ arithmetic(:/, fact, maps)
213
+ end
214
+
152
215
  def eq(fact, maps)
153
- arithmetic(:==, fact, maps)
216
+ cmp(:==, fact, maps)
154
217
  end
155
218
 
156
219
  def lt(fact, maps)
157
- arithmetic(:<, fact, maps)
220
+ cmp(:<, fact, maps)
158
221
  end
159
222
 
160
223
  def gt(fact, maps)
161
- arithmetic(:>, fact, maps)
224
+ cmp(:>, fact, maps)
162
225
  end
163
226
 
164
227
  def size(fact, _maps)
@@ -178,20 +241,20 @@ class Factbase::Term
178
241
 
179
242
  def matches(fact, maps)
180
243
  assert_args(2)
181
- str = the_value(0, fact, maps)
244
+ str = the_values(0, fact, maps)
182
245
  return false if str.nil?
183
246
  raise 'Exactly one string expected' unless str.size == 1
184
- re = the_value(1, fact, maps)
247
+ re = the_values(1, fact, maps)
185
248
  raise 'Regexp is nil' if re.nil?
186
249
  raise 'Exactly one regexp expected' unless re.size == 1
187
250
  str[0].to_s.match?(re[0])
188
251
  end
189
252
 
190
- def arithmetic(op, fact, maps)
253
+ def cmp(op, fact, maps)
191
254
  assert_args(2)
192
- lefts = the_value(0, fact, maps)
255
+ lefts = the_values(0, fact, maps)
193
256
  return false if lefts.nil?
194
- rights = the_value(1, fact, maps)
257
+ rights = the_values(1, fact, maps)
195
258
  return false if rights.nil?
196
259
  lefts.any? do |l|
197
260
  l = l.floor if l.is_a?(Time) && op == :==
@@ -202,6 +265,17 @@ class Factbase::Term
202
265
  end
203
266
  end
204
267
 
268
+ def arithmetic(op, fact, maps)
269
+ assert_args(2)
270
+ lefts = the_values(0, fact, maps)
271
+ raise 'The first argument is NIL, while literal expected' if lefts.nil?
272
+ raise 'Too many values at first position, one expected' unless lefts.size == 1
273
+ rights = the_values(1, fact, maps)
274
+ raise 'The second argument is NIL, while literal expected' if rights.nil?
275
+ raise 'Too many values at second position, one expected' unless rights.size == 1
276
+ lefts[0].send(op, rights[0])
277
+ end
278
+
205
279
  def defn(_fact, _maps)
206
280
  fn = @operands[0]
207
281
  raise 'A symbol expected as first argument of defn' unless fn.is_a?(Symbol)
@@ -269,7 +343,7 @@ class Factbase::Term
269
343
  fact[k]
270
344
  end
271
345
 
272
- def the_value(pos, fact, maps)
346
+ def the_values(pos, fact, maps)
273
347
  v = @operands[pos]
274
348
  v = v.evaluate(fact, maps) if v.is_a?(Factbase::Term)
275
349
  v = fact[v.to_s] if v.is_a?(Symbol)
data/lib/factbase.rb CHANGED
@@ -24,12 +24,32 @@ require 'json'
24
24
  require 'yaml'
25
25
 
26
26
  # Factbase.
27
+ #
28
+ # This is an entry point to a factbase:
29
+ #
30
+ # fb = Factbase.new
31
+ # f = fb.insert # new fact created
32
+ # f.name = 'Jeff Lebowski'
33
+ # f.age = 42
34
+ # found = f.query('(gt 20 age)').each.to_a[0]
35
+ # assert(found.age == 42)
36
+ #
37
+ # A factbase may be exported to a file and then imported back:
38
+ #
39
+ # fb1 = Factbase.new
40
+ # File.writebin(file, fb1.export)
41
+ # fb2 = Factbase.new # it's empty
42
+ # fb2.import(File.readbin(file))
43
+ #
44
+ # It's important to use +writebin+ and +readbin+, because the content is
45
+ # a chain of bytes, not a text.
46
+ #
27
47
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
28
48
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
29
49
  # License:: MIT
30
50
  class Factbase
31
51
  # Current version of the gem (changed by .rultor.yml on every release)
32
- VERSION = '0.0.35'
52
+ VERSION = '0.0.37'
33
53
 
34
54
  # Constructor.
35
55
  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,13 +53,21 @@ 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,
62
- '(eq (size hello) 0)' => 3,
70
+ '(eq (size _hello) 0)' => 3,
63
71
  '(eq num pi)' => 0,
64
72
  '(absent time)' => 2,
65
73
  '(eq pi (agg (eq num 0) (sum pi)))' => 1,
@@ -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 = {})
@@ -49,8 +49,8 @@ class TestFactbase < Minitest::Test
49
49
  f2 = Factbase.new
50
50
  f1.insert.foo = 42
51
51
  Tempfile.open do |f|
52
- File.write(f.path, f1.export)
53
- f2.import(File.read(f.path))
52
+ File.binwrite(f.path, f1.export)
53
+ f2.import(File.binread(f.path))
54
54
  end
55
55
  assert_equal(1, f2.query('(eq foo 42)').each.to_a.count)
56
56
  end
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.35
4
+ version: 0.0.37
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-26 00:00:00.000000000 Z
11
+ date: 2024-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -66,7 +66,11 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0.3'
69
- description: Fact base in memory and on disc
69
+ description: A primitive in-memory collection of key-value records known as "facts,"
70
+ with an ability to insert facts, add properties to facts, and delete facts. There
71
+ is no ability to modify facts. It is also possible to find facts using Lisp-alike
72
+ query predicates. An entire factbase may be exported to a binary file and imported
73
+ back.
70
74
  email: yegor256@gmail.com
71
75
  executables: []
72
76
  extensions: []
@@ -138,7 +142,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
138
142
  requirements:
139
143
  - - ">="
140
144
  - !ruby/object:Gem::Version
141
- version: '2.3'
145
+ version: '3.0'
142
146
  required_rubygems_version: !ruby/object:Gem::Requirement
143
147
  requirements:
144
148
  - - ">="