factbase 0.19.3 → 0.19.5
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 +4 -4
- data/Gemfile +3 -2
- data/Gemfile.lock +10 -9
- data/README.md +56 -41
- data/Rakefile +1 -1
- data/lib/factbase/indexed/indexed_absent.rb +5 -1
- data/lib/factbase/indexed/indexed_and.rb +1 -1
- data/lib/factbase/indexed/indexed_eq.rb +1 -1
- data/lib/factbase/indexed/indexed_exists.rb +5 -1
- data/lib/factbase/indexed/indexed_fact.rb +3 -3
- data/lib/factbase/indexed/indexed_factbase.rb +12 -4
- data/lib/factbase/indexed/indexed_gt.rb +5 -1
- data/lib/factbase/indexed/indexed_lt.rb +5 -1
- data/lib/factbase/indexed/indexed_not.rb +2 -0
- data/lib/factbase/indexed/indexed_one.rb +5 -1
- data/lib/factbase/indexed/indexed_query.rb +4 -2
- data/lib/factbase/indexed/indexed_unique.rb +39 -17
- data/lib/factbase/lazy_taped.rb +5 -4
- data/lib/factbase/syntax.rb +1 -1
- data/lib/factbase/terms/defn.rb +1 -1
- data/lib/factbase/terms/undef.rb +1 -1
- data/lib/factbase/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd4224329381f390e0d8262e6868f1edf6b4f0ae07c972bf0d22fdc7581b0ea9
|
|
4
|
+
data.tar.gz: 92d79cb596b6b9dc647555c60b0ce7ddc30d270b7b7bdccf1cd8e0a1db4190ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a2a9b52b2576c634702c057b2399dc985c6582c7f371c4feae37c8d794f68a3c71ac4467aed8d05becb00e8b63a5fa8cef2b20882455f2ecc6199d215888ce6
|
|
7
|
+
data.tar.gz: 88c27d05a03f5bf0270ad713ab76a3f592ecb4128f5d16e284fbf3f910f7e443f961c184b5f658647b4caafa4ccac83c2fcdd5c1578c2d6f1d7ac1e6c12dae46
|
data/Gemfile
CHANGED
|
@@ -10,6 +10,7 @@ gem 'benchmark', '~>0.5', require: false
|
|
|
10
10
|
gem 'minitest', '~>6.0', require: false
|
|
11
11
|
gem 'minitest-reporters', '~>1.7', require: false
|
|
12
12
|
gem 'os', '~>1.1', require: false
|
|
13
|
+
gem 'psych', '5.3.1', require: false # GPL
|
|
13
14
|
gem 'qbash', '~>0.4', require: false
|
|
14
15
|
gem 'rake', '~>13.2', require: false
|
|
15
16
|
gem 'rdoc', '7.1.0', require: false # GPL
|
|
@@ -19,6 +20,6 @@ gem 'rubocop-performance', '~>1.25', require: false
|
|
|
19
20
|
gem 'rubocop-rake', '~>0.7', require: false
|
|
20
21
|
gem 'simplecov', '~>0.22', require: false
|
|
21
22
|
gem 'simplecov-cobertura', '~>3.0', require: false
|
|
22
|
-
gem 'stackprof', '
|
|
23
|
+
gem 'stackprof', '0.2.27', require: false, platforms: [:ruby] # GPL
|
|
23
24
|
gem 'threads', '~>0.4', require: false
|
|
24
|
-
gem 'yard', '
|
|
25
|
+
gem 'yard', '0.9.38', require: false # GPL
|
data/Gemfile.lock
CHANGED
|
@@ -25,12 +25,12 @@ GEM
|
|
|
25
25
|
date (3.5.1)
|
|
26
26
|
decoor (0.1.0)
|
|
27
27
|
docile (1.4.1)
|
|
28
|
-
elapsed (0.
|
|
28
|
+
elapsed (0.3.1)
|
|
29
29
|
loog (~> 0.6)
|
|
30
30
|
tago (~> 0.1)
|
|
31
31
|
ellipsized (0.3.0)
|
|
32
32
|
erb (6.0.1)
|
|
33
|
-
json (2.18.
|
|
33
|
+
json (2.18.1)
|
|
34
34
|
language_server-protocol (3.17.0.5)
|
|
35
35
|
lint_roller (1.1.0)
|
|
36
36
|
logger (1.7.0)
|
|
@@ -58,11 +58,11 @@ GEM
|
|
|
58
58
|
parser (3.3.10.1)
|
|
59
59
|
ast (~> 2.4.1)
|
|
60
60
|
racc
|
|
61
|
-
prism (1.
|
|
61
|
+
prism (1.9.0)
|
|
62
62
|
psych (5.3.1)
|
|
63
63
|
date
|
|
64
64
|
stringio
|
|
65
|
-
qbash (0.
|
|
65
|
+
qbash (0.7.2)
|
|
66
66
|
backtrace (> 0)
|
|
67
67
|
elapsed (> 0)
|
|
68
68
|
loog (> 0)
|
|
@@ -76,7 +76,7 @@ GEM
|
|
|
76
76
|
tsort
|
|
77
77
|
regexp_parser (2.11.3)
|
|
78
78
|
rexml (3.4.4)
|
|
79
|
-
rubocop (1.
|
|
79
|
+
rubocop (1.84.1)
|
|
80
80
|
json (~> 2.3)
|
|
81
81
|
language_server-protocol (~> 3.17.0.2)
|
|
82
82
|
lint_roller (~> 1.1.0)
|
|
@@ -84,7 +84,7 @@ GEM
|
|
|
84
84
|
parser (>= 3.3.0.2)
|
|
85
85
|
rainbow (>= 2.2.2, < 4.0)
|
|
86
86
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
87
|
-
rubocop-ast (>= 1.
|
|
87
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
88
88
|
ruby-progressbar (~> 1.7)
|
|
89
89
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
90
90
|
rubocop-ast (1.49.0)
|
|
@@ -113,7 +113,7 @@ GEM
|
|
|
113
113
|
simplecov_json_formatter (0.1.4)
|
|
114
114
|
stackprof (0.2.27)
|
|
115
115
|
stringio (3.2.0)
|
|
116
|
-
tago (0.
|
|
116
|
+
tago (0.7.0)
|
|
117
117
|
threads (0.5.0)
|
|
118
118
|
backtrace (~> 0)
|
|
119
119
|
concurrent-ruby (~> 1.0)
|
|
@@ -141,6 +141,7 @@ DEPENDENCIES
|
|
|
141
141
|
minitest (~> 6.0)
|
|
142
142
|
minitest-reporters (~> 1.7)
|
|
143
143
|
os (~> 1.1)
|
|
144
|
+
psych (= 5.3.1)
|
|
144
145
|
qbash (~> 0.4)
|
|
145
146
|
rake (~> 13.2)
|
|
146
147
|
rdoc (= 7.1.0)
|
|
@@ -150,9 +151,9 @@ DEPENDENCIES
|
|
|
150
151
|
rubocop-rake (~> 0.7)
|
|
151
152
|
simplecov (~> 0.22)
|
|
152
153
|
simplecov-cobertura (~> 3.0)
|
|
153
|
-
stackprof (
|
|
154
|
+
stackprof (= 0.2.27)
|
|
154
155
|
threads (~> 0.4)
|
|
155
|
-
yard (
|
|
156
|
+
yard (= 0.9.38)
|
|
156
157
|
|
|
157
158
|
BUNDLED WITH
|
|
158
159
|
2.6.8
|
data/README.md
CHANGED
|
@@ -42,9 +42,9 @@ file = '/tmp/simple.fb'
|
|
|
42
42
|
f1 = Factbase.new
|
|
43
43
|
f = f1.insert
|
|
44
44
|
f.foo = 42
|
|
45
|
-
File.
|
|
45
|
+
File.binwrite(file, f1.export)
|
|
46
46
|
f2 = Factbase.new
|
|
47
|
-
f2.import(File.
|
|
47
|
+
f2.import(File.binread(file))
|
|
48
48
|
assert(f2.query('(eq foo 42)').each.to_a.size == 1)
|
|
49
49
|
```
|
|
50
50
|
|
|
@@ -80,6 +80,21 @@ churn = fb.churn
|
|
|
80
80
|
assert churn.inserted == 1
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
Properties are accumulative.
|
|
84
|
+
Setting a property again adds a value instead of overwriting:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
f = fb.insert
|
|
88
|
+
f.foo = 42
|
|
89
|
+
f.foo = 43
|
|
90
|
+
assert(f.foo == 42)
|
|
91
|
+
assert(f['foo'] == [42, 43])
|
|
92
|
+
fb.query('(eq foo 43)').each do |f|
|
|
93
|
+
assert(f.foo == 42)
|
|
94
|
+
assert(f['foo'].include?(43))
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
83
98
|
## Terms
|
|
84
99
|
|
|
85
100
|
There are some boolean terms available in a query
|
|
@@ -210,54 +225,54 @@ This is the result of the benchmark:
|
|
|
210
225
|
|
|
211
226
|
<!-- benchmark_begin -->
|
|
212
227
|
```text
|
|
213
|
-
|
|
228
|
+
|
|
214
229
|
query all facts from an empty factbase 0.00
|
|
215
230
|
insert 20000 facts 0.67
|
|
216
231
|
export 20000 facts 0.02
|
|
217
|
-
import
|
|
232
|
+
import 410821 bytes (20000 facts) 0.01
|
|
218
233
|
insert 10 facts 0.04
|
|
219
|
-
query 10 times w/txn 2.
|
|
220
|
-
query 10 times w/o txn 0.
|
|
221
|
-
modify 10 attrs w/txn 1.
|
|
222
|
-
delete 10 facts w/txn
|
|
234
|
+
query 10 times w/txn 2.36
|
|
235
|
+
query 10 times w/o txn 0.07
|
|
236
|
+
modify 10 attrs w/txn 1.71
|
|
237
|
+
delete 10 facts w/txn 3.38
|
|
223
238
|
build index on 5000 facts 0.05
|
|
224
|
-
export 5000 facts with index 0.
|
|
225
|
-
import 5000 facts with persisted index 0.
|
|
226
|
-
query 5000 facts using persisted index 0.
|
|
239
|
+
export 5000 facts with index 0.05
|
|
240
|
+
import 5000 facts with persisted index 0.03
|
|
241
|
+
query 5000 facts using persisted index 0.10
|
|
227
242
|
export 5000 facts without index 0.00
|
|
228
|
-
import 5000 facts without index 0.
|
|
229
|
-
query 5000 facts building index on-the-fly 0.
|
|
230
|
-
(and (eq what 'issue-was-closed') (exists... -> 200 1.
|
|
231
|
-
(and (eq what 'issue-was-closed') (exists... -> 200/txn 1.
|
|
232
|
-
(and (eq what 'issue-was-closed') (exists... -> zero 1.
|
|
233
|
-
(and (eq what 'issue-was-closed') (exists... -> zero/txn 1.
|
|
234
|
-
transaction rollback on factbase with 100000 facts 0.
|
|
235
|
-
(gt time '2024-03-23T03:21:43Z') 0.
|
|
236
|
-
(gt cost 50) 0.
|
|
237
|
-
(eq title 'Object Thinking 5000') 0.
|
|
238
|
-
(and (eq foo 42.998) (or (gt bar 200) (absent z... 0.
|
|
239
|
-
(and (exists foo) (not (exists blue))) 1.
|
|
240
|
-
(eq id (agg (always) (max id))) 2.
|
|
241
|
-
(join "c<=cost,b<=bar" (eq id (agg (always) (ma... 4.
|
|
242
|
-
(and (eq what "foo") (join "w<=what" (and (eq i... 7.
|
|
243
|
-
delete! 0.
|
|
244
|
-
(and (eq issue *) (eq repository *) (eq what '*') (eq where '*'))
|
|
245
|
-
Taped.append() x50000 0.
|
|
246
|
-
Taped.each() x125 1.
|
|
247
|
-
Taped.delete_if() x375 0.
|
|
248
|
-
50000 facts: read-only txn (no copy needed)
|
|
249
|
-
50000 facts: rollback txn (no copy needed)
|
|
250
|
-
50000 facts: insert in txn (copy triggered) 3.
|
|
251
|
-
50000 facts: modify in txn (copy triggered)
|
|
252
|
-
100000 facts: read-only txn (no copy needed)
|
|
253
|
-
100000 facts: rollback txn (no copy needed)
|
|
254
|
-
100000 facts: insert in txn (copy triggered)
|
|
255
|
-
100000 facts: modify in txn (copy triggered)
|
|
243
|
+
import 5000 facts without index 0.01
|
|
244
|
+
query 5000 facts building index on-the-fly 0.09
|
|
245
|
+
(and (eq what 'issue-was-closed') (exists... -> 200 1.11
|
|
246
|
+
(and (eq what 'issue-was-closed') (exists... -> 200/txn 1.23
|
|
247
|
+
(and (eq what 'issue-was-closed') (exists... -> zero 1.18
|
|
248
|
+
(and (eq what 'issue-was-closed') (exists... -> zero/txn 1.30
|
|
249
|
+
transaction rollback on factbase with 100000 facts 0.28
|
|
250
|
+
(gt time '2024-03-23T03:21:43Z') 0.37
|
|
251
|
+
(gt cost 50) 0.18
|
|
252
|
+
(eq title 'Object Thinking 5000') 0.03
|
|
253
|
+
(and (eq foo 42.998) (or (gt bar 200) (absent z... 0.03
|
|
254
|
+
(and (exists foo) (not (exists blue))) 1.73
|
|
255
|
+
(eq id (agg (always) (max id))) 2.77
|
|
256
|
+
(join "c<=cost,b<=bar" (eq id (agg (always) (ma... 4.70
|
|
257
|
+
(and (eq what "foo") (join "w<=what" (and (eq i... 7.58
|
|
258
|
+
delete! 0.51
|
|
259
|
+
(and (eq issue *) (eq repository *) (eq what '*') (eq where '*')) 0.43
|
|
260
|
+
Taped.append() x50000 0.02
|
|
261
|
+
Taped.each() x125 1.15
|
|
262
|
+
Taped.delete_if() x375 0.97
|
|
263
|
+
50000 facts: read-only txn (no copy needed) 5.30
|
|
264
|
+
50000 facts: rollback txn (no copy needed) 5.16
|
|
265
|
+
50000 facts: insert in txn (copy triggered) 3.23
|
|
266
|
+
50000 facts: modify in txn (copy triggered) 34.56
|
|
267
|
+
100000 facts: read-only txn (no copy needed) 12.28
|
|
268
|
+
100000 facts: rollback txn (no copy needed) 12.19
|
|
269
|
+
100000 facts: insert in txn (copy triggered) 6.57
|
|
270
|
+
100000 facts: modify in txn (copy triggered) 70.11
|
|
256
271
|
```
|
|
257
272
|
|
|
258
273
|
The results were calculated in [this GHA job][benchmark-gha]
|
|
259
|
-
on
|
|
274
|
+
on 2026-02-04 at 05:19,
|
|
260
275
|
on Linux with 4 CPUs.
|
|
261
276
|
<!-- benchmark_end -->
|
|
262
277
|
|
|
263
|
-
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/
|
|
278
|
+
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/21659492807
|
data/Rakefile
CHANGED
|
@@ -35,7 +35,7 @@ task :picks do
|
|
|
35
35
|
next if OS.windows?
|
|
36
36
|
%w[test lib].each do |d|
|
|
37
37
|
Dir["#{d}/**/*.rb"].each do |f|
|
|
38
|
-
qbash("bundle exec ruby #{Shellwords.escape(f)}",
|
|
38
|
+
qbash("bundle exec ruby #{Shellwords.escape(f)}", stdout: $stdout, env: { 'PICKS' => 'yes' })
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -45,7 +45,7 @@ class Factbase::IndexedAnd
|
|
|
45
45
|
j = tuples.flat_map { |t| entry[:index][t] || [] }.uniq(&:object_id)
|
|
46
46
|
r =
|
|
47
47
|
if maps.respond_to?(:inserted)
|
|
48
|
-
|
|
48
|
+
maps & j
|
|
49
49
|
else
|
|
50
50
|
j
|
|
51
51
|
end
|
|
@@ -15,8 +15,8 @@ class Factbase::IndexedFact
|
|
|
15
15
|
# Ctor.
|
|
16
16
|
# @param [Factbase::Fact] origin The original fact
|
|
17
17
|
# @param [Hash] idx The index
|
|
18
|
-
# @param [
|
|
19
|
-
def initialize(origin, idx, fresh
|
|
18
|
+
# @param [Set] fresh The shared set of fresh fact IDs
|
|
19
|
+
def initialize(origin, idx, fresh)
|
|
20
20
|
@origin = origin
|
|
21
21
|
@idx = idx
|
|
22
22
|
@fresh = fresh
|
|
@@ -30,7 +30,7 @@ class Factbase::IndexedFact
|
|
|
30
30
|
others do |*args|
|
|
31
31
|
# Only clear index when modifying properties on existing (non-fresh) facts
|
|
32
32
|
# Fresh facts are not in the index yet, so modifications don't affect it
|
|
33
|
-
@idx.clear if args[0].to_s.end_with?('=') && !@fresh
|
|
33
|
+
@idx.clear if args[0].to_s.end_with?('=') && !@fresh.include?(object_id)
|
|
34
34
|
@origin.send(*args)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -21,17 +21,21 @@ class Factbase::IndexedFactbase
|
|
|
21
21
|
# Constructor.
|
|
22
22
|
# @param [Factbase] origin Original factbase to decorate
|
|
23
23
|
# @param [Hash] idx Index to use
|
|
24
|
-
|
|
24
|
+
# @param [Set] fresh The set of IDs of newly inserted facts
|
|
25
|
+
def initialize(origin, idx = {}, fresh = Set.new)
|
|
25
26
|
raise 'Wrong type of original' unless origin.respond_to?(:query)
|
|
26
27
|
@origin = origin
|
|
27
28
|
raise 'Wrong type of index' unless idx.is_a?(Hash)
|
|
28
29
|
@idx = idx
|
|
30
|
+
@fresh = fresh
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
# Insert a new fact and return it.
|
|
32
34
|
# @return [Factbase::Fact] The fact just inserted
|
|
33
35
|
def insert
|
|
34
|
-
Factbase::IndexedFact.new(@origin.insert, @idx, fresh
|
|
36
|
+
f = Factbase::IndexedFact.new(@origin.insert, @idx, @fresh)
|
|
37
|
+
@fresh.add(f.object_id)
|
|
38
|
+
f
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
# Convert a query to a term.
|
|
@@ -49,7 +53,9 @@ class Factbase::IndexedFactbase
|
|
|
49
53
|
def query(term, maps = nil)
|
|
50
54
|
term = to_term(term) if term.is_a?(String)
|
|
51
55
|
q = @origin.query(term, maps)
|
|
52
|
-
Factbase::IndexedQuery.new(q, @idx, self)
|
|
56
|
+
q = Factbase::IndexedQuery.new(q, @idx, self, @fresh)
|
|
57
|
+
@fresh.clear
|
|
58
|
+
q
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
# Run an ACID transaction.
|
|
@@ -57,9 +63,10 @@ class Factbase::IndexedFactbase
|
|
|
57
63
|
def txn
|
|
58
64
|
result =
|
|
59
65
|
@origin.txn do |fbt|
|
|
60
|
-
yield Factbase::IndexedFactbase.new(fbt, @idx)
|
|
66
|
+
yield Factbase::IndexedFactbase.new(fbt, @idx, @fresh)
|
|
61
67
|
end
|
|
62
68
|
@idx.clear
|
|
69
|
+
@fresh.clear
|
|
63
70
|
result
|
|
64
71
|
end
|
|
65
72
|
|
|
@@ -101,6 +108,7 @@ class Factbase::IndexedFactbase
|
|
|
101
108
|
@origin.import(bytes)
|
|
102
109
|
@idx.clear
|
|
103
110
|
end
|
|
111
|
+
@fresh.clear
|
|
104
112
|
end
|
|
105
113
|
|
|
106
114
|
# Size, the total number of facts in the factbase.
|
|
@@ -36,7 +36,11 @@ class Factbase::IndexedGt
|
|
|
36
36
|
return nil if threshold.nil?
|
|
37
37
|
i = entry[:sorted].bsearch_index { |pair| pair[0] > threshold } || entry[:sorted].size
|
|
38
38
|
result = entry[:sorted][i..].map { |pair| pair[1] }.uniq
|
|
39
|
-
(
|
|
39
|
+
if maps.respond_to?(:ensure_copied!)
|
|
40
|
+
maps & result
|
|
41
|
+
else
|
|
42
|
+
(maps & []) | result
|
|
43
|
+
end
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
private
|
|
@@ -41,7 +41,11 @@ class Factbase::IndexedLt
|
|
|
41
41
|
return nil if threshold.nil?
|
|
42
42
|
i = entry[:sorted].bsearch_index { |pair| pair[0] >= threshold } || entry[:sorted].size
|
|
43
43
|
result = entry[:sorted][0...i].map { |pair| pair[1] }.uniq
|
|
44
|
-
(
|
|
44
|
+
if maps.respond_to?(:ensure_copied!)
|
|
45
|
+
maps & result
|
|
46
|
+
else
|
|
47
|
+
(maps & []) | result
|
|
48
|
+
end
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
private
|
|
@@ -17,10 +17,12 @@ class Factbase::IndexedQuery
|
|
|
17
17
|
# Constructor.
|
|
18
18
|
# @param [Factbase::Query] origin Original query
|
|
19
19
|
# @param [Hash] idx The index
|
|
20
|
-
|
|
20
|
+
# @param [Set] fresh The set of IDs of newly inserted facts
|
|
21
|
+
def initialize(origin, idx, fb, fresh)
|
|
21
22
|
@origin = origin
|
|
22
23
|
@idx = idx
|
|
23
24
|
@fb = fb
|
|
25
|
+
@fresh = fresh
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
# Print it as a string.
|
|
@@ -37,7 +39,7 @@ class Factbase::IndexedQuery
|
|
|
37
39
|
return to_enum(__method__, fb, params) unless block_given?
|
|
38
40
|
a = @origin.each(fb, params).to_a
|
|
39
41
|
a.each do |f|
|
|
40
|
-
yield Factbase::IndexedFact.new(f, @idx)
|
|
42
|
+
yield Factbase::IndexedFact.new(f, @idx, @fresh)
|
|
41
43
|
end
|
|
42
44
|
a.size
|
|
43
45
|
end
|
|
@@ -4,9 +4,23 @@
|
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
6
|
# Indexed term 'unique'.
|
|
7
|
-
# @
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# The @idx[ikey] structure:
|
|
8
|
+
# {
|
|
9
|
+
# count: Integer (number of facts already processed),
|
|
10
|
+
# buckets: {
|
|
11
|
+
# key => {
|
|
12
|
+
# facts: Array (unique facts found),
|
|
13
|
+
# seen: Set (composite values already indexed to skip duplicates)
|
|
14
|
+
# }
|
|
15
|
+
# }
|
|
16
|
+
# }
|
|
17
|
+
# Example 1: (unique "fruit")
|
|
18
|
+
# - Apple, Apple, Banana
|
|
19
|
+
# - count: 3, facts: [Apple, Banana], seen: { [Apple], [Banana] }
|
|
20
|
+
#
|
|
21
|
+
# Example 2: (unique "fruit" "color")
|
|
22
|
+
# - [Apple, Red], [Apple, Green], [Apple, Red]
|
|
23
|
+
# - count: 3, facts: [[Apple, Red], [Apple, Green]], seen: { [Apple, Red], [Apple, Green] }
|
|
10
24
|
class Factbase::IndexedUnique
|
|
11
25
|
def initialize(term, idx)
|
|
12
26
|
@term = term
|
|
@@ -14,21 +28,29 @@ class Factbase::IndexedUnique
|
|
|
14
28
|
end
|
|
15
29
|
|
|
16
30
|
def predict(maps, _fb, _params)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
operands = @term.operands.map(&:to_s)
|
|
32
|
+
bucket_key = operands.join('|')
|
|
33
|
+
idx_key = [maps.object_id, @term.op.to_s, bucket_key]
|
|
34
|
+
entry = (@idx[idx_key] ||= { buckets: {}, count: 0 })
|
|
35
|
+
feed(maps.to_a, entry, operands, bucket_key)
|
|
36
|
+
bucket = entry[:buckets][bucket_key]
|
|
37
|
+
if maps.respond_to?(:ensure_copied!)
|
|
38
|
+
maps & (bucket[:facts] || [])
|
|
39
|
+
else
|
|
40
|
+
(maps & []) | (bucket[:facts] || [])
|
|
24
41
|
end
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def feed(facts, entry, operands, bucket_key)
|
|
47
|
+
entry[:buckets][bucket_key] ||= { facts: [], seen: Set.new }
|
|
48
|
+
bucket = entry[:buckets][bucket_key]
|
|
49
|
+
(facts[entry[:count]..] || []).each do |fact|
|
|
50
|
+
composite_val = operands.map { |o| fact[o] }
|
|
51
|
+
next if composite_val.any?(&:nil?)
|
|
52
|
+
bucket[:facts] << fact if bucket[:seen].add?(composite_val)
|
|
31
53
|
end
|
|
32
|
-
|
|
54
|
+
entry[:count] = facts.size
|
|
33
55
|
end
|
|
34
56
|
end
|
data/lib/factbase/lazy_taped.rb
CHANGED
|
@@ -62,7 +62,7 @@ class Factbase::LazyTaped
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def <<(map)
|
|
65
|
-
ensure_copied
|
|
65
|
+
ensure_copied!
|
|
66
66
|
@maps << map
|
|
67
67
|
@inserted.append(map.object_id)
|
|
68
68
|
end
|
|
@@ -81,7 +81,7 @@ class Factbase::LazyTaped
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def delete_if
|
|
84
|
-
ensure_copied
|
|
84
|
+
ensure_copied!
|
|
85
85
|
@maps.delete_if do |m|
|
|
86
86
|
r = yield m
|
|
87
87
|
@deleted.append(@pairs[m].object_id) if r
|
|
@@ -108,7 +108,7 @@ class Factbase::LazyTaped
|
|
|
108
108
|
join(other, &:|)
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
def ensure_copied
|
|
111
|
+
def ensure_copied!
|
|
112
112
|
return if @copied
|
|
113
113
|
@pairs = {}.compare_by_identity
|
|
114
114
|
@maps =
|
|
@@ -121,13 +121,14 @@ class Factbase::LazyTaped
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
def get_copied_map(original_map)
|
|
124
|
-
ensure_copied
|
|
124
|
+
ensure_copied!
|
|
125
125
|
@maps.find { |m| @pairs[m].equal?(original_map) }
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
private
|
|
129
129
|
|
|
130
130
|
def join(other)
|
|
131
|
+
ensure_copied!
|
|
131
132
|
n = yield (@maps || @origin).to_a, other.to_a
|
|
132
133
|
raise 'Cannot join with another Taped' if other.respond_to?(:inserted)
|
|
133
134
|
raise 'Can only join with array' unless other.is_a?(Array)
|
data/lib/factbase/syntax.rb
CHANGED
data/lib/factbase/terms/defn.rb
CHANGED
|
@@ -27,7 +27,7 @@ class Factbase::Defn < Factbase::TermBase
|
|
|
27
27
|
fn = @operands[0]
|
|
28
28
|
raise "A symbol expected as first argument of 'defn'" unless fn.is_a?(Symbol)
|
|
29
29
|
raise "Can't use '#{fn}' name as a term" if Factbase::Term.method_defined?(fn)
|
|
30
|
-
raise "Term '#{fn}' is already defined" if Factbase::Term.
|
|
30
|
+
raise "Term '#{fn}' is already defined" if Factbase::Term.private_method_defined?(fn, false)
|
|
31
31
|
raise "The '#{fn}' is a bad name for a term" unless fn.match?(/^[a-z_]+$/)
|
|
32
32
|
e = "class Factbase::Term\nprivate\ndef #{fn}(fact, maps, fb)\n#{@operands[1]}\nend\nend"
|
|
33
33
|
# rubocop:disable Security/Eval
|
data/lib/factbase/terms/undef.rb
CHANGED
|
@@ -25,7 +25,7 @@ class Factbase::Undef < Factbase::TermBase
|
|
|
25
25
|
assert_args(1)
|
|
26
26
|
fn = @operands[0]
|
|
27
27
|
raise "A symbol expected as first argument of 'undef'" unless fn.is_a?(Symbol)
|
|
28
|
-
if Factbase::Term.
|
|
28
|
+
if Factbase::Term.private_method_defined?(fn, false)
|
|
29
29
|
Factbase::Term.class_eval("undef :#{fn}", __FILE__, __LINE__ - 1) # undef :foo
|
|
30
30
|
end
|
|
31
31
|
true
|
data/lib/factbase/version.rb
CHANGED