factbase 0.19.9 → 0.19.11

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: c7c832590816ff7438ea62c6692d2567775088ab27567d407b2294b8caafced7
4
- data.tar.gz: 3c4b7313117776731de9b91cca81c472cd6dfe9dd64e7390381e54194b912b77
3
+ metadata.gz: 712cd682bc9457b563afc7a855e56cad191d2a302406e0e4e9f886ce7fa83af3
4
+ data.tar.gz: cd3a312b78a152b7c9631777473e7465648e6b57d31d713d37d93c63834048ed
5
5
  SHA512:
6
- metadata.gz: b7569a94be51517bb2860cf368849aaa22f89f96b04a16a29fafba294c6ad72c8b03e359f35024a482a82f657edfb36710ec7f60504ff4422663a96cc0a3ebc8
7
- data.tar.gz: 7991d24eae8705444c781b35e00fe74b3f3087e015b3ac2cc1d59d250e0afddd70b7c23006dabf85d4d14dbc2c920a78f1e7e6068edc235c0b45dbdb4af5c17b
6
+ metadata.gz: 013ff487bbd8d2379224361b7794d5219d3235c9c66f645ebe1d6dd928d2298f1a411240328839ea48e08f7dd4c1161cde173530c52f5f8a5afdab52c66cce98
7
+ data.tar.gz: cf57ff6a8f881224a2124306871d245cab380331905fd0ec1acecd381a46d636a7877867a8f1ef27611f1dafe4f1e777c1a01aa57f08b35eb3486682aef22ad3
data/Gemfile.lock CHANGED
@@ -16,7 +16,7 @@ PATH
16
16
  GEM
17
17
  remote: https://rubygems.org/
18
18
  specs:
19
- ansi (1.5.0)
19
+ ansi (1.6.0)
20
20
  ast (2.4.3)
21
21
  backtrace (0.4.1)
22
22
  benchmark (0.5.0)
@@ -38,13 +38,13 @@ GEM
38
38
  loog (0.8.0)
39
39
  ellipsized
40
40
  logger (~> 1.0)
41
- minitest (6.0.2)
41
+ minitest (6.0.6)
42
42
  drb (~> 2.0)
43
43
  prism (~> 1.5)
44
- minitest-reporters (1.7.1)
44
+ minitest-reporters (1.8.0)
45
45
  ansi
46
46
  builder
47
- minitest (>= 5.0)
47
+ minitest (>= 5.0, < 7)
48
48
  ruby-progressbar
49
49
  nokogiri (1.19.0-arm64-darwin)
50
50
  racc (~> 1.4)
@@ -64,14 +64,14 @@ GEM
64
64
  psych (5.3.1)
65
65
  date
66
66
  stringio
67
- qbash (0.8.0)
67
+ qbash (0.8.4)
68
68
  backtrace (> 0)
69
69
  elapsed (> 0)
70
70
  loog (> 0)
71
71
  tago (> 0)
72
72
  racc (1.8.1)
73
73
  rainbow (3.1.1)
74
- rake (13.3.1)
74
+ rake (13.4.2)
75
75
  rdoc (7.1.0)
76
76
  erb
77
77
  psych (>= 4.0.0)
data/README.md CHANGED
@@ -129,8 +129,7 @@ There are some boolean terms available in a query
129
129
  * `(not b)` is the inverse of `b`
130
130
  * `(or b1 b2 ...)` is `true` if at least one argument is `true`
131
131
  * `(and b1 b2 ...)` — if all arguments are `true`
132
- * `(when b1 b2)` — if `b1` is `true` and `b2` is `true`
133
- or `b1` is `false`
132
+ * `(when b1 b2)` — true if `b1` is `false`, or if both `b1` and `b2` are `true`
134
133
  * `(exists p)` — if `p` property exists
135
134
  * `(absent p)` — if `p` property is absent
136
135
  * `(zero v)` — if any `v` equals to zero
@@ -145,6 +144,9 @@ There are string manipulators:
145
144
  * `(concat v1 v2 v3 ...)` — concatenates all `v`
146
145
  * `(sprintf v v1 v2 ...)` — creates a string by `v` format with params
147
146
  * `(matches v s)` — if any `v` matches the `s` regular expression
147
+ * `(contains v s)` — if any value of `v` contains any value of `s` (substring match)
148
+ * `(starts_with v s)` — if any `v` starts with `s`
149
+ * `(ends_with v s)` — if any `v` ends with `s`
148
150
 
149
151
  There are a few terms that return non-boolean values:
150
152
 
@@ -152,7 +154,7 @@ There are a few terms that return non-boolean values:
152
154
  * `(size v)` is the cardinality of `v` (zero if `v` is `nil`)
153
155
  * `(type v)` is the type of `v`
154
156
  (`"String"`, `"Integer"`, `"Float"`, `"Time"`, or `"Array"`)
155
- * `(either v1 v1)` is `v2` if `v1` is `nil`
157
+ * `(either v1 v2)` is `v2` if `v1` is `nil`
156
158
 
157
159
  It's possible to modify the facts retrieved, on fly:
158
160
 
@@ -227,6 +229,80 @@ There are some system-level terms:
227
229
  * `(env v1 v2)` returns the value of environment variable `v1` or the string
228
230
  `v2` if it's not set
229
231
 
232
+ ## Architecture
233
+
234
+ The entire database is a single flat [Ruby](https://www.ruby-lang.org/en/)
235
+ `Array` of `Hash` objects held in RAM (`Factbase#@maps`). There are no
236
+ tables, schemas, or type enforcement beyond four scalar types: `Integer`,
237
+ `Float`, `String`, and `Time`. This contrasts with
238
+ [SQLite](https://sqlite.org/) (fixed-column tables on disk) and
239
+ [MongoDB](https://www.mongodb.com/) (typed document collections). New
240
+ programmers must understand that all data vanishes on process exit unless
241
+ `export`/`import` is called explicitly.
242
+
243
+ Each property of a fact is a non-empty ordered set of values rather than a
244
+ single value. Assigning `f.foo = 1` then `f.foo = 2` produces
245
+ `f['foo'] == [1, 2]`; each assignment appends. Reading `f.foo` returns
246
+ the first element; `f['foo']` returns the full array. This accumulative
247
+ semantics differs from [SQL](https://www.iso.org/standard/76583.html)
248
+ (one value per column) and most NoSQL stores where assignment overwrites.
249
+ New programmers must expect multi-element arrays on every property read.
250
+
251
+ Queries use a custom Lisp-style
252
+ [S-expression](https://en.wikipedia.org/wiki/S-expression) language:
253
+ `(and (eq kind 'book') (gt age 10))`. `Factbase::Syntax` tokenizes and
254
+ parses a query string into an AST of `Factbase::Term` objects;
255
+ `Factbase::Query#each` evaluates that AST against every fact. This
256
+ differs from [SQL](https://www.iso.org/standard/76583.html),
257
+ [XPath](https://www.w3.org/TR/xpath-31/), and
258
+ [JSONPath](https://datatracker.ietf.org/doc/html/rfc9535). New
259
+ programmers add operators by implementing a term class, not by modifying
260
+ parser grammar.
261
+
262
+ Each query operator (`eq`, `gt`, `agg`, `join`, etc.) is a separate class
263
+ under `lib/factbase/terms/`. `Factbase::Term` holds a dispatch hash
264
+ (`@terms`) mapping operator symbols to instances and delegates `evaluate`
265
+ and `predict` calls there. This is not a class hierarchy — adding a new
266
+ operator requires a new file in `terms/` and a registration line in the
267
+ `Factbase::Term` constructor. New programmers extending the query
268
+ language must follow this two-step pattern.
269
+
270
+ Transactions are ACID and implemented via lazy copy-on-write journaling.
271
+ `Factbase#txn` wraps the array in `Factbase::LazyTaped`, which defers
272
+ physical duplication of hash objects until the first write. Inserts,
273
+ deletes, and property additions are tracked by Ruby `object_id`. On
274
+ commit the journal is replayed into the main array; raising
275
+ `Factbase::Rollback` discards it. Nesting transactions is explicitly
276
+ forbidden by `Factbase::Light`. This differs from SQLite's
277
+ [WAL](https://sqlite.org/wal.html) and PostgreSQL's
278
+ [MVCC](https://www.postgresql.org/docs/current/mvcc.html).
279
+
280
+ Cross-cutting capabilities — thread safety, indexing, constraint
281
+ validation, logging, and change counting — are added via decorators:
282
+ `Factbase::SyncFactbase`, `Factbase::IndexedFactbase`,
283
+ `Factbase::Rules`, `Factbase::Logged`, and `Factbase::Tallied`. The
284
+ [`decoor`](https://github.com/yegor256/decoor) gem provides delegation
285
+ boilerplate. The bare `Factbase` class is not thread-safe; new
286
+ programmers must wrap it with `SyncFactbase` before sharing across
287
+ threads.
288
+
289
+ Persistence uses Ruby's
290
+ [`Marshal`](https://ruby-doc.org/core/Marshal.html), serializing the
291
+ internal array of hashes to a binary blob via `Marshal.dump`. The format
292
+ is Ruby-version-specific and not portable across major Ruby versions or
293
+ platforms, unlike [JSON](https://www.json.org/json-en.html) or
294
+ [Protocol Buffers](https://protobuf.dev/). Output-only decorators
295
+ `Factbase::ToJSON`, `Factbase::ToXML`, and `Factbase::ToYAML` exist but
296
+ do not support round-trip import.
297
+
298
+ `Factbase::IndexedFactbase` lazily builds a hash-based inverted index for
299
+ equality queries, keyed by array `object_id`, property name, and
300
+ operator. The index is built incrementally on each query and invalidated
301
+ entirely on any mutation (delete or property addition). Without this
302
+ decorator every `query#each` call performs a full linear scan over all
303
+ facts. New programmers should add `IndexedFactbase` whenever the
304
+ factbase holds more than a few thousand facts.
305
+
230
306
  ## How to contribute
231
307
 
232
308
  Read
@@ -249,91 +325,25 @@ This is the result of the benchmark:
249
325
 
250
326
  <!-- benchmark_begin -->
251
327
  ```text
252
-
253
- query all facts from an empty factbase 0.00
254
- insert 20000 facts 0.66
255
- export 20000 facts 0.02
256
- import 410996 bytes (20000 facts) 0.02
257
- insert 10 facts 0.00
258
- query 10 times w/txn 2.13
259
- query 10 times w/o txn 0.12
260
- modify 10 attrs w/txn 1.62
261
- delete 10 facts w/txn 10.22
262
- build index on 5000 facts 0.03
263
- export 5000 facts with index 0.04
264
- import 5000 facts with persisted index 0.03
265
- query 5000 facts using persisted index 0.08
266
- export 5000 facts without index 0.02
267
- import 5000 facts without index 0.01
268
- query 5000 facts building index on-the-fly 0.07
269
- query 15k facts sel: 20% card: 10 absent plain 0.60
270
- query 15k facts sel: 20% card: 10 absent indexed(cold) 0.17
271
- query 15k facts sel: 20% card: 10 absent indexed(warm) 0.16
272
- query 15k facts sel: 20% card: 10 exists plain 0.57
273
- query 15k facts sel: 20% card: 10 exists indexed(cold) 0.17
274
- query 15k facts sel: 20% card: 10 exists indexed(warm) 0.13
275
- query 15k facts sel: 20% card: 10 eq plain 0.86
276
- query 15k facts sel: 20% card: 10 eq indexed(cold) 0.26
277
- query 15k facts sel: 20% card: 10 eq indexed(warm) 0.19
278
- query 15k facts sel: 20% card: 10 not plain 1.16
279
- query 15k facts sel: 20% card: 10 not indexed(cold) 0.53
280
- query 15k facts sel: 20% card: 10 not indexed(warm) 0.51
281
- query 15k facts sel: 20% card: 10 gt plain 0.88
282
- query 15k facts sel: 20% card: 10 gt indexed(cold) 0.29
283
- query 15k facts sel: 20% card: 10 gt indexed(warm) 0.24
284
- query 15k facts sel: 20% card: 10 lt plain 0.87
285
- query 15k facts sel: 20% card: 10 lt indexed(cold) 0.29
286
- query 15k facts sel: 20% card: 10 lt indexed(warm) 0.20
287
- query 15k facts sel: 20% card: 10 and eq plain 1.43
288
- query 15k facts sel: 20% card: 10 and eq indexed(cold) 0.91
289
- query 15k facts sel: 20% card: 10 and eq indexed(warm) 0.50
290
- query 15k facts sel: 20% card: 10 and complex plain 1.38
291
- query 15k facts sel: 20% card: 10 and complex indexed(cold) 0.51
292
- query 15k facts sel: 20% card: 10 and complex indexed(warm) 0.45
293
- query 15k facts sel: 20% card: 10 one plain 0.75
294
- query 15k facts sel: 20% card: 10 one indexed(cold) 0.21
295
- query 15k facts sel: 20% card: 10 one indexed(warm) 0.16
296
- query 15k facts sel: 20% card: 10 or plain 2.02
297
- query 15k facts sel: 20% card: 10 or indexed(cold) 0.46
298
- query 15k facts sel: 20% card: 10 or indexed(warm) 0.32
299
- query 15k facts sel: 20% card: 10 unique plain 1.87
300
- query 15k facts sel: 20% card: 10 unique indexed(cold) 0.67
301
- query 15k facts sel: 20% card: 10 unique indexed(warm) 0.42
302
- (and (eq what 'issue-was-closed') (exists... -> 200 1.08
303
- (and (eq what 'issue-was-closed') (exists... -> 200/txn 1.24
304
- (and (eq what 'issue-was-closed') (exists... -> zero 1.08
305
- (and (eq what 'issue-was-closed') (exists... -> zero/txn 1.28
306
- transaction rollback on factbase with 100000 facts 0.01
307
- (gt time '2024-03-23T03:21:43Z') 0.31
308
- (gt cost 50) 0.14
309
- (eq title 'Object Thinking 5000') 0.02
310
- (and (eq foo 42.998) (or (gt bar 200) (absent z... 0.03
311
- (and (exists foo) (not (exists blue))) 1.23
312
- (eq id (agg (always) (max id))) 2.80
313
- (join "c<=cost,b<=bar" (eq id (agg (always) (ma... 4.44
314
- (and (eq what "foo") (join "w<=what" (and (eq i... 7.39
315
- delete! 0.44
316
- (and (eq issue *) (eq repository *) (eq what '*') (eq where '*')) 0.41
317
- Taped.append() x50000 0.02
318
- Taped.each() x125 1.10
319
- Taped.delete_if() x375 0.86
320
- 50000 facts: plain read (no txn) 4.10
321
- 50000 facts: read-only txn (no copy) 5.33
322
- 50000 facts: plain insert (no txn) 0.00
323
- 50000 facts: insert in txn (no copy triggered) 0.00
324
- 50000 facts: plain modify (no txn) 28.59
325
- 50000 facts: modify in txn (copy triggered) 37.78
326
- 100000 facts: plain read (no txn) 8.33
327
- 100000 facts: read-only txn (no copy) 12.80
328
- 100000 facts: plain insert (no txn) 0.00
329
- 100000 facts: insert in txn (no copy triggered) 0.00
330
- 100000 facts: plain modify (no txn) 57.26
331
- 100000 facts: modify in txn (copy triggered) 75.84
328
+ user
329
+ void scan 0.001049
330
+ 20k facts: export: 2980KB 0.840193
331
+ 20k facts: import: 2980KB 1.033492
332
+ 50k facts: read 0.000179
333
+ 50k facts: read in txn 0.002282
334
+ 50k facts: insert 0.000082
335
+ 50k facts: insert in txn 0.000613
336
+ 50k facts: modify 1.427349
337
+ 50k facts: modify in txn 2.773065
338
+ 12k facts: large query: match 3k 13.166170
339
+ 12k facts: large query: match 3k in txn 18.502015
340
+ 12k facts: large query: match zero 14.234812
341
+ 12k facts: large query: match zero in txn 19.847547
332
342
  ```
333
343
 
334
344
  The results were calculated in [this GHA job][benchmark-gha]
335
- on 2026-02-26 at 06:08,
345
+ on 2026-05-23 at 03:40,
336
346
  on Linux with 4 CPUs.
337
347
  <!-- benchmark_end -->
338
348
 
339
- [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/22430010182
349
+ [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/26322478578
data/Rakefile CHANGED
@@ -61,27 +61,30 @@ RuboCop::RakeTask.new(:rubocop) do |task|
61
61
  end
62
62
 
63
63
  desc 'Benchmark them all'
64
- task :benchmark, [:name] do |_t, args|
65
- bname = args[:name] || 'all'
64
+ task :benchmark, [:name, :cycles] do |_t, args|
65
+ bname = args[:name] || 'essential'
66
+ cycles = (args[:cycles] || 5).to_i
66
67
  require_relative 'lib/factbase'
67
68
  require_relative 'lib/factbase/cached/cached_factbase'
68
69
  require_relative 'lib/factbase/indexed/indexed_factbase'
69
70
  require_relative 'lib/factbase/sync/sync_factbase'
70
71
  require 'benchmark'
71
72
  Benchmark.bm(60) do |b|
72
- fb = Factbase.new
73
- fb = Factbase::CachedFactbase.new(fb)
74
- fb = Factbase::IndexedFactbase.new(fb)
75
- fb = Factbase::SyncFactbase.new(fb)
76
- if bname == 'all'
77
- Dir['benchmark/bench_*.rb'].each do |f|
78
- require_relative f
79
- Kernel.send(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb)
73
+ files =
74
+ case bname
75
+ when 'all'
76
+ Dir['benchmark/bench_*.rb']
77
+ when 'essential'
78
+ %w[empty serialization txns large_query].map { |f| "benchmark/bench_#{f}.rb" }
79
+ else
80
+ ["benchmark/#{bname}.rb"]
80
81
  end
81
- else
82
- f = "benchmark/#{bname}.rb"
82
+ files.each do |f|
83
83
  require_relative f
84
- Kernel.send(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb)
84
+ fb = Factbase.new
85
+ fb = Factbase::IndexedFactbase.new(fb)
86
+ fb = Factbase::CachedFactbase.new(fb)
87
+ Kernel.send(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb, cycles)
85
88
  end
86
89
  end
87
90
  end
@@ -61,11 +61,12 @@ class Factbase::IndexedFactbase
61
61
  # Run an ACID transaction.
62
62
  # @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
63
63
  def txn
64
+ inner_idx = {}
64
65
  result =
65
66
  @origin.txn do |fbt|
66
- yield Factbase::IndexedFactbase.new(fbt, @idx, @fresh)
67
+ yield Factbase::IndexedFactbase.new(fbt, inner_idx, @fresh)
67
68
  end
68
- @idx.clear
69
+ @idx.clear if result.deleted.positive? || result.added.positive?
69
70
  @fresh.clear
70
71
  result
71
72
  end
@@ -142,7 +142,7 @@ class Factbase::Syntax
142
142
  t[1..-2]
143
143
  elsif t.match?(/^(\+|-)?[0-9]+$/)
144
144
  t.to_i
145
- elsif t.match?(/^(\+|-)?[0-9]+\.[0-9]+(e\+[0-9]+)?$/)
145
+ elsif t.match?(/^(\+|-)?[0-9]+\.[0-9]+(e(\+|-)[0-9]+)?$/)
146
146
  t.to_f
147
147
  elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
148
148
  Time.parse(t)
@@ -8,7 +8,7 @@ require 'others'
8
8
  require_relative '../factbase'
9
9
  require_relative 'churn'
10
10
 
11
- # A decorator of a Factbase, that count all operations and then returns
11
+ # A decorator of a Factbase, that counts all operations and then returns
12
12
  # an instance of Factbase::Churn.
13
13
  #
14
14
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
data/lib/factbase/term.rb CHANGED
@@ -12,6 +12,9 @@ require_relative 'terms/prev'
12
12
  require_relative 'terms/concat'
13
13
  require_relative 'terms/sprintf'
14
14
  require_relative 'terms/matches'
15
+ require_relative 'terms/contains'
16
+ require_relative 'terms/starts_with'
17
+ require_relative 'terms/ends_with'
15
18
  require_relative 'terms/traced'
16
19
  require_relative 'terms/assert'
17
20
  require_relative 'terms/env'
@@ -108,6 +111,9 @@ class Factbase::Term < Factbase::TermBase
108
111
  concat: Factbase::Concat.new(operands),
109
112
  sprintf: Factbase::Sprintf.new(operands),
110
113
  matches: Factbase::Matches.new(operands),
114
+ contains: Factbase::Contains.new(operands),
115
+ starts_with: Factbase::StartsWith.new(operands),
116
+ ends_with: Factbase::EndsWith.new(operands),
111
117
  traced: Factbase::Traced.new(operands),
112
118
  assert: Factbase::Assert.new(operands),
113
119
  env: Factbase::Env.new(operands),
@@ -3,12 +3,10 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- # Test for unique term.
6
+ # Base class for all terms.
7
7
  # Author:: Volodya Lombrozo (volodya.lombrozo@gmail.com)
8
8
  # Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
9
9
  # License:: MIT
10
-
11
- # Base class for all terms.
12
10
  class Factbase::TermBase
13
11
  # Turns it into a string.
14
12
  # @return [String] The string of it
@@ -31,8 +31,23 @@ class Factbase::Compare < Factbase::TermBase
31
31
  l = l.floor if l.is_a?(Time) && @op == :==
32
32
  rights.any? do |r|
33
33
  r = r.floor if r.is_a?(Time) && @op == :==
34
- l.send(@op, r)
34
+ _compare(l, r)
35
35
  end
36
36
  end
37
37
  end
38
+
39
+ private
40
+
41
+ # Compare values with a contextual error if Ruby rejects the operands.
42
+ # @param [Object] left Left value
43
+ # @param [Object] right Right value
44
+ # @return [Boolean] The result of the comparison
45
+ def _compare(left, right)
46
+ left.send(@op, right)
47
+ rescue ArgumentError => e
48
+ raise(
49
+ "Cannot compare #{left.inspect} (#{left.class}) " \
50
+ "with #{right.inspect} (#{right.class}) using (compare #{@op}): #{e.message}"
51
+ )
52
+ end
38
53
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative 'base'
7
+ require_relative 'compare'
8
+
9
+ # Represents a 'contains' term in the Factbase.
10
+ # Returns true if any value of the left operand contains any value of the right
11
+ # as a substring. Operates on string values via `String#include?`.
12
+ class Factbase::Contains < Factbase::TermBase
13
+ # Constructor.
14
+ # @param [Array] operands Operands
15
+ def initialize(operands)
16
+ super()
17
+ @op = Factbase::Compare.new(:include?, operands)
18
+ end
19
+
20
+ # Evaluate term on a fact.
21
+ # @param [Factbase::Fact] fact The fact
22
+ # @param [Array<Factbase::Fact>] maps All maps available
23
+ # @param [Factbase] fb Factbase to use for sub-queries
24
+ # @return [Boolean] True if any left value includes any right value
25
+ def evaluate(fact, maps, fb)
26
+ @op.evaluate(fact, maps, fb)
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative 'base'
7
+ require_relative 'compare'
8
+
9
+ # Represents an 'ends_with' term in the Factbase.
10
+ # Returns true if any value of the left operand ends with any value of the right.
11
+ class Factbase::EndsWith < Factbase::TermBase
12
+ # Constructor.
13
+ # @param [Array] operands Operands
14
+ def initialize(operands)
15
+ super()
16
+ @op = Factbase::Compare.new(:end_with?, operands)
17
+ end
18
+
19
+ # Evaluate term on a fact.
20
+ # @param [Factbase::Fact] fact The fact
21
+ # @param [Array<Factbase::Fact>] maps All maps available
22
+ # @param [Factbase] fb Factbase to use for sub-queries
23
+ # @return [Boolean] True if any left value ends with any right value
24
+ def evaluate(fact, maps, fb)
25
+ @op.evaluate(fact, maps, fb)
26
+ end
27
+ end
@@ -14,6 +14,7 @@ class Factbase::Matches < Factbase::TermBase
14
14
  def initialize(operands)
15
15
  super()
16
16
  @operands = operands
17
+ @regexps = {}
17
18
  end
18
19
 
19
20
  # Evaluate term on a fact.
@@ -29,6 +30,15 @@ class Factbase::Matches < Factbase::TermBase
29
30
  re = _values(1, fact, maps, fb)
30
31
  raise 'Regexp is nil' if re.nil?
31
32
  raise 'Exactly one regexp is expected' unless re.size == 1
32
- str[0].to_s.match?(re[0])
33
+ str[0].to_s.match?(regexp(re[0]))
34
+ end
35
+
36
+ private
37
+
38
+ def regexp(pattern)
39
+ key = pattern.to_s
40
+ @regexps[key] ||= Regexp.new(key)
41
+ rescue RegexpError => e
42
+ raise "Invalid regexp '#{key}': #{e.message}"
33
43
  end
34
44
  end
@@ -24,6 +24,14 @@ class Factbase::Sprintf < Factbase::TermBase
24
24
  def evaluate(fact, maps, fb)
25
25
  fmt = _values(0, fact, maps, fb)[0]
26
26
  ops = (1..(@operands.length - 1)).map { |i| _values(i, fact, maps, fb)&.first }
27
+ formatted(fmt, ops)
28
+ end
29
+
30
+ private
31
+
32
+ def formatted(fmt, ops)
27
33
  format(*([fmt] + ops))
34
+ rescue ArgumentError => e
35
+ raise "Cannot format #{ops.inspect} with '#{fmt}' in (sprintf ...): #{e.message}"
28
36
  end
29
37
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative 'base'
7
+ require_relative 'compare'
8
+
9
+ # Represents a 'starts_with' term in the Factbase.
10
+ # Returns true if any value of the left operand starts with any value of the right.
11
+ class Factbase::StartsWith < Factbase::TermBase
12
+ # Constructor.
13
+ # @param [Array] operands Operands
14
+ def initialize(operands)
15
+ super()
16
+ @op = Factbase::Compare.new(:start_with?, operands)
17
+ end
18
+
19
+ # Evaluate term on a fact.
20
+ # @param [Factbase::Fact] fact The fact
21
+ # @param [Array<Factbase::Fact>] maps All maps available
22
+ # @param [Factbase] fb Factbase to use for sub-queries
23
+ # @return [Boolean] True if any left value starts with any right value
24
+ def evaluate(fact, maps, fb)
25
+ @op.evaluate(fact, maps, fb)
26
+ end
27
+ end
@@ -6,7 +6,7 @@
6
6
  require_relative 'base'
7
7
 
8
8
  # This class represents a specialized 'sum' term.
9
- # This term calculates the sum of values for a specified key.
9
+ # This term calculates the sum of values for a specified key.
10
10
  class Factbase::Sum < Factbase::TermBase
11
11
  # Constructor.
12
12
  # @param [Array] operands Operands
@@ -23,6 +23,14 @@ class Factbase::ToTime < Factbase::TermBase
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
25
  return nil if vv.nil?
26
- Time.parse(vv[0].to_s)
26
+ parse(vv[0])
27
+ end
28
+
29
+ private
30
+
31
+ def parse(value)
32
+ Time.parse(value.to_s)
33
+ rescue ArgumentError => e
34
+ raise "Cannot parse '#{value}' as Time in (to_time ...): #{e.message}"
27
35
  end
28
36
  end
@@ -22,8 +22,7 @@ class Factbase::When < Factbase::TermBase
22
22
  # @return [Boolean] True if first operand is false OR both are true
23
23
  def evaluate(fact, maps, fb)
24
24
  assert_args(2)
25
- a = @operands[0]
26
- b = @operands[1]
27
- !a.evaluate(fact, maps, fb) || (a.evaluate(fact, maps, fb) && b.evaluate(fact, maps, fb))
25
+ return true unless @operands[0].evaluate(fact, maps, fb)
26
+ @operands[1].evaluate(fact, maps, fb)
28
27
  end
29
28
  end
@@ -9,7 +9,7 @@ require_relative '../factbase/flatten'
9
9
 
10
10
  # Factbase to JSON converter.
11
11
  #
12
- # This class helps converting an entire Factbase to YAML format, for example:
12
+ # This class helps converting an entire Factbase to JSON format, for example:
13
13
  #
14
14
  # require 'factbase/to_json'
15
15
  # fb = Factbase.new
@@ -10,7 +10,7 @@ require_relative '../factbase/flatten'
10
10
 
11
11
  # Factbase to XML converter.
12
12
  #
13
- # This class helps converting an entire Factbase to YAML format, for example:
13
+ # This class helps converting an entire Factbase to XML format, for example:
14
14
  #
15
15
  # require 'factbase/to_xml'
16
16
  # fb = Factbase.new
@@ -9,5 +9,5 @@
9
9
  # License:: MIT
10
10
  class Factbase
11
11
  # Current version of the gem (changed by .rultor.yml on every release)
12
- VERSION = '0.19.9' unless const_defined?(:VERSION)
12
+ VERSION = '0.19.11' unless const_defined?(:VERSION)
13
13
  end
data/lib/factbase.rb CHANGED
@@ -102,9 +102,6 @@ class Factbase
102
102
  #
103
103
  # A fact, when inserted, is empty. It doesn't contain any properties.
104
104
  #
105
- # The operation is thread-safe, meaning that different threads may
106
- # insert facts in parallel without breaking the consistency of the factbase.
107
- #
108
105
  # @return [Factbase::Fact] The fact just inserted
109
106
  def insert
110
107
  map = {}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factbase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.9
4
+ version: 0.19.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -222,11 +222,13 @@ files:
222
222
  - lib/factbase/terms/boolean.rb
223
223
  - lib/factbase/terms/compare.rb
224
224
  - lib/factbase/terms/concat.rb
225
+ - lib/factbase/terms/contains.rb
225
226
  - lib/factbase/terms/count.rb
226
227
  - lib/factbase/terms/defn.rb
227
228
  - lib/factbase/terms/div.rb
228
229
  - lib/factbase/terms/either.rb
229
230
  - lib/factbase/terms/empty.rb
231
+ - lib/factbase/terms/ends_with.rb
230
232
  - lib/factbase/terms/env.rb
231
233
  - lib/factbase/terms/eq.rb
232
234
  - lib/factbase/terms/exists.rb
@@ -255,6 +257,7 @@ files:
255
257
  - lib/factbase/terms/size.rb
256
258
  - lib/factbase/terms/sorted.rb
257
259
  - lib/factbase/terms/sprintf.rb
260
+ - lib/factbase/terms/starts_with.rb
258
261
  - lib/factbase/terms/sum.rb
259
262
  - lib/factbase/terms/times.rb
260
263
  - lib/factbase/terms/to_float.rb