factbase 0.17.0 → 0.18.0
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 +2 -1
- data/Gemfile.lock +7 -5
- data/Rakefile +16 -0
- data/lib/factbase/indexed/indexed_factbase.rb +6 -3
- data/lib/factbase/lazy_taped.rb +141 -0
- data/lib/factbase/lazy_taped_array.rb +62 -0
- data/lib/factbase/lazy_taped_hash.rb +79 -0
- data/lib/factbase/term.rb +23 -15
- data/lib/factbase/terms/agg.rb +35 -0
- data/lib/factbase/terms/and.rb +7 -0
- data/lib/factbase/terms/base.rb +56 -3
- data/lib/factbase/terms/best.rb +25 -0
- data/lib/factbase/terms/empty.rb +36 -0
- data/lib/factbase/terms/max.rb +32 -0
- data/lib/factbase/terms/min.rb +32 -0
- data/lib/factbase/terms/nth.rb +33 -0
- data/lib/factbase/terms/or.rb +7 -0
- data/lib/factbase/terms/simplified.rb +31 -0
- data/lib/factbase/terms/sum.rb +38 -0
- data/lib/factbase/version.rb +1 -1
- data/lib/factbase.rb +13 -20
- metadata +12 -4
- data/lib/factbase/terms/aggregates.rb +0 -86
- data/lib/factbase/terms/logical.rb +0 -41
- data/lib/factbase/terms/shared.rb +0 -69
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90b1ab3d82ed371b4de11b7eceb98137b2a7687bc2625e61d4d54a6538849b34
|
|
4
|
+
data.tar.gz: f9fd82d8254d7cfa646a742db6989790791ce157ce53d9e5a310f8f8f8c0db11
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae0d064e075f304c9fae6f4903c961293d07e186ee6bdf4e180d5f407f5b166f2b3cc345cec612d8184675394a2ee504197d4c13d9ce0f480cffd850d7d10169
|
|
7
|
+
data.tar.gz: b53da29f44f9ad5520bed3602c9c7dadf882115856ddb344dfba95a32017cd2e89c4582a7a160baa2ed611ff37acd0ba39e8fb9a8aa8ed7bec62f0acf9f0ee70
|
data/Gemfile
CHANGED
|
@@ -12,12 +12,13 @@ gem 'minitest-reporters', '~>1.7', require: false
|
|
|
12
12
|
gem 'os', '~>1.1', require: false
|
|
13
13
|
gem 'qbash', '~>0.4', require: false
|
|
14
14
|
gem 'rake', '~>13.2', require: false
|
|
15
|
-
gem 'rdoc', '6.
|
|
15
|
+
gem 'rdoc', '6.17.0', require: false # GPL
|
|
16
16
|
gem 'rubocop', '~>1.74', require: false
|
|
17
17
|
gem 'rubocop-minitest', '~>0.38', require: false
|
|
18
18
|
gem 'rubocop-performance', '~>1.25', require: false
|
|
19
19
|
gem 'rubocop-rake', '~>0.7', require: false
|
|
20
20
|
gem 'simplecov', '~>0.22', require: false
|
|
21
21
|
gem 'simplecov-cobertura', '~>3.0', require: false
|
|
22
|
+
gem 'stackprof', '~>0.2', require: false, platforms: [:ruby]
|
|
22
23
|
gem 'threads', '~>0.4', require: false
|
|
23
24
|
gem 'yard', '~>0.9', require: false
|
data/Gemfile.lock
CHANGED
|
@@ -30,7 +30,7 @@ GEM
|
|
|
30
30
|
tago (~> 0.1)
|
|
31
31
|
ellipsized (0.3.0)
|
|
32
32
|
erb (6.0.0)
|
|
33
|
-
json (2.
|
|
33
|
+
json (2.17.1)
|
|
34
34
|
language_server-protocol (3.17.0.5)
|
|
35
35
|
lint_roller (1.1.0)
|
|
36
36
|
logger (1.7.0)
|
|
@@ -68,7 +68,7 @@ GEM
|
|
|
68
68
|
racc (1.8.1)
|
|
69
69
|
rainbow (3.1.1)
|
|
70
70
|
rake (13.3.1)
|
|
71
|
-
rdoc (6.
|
|
71
|
+
rdoc (6.17.0)
|
|
72
72
|
erb
|
|
73
73
|
psych (>= 4.0.0)
|
|
74
74
|
tsort
|
|
@@ -109,7 +109,8 @@ GEM
|
|
|
109
109
|
simplecov (~> 0.19)
|
|
110
110
|
simplecov-html (0.13.2)
|
|
111
111
|
simplecov_json_formatter (0.1.4)
|
|
112
|
-
|
|
112
|
+
stackprof (0.2.27)
|
|
113
|
+
stringio (3.1.9)
|
|
113
114
|
tago (0.4.0)
|
|
114
115
|
threads (0.5.0)
|
|
115
116
|
backtrace (~> 0)
|
|
@@ -119,7 +120,7 @@ GEM
|
|
|
119
120
|
unicode-emoji (~> 4.1)
|
|
120
121
|
unicode-emoji (4.1.0)
|
|
121
122
|
yaml (0.4.0)
|
|
122
|
-
yard (0.9.
|
|
123
|
+
yard (0.9.38)
|
|
123
124
|
|
|
124
125
|
PLATFORMS
|
|
125
126
|
arm64-darwin-22
|
|
@@ -139,13 +140,14 @@ DEPENDENCIES
|
|
|
139
140
|
os (~> 1.1)
|
|
140
141
|
qbash (~> 0.4)
|
|
141
142
|
rake (~> 13.2)
|
|
142
|
-
rdoc (= 6.
|
|
143
|
+
rdoc (= 6.17.0)
|
|
143
144
|
rubocop (~> 1.74)
|
|
144
145
|
rubocop-minitest (~> 0.38)
|
|
145
146
|
rubocop-performance (~> 1.25)
|
|
146
147
|
rubocop-rake (~> 0.7)
|
|
147
148
|
simplecov (~> 0.22)
|
|
148
149
|
simplecov-cobertura (~> 3.0)
|
|
150
|
+
stackprof (~> 0.2)
|
|
149
151
|
threads (~> 0.4)
|
|
150
152
|
yard (~> 0.9)
|
|
151
153
|
|
data/Rakefile
CHANGED
|
@@ -78,3 +78,19 @@ task :benchmark, [:name] do |_t, args|
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
|
+
|
|
82
|
+
# Run profiling on a benchmark and generate a flamegraph.
|
|
83
|
+
# To run this task, you need to have stackprof installed.
|
|
84
|
+
# https://github.com/tmm1/stackprof
|
|
85
|
+
# To run profiling for a specific benchmark you can run:
|
|
86
|
+
# bundle exec rake flamegraph\[bench_slow_query\]
|
|
87
|
+
desc 'Profile a benchmark (e.g., flamegraph[bench_slow_query])'
|
|
88
|
+
task :flamegraph, [:name] do |_t, args|
|
|
89
|
+
require 'stackprof'
|
|
90
|
+
bname = args[:name] || 'all'
|
|
91
|
+
puts "Starting profiling for '#{bname}'..."
|
|
92
|
+
StackProf.run(mode: :cpu, out: 'stackprof-cpu-myapp.dump', raw: true) do
|
|
93
|
+
Rake::Task['benchmark'].invoke(bname)
|
|
94
|
+
end
|
|
95
|
+
`stackprof --d3-flamegraph stackprof-cpu-myapp.dump > flamegraph.html`
|
|
96
|
+
end
|
|
@@ -57,9 +57,12 @@ class Factbase::IndexedFactbase
|
|
|
57
57
|
# Run an ACID transaction.
|
|
58
58
|
# @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
|
|
59
59
|
def txn
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
result =
|
|
61
|
+
@origin.txn do |fbt|
|
|
62
|
+
yield Factbase::IndexedFactbase.new(fbt, @idx)
|
|
63
|
+
end
|
|
64
|
+
@idx.clear
|
|
65
|
+
result
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
# Export it into a chain of bytes, including both data and index.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative '../factbase'
|
|
7
|
+
require_relative 'taped'
|
|
8
|
+
require_relative 'lazy_taped_hash'
|
|
9
|
+
|
|
10
|
+
# A lazy decorator of an Array with HashMaps that defers copying until modification.
|
|
11
|
+
class Factbase::LazyTaped
|
|
12
|
+
def initialize(origin)
|
|
13
|
+
@origin = origin
|
|
14
|
+
@copied = false
|
|
15
|
+
@maps = nil
|
|
16
|
+
@pairs = nil
|
|
17
|
+
@inserted = []
|
|
18
|
+
@deleted = []
|
|
19
|
+
@added = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns a hash mapping copied maps to their originals.
|
|
23
|
+
# This is used during transaction commit to identify which original facts
|
|
24
|
+
# were modified, allowing the factbase to update the correct entries.
|
|
25
|
+
def pairs
|
|
26
|
+
return {} unless @pairs
|
|
27
|
+
result = {}.compare_by_identity
|
|
28
|
+
@pairs.each { |copied, original| result[copied] = original }
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the unique object IDs of maps that were inserted (newly created).
|
|
33
|
+
# This is used during transaction commit to identify new facts that need
|
|
34
|
+
# to be added to the factbase.
|
|
35
|
+
def inserted
|
|
36
|
+
@inserted.uniq
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns the unique object IDs of maps that were deleted.
|
|
40
|
+
# This is used during transaction commit to identify facts that need
|
|
41
|
+
# to be removed from the factbase.
|
|
42
|
+
def deleted
|
|
43
|
+
@deleted.uniq
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the unique object IDs of maps that were modified (properties added).
|
|
47
|
+
# This is used during transaction commit to track the churn (number of changes).
|
|
48
|
+
def added
|
|
49
|
+
@added.uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_by_object_id(oid)
|
|
53
|
+
(@maps || @origin).find { |m| m.object_id == oid }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def size
|
|
57
|
+
(@maps || @origin).size
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def empty?
|
|
61
|
+
(@maps || @origin).empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def <<(map)
|
|
65
|
+
ensure_copied
|
|
66
|
+
@maps << map
|
|
67
|
+
@inserted.append(map.object_id)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def each
|
|
71
|
+
return to_enum(__method__) unless block_given?
|
|
72
|
+
if @copied
|
|
73
|
+
@maps.each do |m|
|
|
74
|
+
yield Factbase::Taped::TapedHash.new(m, @added)
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
@origin.each do |m|
|
|
78
|
+
yield LazyTapedHash.new(m, self, @added)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def delete_if
|
|
84
|
+
ensure_copied
|
|
85
|
+
@maps.delete_if do |m|
|
|
86
|
+
r = yield m
|
|
87
|
+
@deleted.append(@pairs[m].object_id) if r
|
|
88
|
+
r
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_a
|
|
93
|
+
(@maps || @origin).to_a
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def &(other)
|
|
97
|
+
if other == [] || (@maps || @origin).empty?
|
|
98
|
+
return Factbase::Taped.new([], inserted: @inserted, deleted: @deleted, added: @added)
|
|
99
|
+
end
|
|
100
|
+
join(other, &:&)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def |(other)
|
|
104
|
+
return Factbase::Taped.new(to_a, inserted: @inserted, deleted: @deleted, added: @added) if other == []
|
|
105
|
+
if (@maps || @origin).empty?
|
|
106
|
+
return Factbase::Taped.new(other, inserted: @inserted, deleted: @deleted, added: @added)
|
|
107
|
+
end
|
|
108
|
+
join(other, &:|)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ensure_copied
|
|
112
|
+
return if @copied
|
|
113
|
+
@pairs = {}.compare_by_identity
|
|
114
|
+
@maps =
|
|
115
|
+
@origin.map do |m|
|
|
116
|
+
n = m.transform_values(&:dup)
|
|
117
|
+
@pairs[n] = m
|
|
118
|
+
n
|
|
119
|
+
end
|
|
120
|
+
@copied = true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def get_copied_map(original_map)
|
|
124
|
+
ensure_copied
|
|
125
|
+
@maps.find { |m| @pairs[m].equal?(original_map) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def join(other)
|
|
131
|
+
n = yield (@maps || @origin).to_a, other.to_a
|
|
132
|
+
raise 'Cannot join with another Taped' if other.respond_to?(:inserted)
|
|
133
|
+
raise 'Can only join with array' unless other.is_a?(Array)
|
|
134
|
+
Factbase::Taped.new(
|
|
135
|
+
n,
|
|
136
|
+
inserted: @inserted,
|
|
137
|
+
deleted: @deleted,
|
|
138
|
+
added: @added
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative '../factbase'
|
|
7
|
+
|
|
8
|
+
class Factbase::LazyTaped
|
|
9
|
+
# Decorator of Array that triggers copy-on-write.
|
|
10
|
+
# @todo #424:30min Add dedicated unit tests for LazyTapedArray class.
|
|
11
|
+
# Currently this class is tested indirectly through LazyTaped tests.
|
|
12
|
+
# We should add explicit tests for all public methods including each, [],
|
|
13
|
+
# to_a, any?, <<, and uniq! to ensure proper copy-on-write behavior.
|
|
14
|
+
class LazyTapedArray
|
|
15
|
+
# Creates a new lazy array wrapper.
|
|
16
|
+
# @param origin [Array] The original array to wrap
|
|
17
|
+
# @param key [String] The key in the parent hash where this array is stored
|
|
18
|
+
# @param taped_hash [LazyTapedHash] The parent hash wrapper that owns this array
|
|
19
|
+
# @param added [Array] Accumulator for tracking object IDs of modified facts
|
|
20
|
+
def initialize(origin, key, taped_hash, added)
|
|
21
|
+
@origin = origin
|
|
22
|
+
@key = key
|
|
23
|
+
@taped_hash = taped_hash
|
|
24
|
+
@added = added
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def each(&)
|
|
28
|
+
return to_enum(__method__) unless block_given?
|
|
29
|
+
current_array.each(&)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def [](idx)
|
|
33
|
+
current_array[idx]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_a
|
|
37
|
+
current_array.to_a
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def any?(pattern = nil, &)
|
|
41
|
+
pattern ? current_array.any?(pattern) : current_array.any?(&)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def <<(item)
|
|
45
|
+
@taped_hash.ensure_copied_map
|
|
46
|
+
@added.append(@taped_hash.tracking_id)
|
|
47
|
+
@taped_hash.get_copied_array(@key) << item
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def uniq!
|
|
51
|
+
@taped_hash.ensure_copied_map
|
|
52
|
+
@added.append(@taped_hash.tracking_id)
|
|
53
|
+
@taped_hash.get_copied_array(@key).uniq!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def current_array
|
|
59
|
+
@taped_hash.copied? ? @taped_hash.get_copied_array(@key) : @origin
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative '../factbase'
|
|
7
|
+
require_relative 'lazy_taped_array'
|
|
8
|
+
|
|
9
|
+
class Factbase::LazyTaped
|
|
10
|
+
# Decorator of Hash that triggers copy-on-write.
|
|
11
|
+
# @todo #424:30min Add dedicated unit tests for LazyTapedHash class.
|
|
12
|
+
# Currently this class is tested indirectly through LazyTaped tests.
|
|
13
|
+
# We should add explicit tests for all public methods including keys, map,
|
|
14
|
+
# bracket access, bracket assignment, and the copy-on-write behavior.
|
|
15
|
+
class LazyTapedHash
|
|
16
|
+
# Creates a new LazyTapedHash decorator.
|
|
17
|
+
# @param origin [Hash] The original hash being wrapped (not yet copied)
|
|
18
|
+
# @param lazy_taped [Factbase::LazyTaped] The parent LazyTaped instance that manages copy-on-write
|
|
19
|
+
# @param added [Array] Array to track object IDs of maps that have been modified
|
|
20
|
+
def initialize(origin, lazy_taped, added)
|
|
21
|
+
@origin = origin
|
|
22
|
+
@lazy_taped = lazy_taped
|
|
23
|
+
@added = added
|
|
24
|
+
@copied_map = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def keys
|
|
28
|
+
current_map.keys
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def map(&)
|
|
32
|
+
current_map.map(&)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def [](key)
|
|
36
|
+
v = current_map[key]
|
|
37
|
+
v = LazyTapedArray.new(v, key, self, @added) if v.is_a?(Array)
|
|
38
|
+
v
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def []=(key, value)
|
|
42
|
+
ensure_copied_map
|
|
43
|
+
@copied_map[key] = value
|
|
44
|
+
@added.append(@copied_map.object_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ensure_copied_map
|
|
48
|
+
return if @copied_map
|
|
49
|
+
@copied_map = @lazy_taped.get_copied_map(@origin)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def get_copied_array(key)
|
|
53
|
+
ensure_copied_map
|
|
54
|
+
@copied_map[key]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def tracking_id
|
|
58
|
+
@copied_map ? @copied_map.object_id : @origin.object_id
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def copied?
|
|
62
|
+
!@copied_map.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def current_map
|
|
68
|
+
@copied_map || @origin
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def method_missing(method, *, &)
|
|
72
|
+
current_map.send(method, *, &)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def respond_to_missing?(method, include_private = false)
|
|
76
|
+
current_map.respond_to?(method, include_private)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/factbase/term.rb
CHANGED
|
@@ -52,6 +52,12 @@ require_relative 'terms/when'
|
|
|
52
52
|
require_relative 'terms/either'
|
|
53
53
|
require_relative 'terms/count'
|
|
54
54
|
require_relative 'terms/first'
|
|
55
|
+
require_relative 'terms/nth'
|
|
56
|
+
require_relative 'terms/sum'
|
|
57
|
+
require_relative 'terms/agg'
|
|
58
|
+
require_relative 'terms/empty'
|
|
59
|
+
require_relative 'terms/min'
|
|
60
|
+
require_relative 'terms/max'
|
|
55
61
|
|
|
56
62
|
# Term.
|
|
57
63
|
#
|
|
@@ -80,7 +86,7 @@ require_relative 'terms/first'
|
|
|
80
86
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
81
87
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
82
88
|
# License:: MIT
|
|
83
|
-
class Factbase::Term
|
|
89
|
+
class Factbase::Term < Factbase::TermBase
|
|
84
90
|
# The operator of this term
|
|
85
91
|
# @return [Symbol] The operator
|
|
86
92
|
attr_reader :op
|
|
@@ -89,19 +95,11 @@ class Factbase::Term
|
|
|
89
95
|
# @return [Array] The operands
|
|
90
96
|
attr_reader :operands
|
|
91
97
|
|
|
92
|
-
require_relative 'terms/logical'
|
|
93
|
-
include Factbase::Logical
|
|
94
|
-
|
|
95
|
-
require_relative 'terms/aggregates'
|
|
96
|
-
include Factbase::Aggregates
|
|
97
|
-
|
|
98
|
-
require_relative 'terms/shared'
|
|
99
|
-
include Factbase::TermShared
|
|
100
|
-
|
|
101
98
|
# Ctor.
|
|
102
99
|
# @param [Symbol] operator Operator
|
|
103
100
|
# @param [Array] operands Operands
|
|
104
101
|
def initialize(operator, operands)
|
|
102
|
+
super()
|
|
105
103
|
@op = operator
|
|
106
104
|
@operands = operands
|
|
107
105
|
@terms = {
|
|
@@ -149,7 +147,13 @@ class Factbase::Term
|
|
|
149
147
|
when: Factbase::When.new(operands),
|
|
150
148
|
either: Factbase::Either.new(operands),
|
|
151
149
|
count: Factbase::Count.new(operands),
|
|
152
|
-
first: Factbase::First.new(operands)
|
|
150
|
+
first: Factbase::First.new(operands),
|
|
151
|
+
nth: Factbase::Nth.new(operands),
|
|
152
|
+
sum: Factbase::Sum.new(operands),
|
|
153
|
+
agg: Factbase::Agg.new(operands),
|
|
154
|
+
empty: Factbase::Empty.new(operands),
|
|
155
|
+
min: Factbase::Min.new(operands),
|
|
156
|
+
max: Factbase::Max.new(operands)
|
|
153
157
|
}
|
|
154
158
|
end
|
|
155
159
|
|
|
@@ -211,11 +215,15 @@ class Factbase::Term
|
|
|
211
215
|
# Simplify it if possible.
|
|
212
216
|
# @return [Factbase::Term] New term or itself
|
|
213
217
|
def simplify
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
send(m)
|
|
218
|
+
if @terms.key?(@op) && @terms[@op].respond_to?(:simplify)
|
|
219
|
+
@terms[@op].simplify
|
|
217
220
|
else
|
|
218
|
-
|
|
221
|
+
m = "#{@op}_simplify"
|
|
222
|
+
if respond_to?(m, true)
|
|
223
|
+
send(m)
|
|
224
|
+
else
|
|
225
|
+
self
|
|
226
|
+
end
|
|
219
227
|
end
|
|
220
228
|
end
|
|
221
229
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative 'base'
|
|
7
|
+
# The term 'agg' that aggregates.
|
|
8
|
+
class Factbase::Agg < Factbase::TermBase
|
|
9
|
+
# Constructor.
|
|
10
|
+
# @param [Array] operands Operands
|
|
11
|
+
def initialize(operands = [])
|
|
12
|
+
super()
|
|
13
|
+
@operands = operands
|
|
14
|
+
@op = :agg
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Evaluate term on a fact.
|
|
18
|
+
# @param [Factbase::Fact] fact The fact
|
|
19
|
+
# @param [Array<Factbase::Fact>] maps All maps available
|
|
20
|
+
# @param [Factbase] fb Factbase to use for sub-queries
|
|
21
|
+
# @return [Object] The result of evaluation
|
|
22
|
+
def evaluate(fact, maps, fb)
|
|
23
|
+
assert_args(2)
|
|
24
|
+
selector = @operands[0]
|
|
25
|
+
unless selector.is_a?(Factbase::Term) || selector.is_a?(Factbase::TermBase)
|
|
26
|
+
raise "A term is expected, but '#{selector}' provided"
|
|
27
|
+
end
|
|
28
|
+
term = @operands[1]
|
|
29
|
+
unless term.is_a?(Factbase::Term) || term.is_a?(Factbase::TermBase)
|
|
30
|
+
raise "A term is expected, but '#{term}' provided"
|
|
31
|
+
end
|
|
32
|
+
subset = fb.query(selector, maps).each(fb, fact).to_a
|
|
33
|
+
term.evaluate(nil, subset, fb)
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/factbase/terms/and.rb
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
require_relative 'base'
|
|
7
7
|
require_relative 'boolean'
|
|
8
|
+
require_relative 'simplified'
|
|
8
9
|
# The 'and' term that represents a logical AND operation between multiple operands.
|
|
9
10
|
class Factbase::And < Factbase::TermBase
|
|
10
11
|
# Constructor.
|
|
@@ -25,4 +26,10 @@ class Factbase::And < Factbase::TermBase
|
|
|
25
26
|
end
|
|
26
27
|
true
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
def simplify
|
|
31
|
+
unique = Factbase::Simplified.new(@operands).unique
|
|
32
|
+
return unique[0] if unique.size == 1
|
|
33
|
+
Factbase::Term.new(@op, unique)
|
|
34
|
+
end
|
|
28
35
|
end
|
data/lib/factbase/terms/base.rb
CHANGED
|
@@ -10,8 +10,61 @@
|
|
|
10
10
|
|
|
11
11
|
# Base class for all terms.
|
|
12
12
|
class Factbase::TermBase
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# Turns it into a string.
|
|
14
|
+
# @return [String] The string of it
|
|
15
|
+
def to_s
|
|
16
|
+
@to_s ||=
|
|
17
|
+
begin
|
|
18
|
+
items = []
|
|
19
|
+
items << @op
|
|
20
|
+
items +=
|
|
21
|
+
@operands.map do |o|
|
|
22
|
+
if o.is_a?(String)
|
|
23
|
+
"'#{o.gsub("'", "\\\\'").gsub('"', '\\\\"')}'"
|
|
24
|
+
elsif o.is_a?(Time)
|
|
25
|
+
o.utc.iso8601
|
|
26
|
+
else
|
|
27
|
+
o.to_s
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
"(#{items.join(' ')})"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
15
33
|
|
|
16
|
-
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def assert_args(num)
|
|
37
|
+
c = @operands.size
|
|
38
|
+
raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
|
|
39
|
+
raise "Too few (#{c}) operands for '#{@op}' (#{num} expected)" if c < num
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def _by_symbol(pos, fact)
|
|
43
|
+
o = @operands[pos]
|
|
44
|
+
raise "A symbol expected at ##{pos}, but '#{o}' (#{o.class}) provided" unless o.is_a?(Symbol)
|
|
45
|
+
k = o.to_s
|
|
46
|
+
fact[k]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Array|nil] Either array of values or NIL
|
|
50
|
+
def _values(pos, fact, maps, fb)
|
|
51
|
+
v = @operands[pos]
|
|
52
|
+
v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::Term)
|
|
53
|
+
v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::TermBase)
|
|
54
|
+
v = fact[v.to_s] if v.is_a?(Symbol)
|
|
55
|
+
return v if v.nil?
|
|
56
|
+
unless v.is_a?(Array)
|
|
57
|
+
v =
|
|
58
|
+
if v.respond_to?(:each)
|
|
59
|
+
v.to_a
|
|
60
|
+
else
|
|
61
|
+
[v]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
raise 'Why not array?' unless v.is_a?(Array)
|
|
65
|
+
unless v.all? { |i| [Float, Integer, String, Time, TrueClass, FalseClass].any? { |t| i.is_a?(t) } }
|
|
66
|
+
raise 'Wrong type inside'
|
|
67
|
+
end
|
|
68
|
+
v
|
|
69
|
+
end
|
|
17
70
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# The 'best' term evaluates the best value for a given key.
|
|
7
|
+
class Factbase::Best
|
|
8
|
+
def initialize(&block)
|
|
9
|
+
@criteria = block
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def evaluate(key, maps)
|
|
13
|
+
raise "A symbol is expected, but #{key} provided" unless key.is_a?(Symbol)
|
|
14
|
+
best = nil
|
|
15
|
+
maps.each do |m|
|
|
16
|
+
vv = m[key.to_s]
|
|
17
|
+
next if vv.nil?
|
|
18
|
+
vv = [vv] unless vv.respond_to?(:to_a)
|
|
19
|
+
vv.each do |v|
|
|
20
|
+
best = v if best.nil? || @criteria.call(v, best)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
best
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative 'base'
|
|
7
|
+
|
|
8
|
+
# The 'empty' term checks for emptiness in the results of a query evaluation.
|
|
9
|
+
class Factbase::Empty < Factbase::TermBase
|
|
10
|
+
# Constructor.
|
|
11
|
+
# @param [Array] operands Operands
|
|
12
|
+
def initialize(operands)
|
|
13
|
+
super()
|
|
14
|
+
@operands = operands
|
|
15
|
+
@op = :empty
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Evaluate term on a fact.
|
|
19
|
+
# @param [Factbase::Fact] fact The fact
|
|
20
|
+
# @param [Array<Factbase::Fact>] maps All maps available
|
|
21
|
+
# @param [Factbase] fb Factbase to use for sub-queries
|
|
22
|
+
# @return [Boolean] The result of the emptiness check
|
|
23
|
+
def evaluate(fact, maps, fb)
|
|
24
|
+
assert_args(1)
|
|
25
|
+
term = @operands[0]
|
|
26
|
+
unless term.is_a?(Factbase::Term) || term.is_a?(Factbase::TermBase)
|
|
27
|
+
raise "A term is expected, but '#{term}' provided"
|
|
28
|
+
end
|
|
29
|
+
# rubocop:disable Lint/UnreachableLoop
|
|
30
|
+
fb.query(term, maps).each(fb, fact) do
|
|
31
|
+
return false
|
|
32
|
+
end
|
|
33
|
+
# rubocop:enable Lint/UnreachableLoop
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative '../../factbase'
|
|
7
|
+
require_relative 'best'
|
|
8
|
+
require_relative 'base'
|
|
9
|
+
|
|
10
|
+
# The 'max' term.
|
|
11
|
+
# This term calculates the max value among the evaluated operands.
|
|
12
|
+
class Factbase::Max < Factbase::TermBase
|
|
13
|
+
MAX = Factbase::Best.new { |v, b| v > b }
|
|
14
|
+
|
|
15
|
+
# Constructor.
|
|
16
|
+
# @param [Array] operands Operands
|
|
17
|
+
def initialize(operands)
|
|
18
|
+
super()
|
|
19
|
+
@operands = operands
|
|
20
|
+
@op = :max
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Evaluate term on a fact.
|
|
24
|
+
# @param [Factbase::Fact] _fact The fact
|
|
25
|
+
# @param [Array<Factbase::Fact>] maps All maps available
|
|
26
|
+
# @param [Factbase] _fb Factbase to use for sub-queries
|
|
27
|
+
# @return [Object] The max value among the evaluated operands
|
|
28
|
+
def evaluate(_fact, maps, _fb)
|
|
29
|
+
assert_args(1)
|
|
30
|
+
MAX.evaluate(@operands[0], maps)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative '../../factbase'
|
|
7
|
+
require_relative 'best'
|
|
8
|
+
require_relative 'base'
|
|
9
|
+
|
|
10
|
+
# The 'min' term.
|
|
11
|
+
# This term calculates the minimum value among the evaluated operands.
|
|
12
|
+
class Factbase::Min < Factbase::TermBase
|
|
13
|
+
MIN = Factbase::Best.new { |v, b| v < b }
|
|
14
|
+
|
|
15
|
+
# Constructor.
|
|
16
|
+
# @param [Array] operands Operands
|
|
17
|
+
def initialize(operands)
|
|
18
|
+
super()
|
|
19
|
+
@operands = operands
|
|
20
|
+
@op = :min
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Evaluate term on a fact.
|
|
24
|
+
# @param [Factbase::Fact] _fact The fact
|
|
25
|
+
# @param [Array<Factbase::Fact>] maps All maps available
|
|
26
|
+
# @param [Factbase] _fb Factbase to use for sub-queries
|
|
27
|
+
# @return [Object] The minimum value among the evaluated operands
|
|
28
|
+
def evaluate(_fact, maps, _fb)
|
|
29
|
+
assert_args(1)
|
|
30
|
+
MIN.evaluate(@operands[0], maps)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative 'base'
|
|
7
|
+
# Represents an 'nth' term in the Factbase.
|
|
8
|
+
# Retrieves the value of a specified key from the nth map.
|
|
9
|
+
class Factbase::Nth < Factbase::TermBase
|
|
10
|
+
# Constructor.
|
|
11
|
+
# @param [Array] operands Operands
|
|
12
|
+
def initialize(operands)
|
|
13
|
+
super()
|
|
14
|
+
@operands = operands
|
|
15
|
+
@op = :nth
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Evaluate term on a fact.
|
|
19
|
+
# @param [Factbase::Fact] _fact The fact
|
|
20
|
+
# @param [Array<Factbase::Fact>] maps All maps available
|
|
21
|
+
# @param [Factbase] _fb Factbase to use for sub-queries
|
|
22
|
+
# @return [Object] The value of the specified key from the nth map
|
|
23
|
+
def evaluate(_fact, maps, _fb)
|
|
24
|
+
assert_args(2)
|
|
25
|
+
pos = @operands[0]
|
|
26
|
+
raise "An integer is expected, but #{pos} provided" unless pos.is_a?(Integer)
|
|
27
|
+
k = @operands[1]
|
|
28
|
+
raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
|
|
29
|
+
m = maps[pos]
|
|
30
|
+
return nil if m.nil?
|
|
31
|
+
m[k.to_s]
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/factbase/terms/or.rb
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
require_relative 'base'
|
|
7
7
|
require_relative 'boolean'
|
|
8
|
+
require_relative 'simplified'
|
|
8
9
|
# The 'or' term that represents a logical OR operation between multiple operands.
|
|
9
10
|
class Factbase::Or < Factbase::TermBase
|
|
10
11
|
# Constructor.
|
|
@@ -25,4 +26,10 @@ class Factbase::Or < Factbase::TermBase
|
|
|
25
26
|
end
|
|
26
27
|
false
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
def simplify
|
|
31
|
+
unique = Factbase::Simplified.new(@operands).unique
|
|
32
|
+
return unique[0] if unique.size == 1
|
|
33
|
+
Factbase::Term.new(@op, unique)
|
|
34
|
+
end
|
|
28
35
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative '../../factbase'
|
|
7
|
+
|
|
8
|
+
# Simplified operands.
|
|
9
|
+
#
|
|
10
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
11
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
12
|
+
# License:: MIT
|
|
13
|
+
class Factbase::Simplified
|
|
14
|
+
def initialize(operands)
|
|
15
|
+
@operands = operands
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Removes duplicate operands
|
|
19
|
+
def unique
|
|
20
|
+
strs = []
|
|
21
|
+
ops = []
|
|
22
|
+
@operands.each do |o|
|
|
23
|
+
o = o.simplify
|
|
24
|
+
s = o.to_s
|
|
25
|
+
next if strs.include?(s)
|
|
26
|
+
strs << s
|
|
27
|
+
ops << o
|
|
28
|
+
end
|
|
29
|
+
ops
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require_relative 'base'
|
|
7
|
+
|
|
8
|
+
# This class represents a specialized 'sum' term.
|
|
9
|
+
# This term calculates the sum of values for a specified key.
|
|
10
|
+
class Factbase::Sum < Factbase::TermBase
|
|
11
|
+
# Constructor.
|
|
12
|
+
# @param [Array] operands Operands
|
|
13
|
+
def initialize(operands)
|
|
14
|
+
super()
|
|
15
|
+
@operands = operands
|
|
16
|
+
@op = :sum
|
|
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 [Integer] The sum of values for the specified key across all maps
|
|
24
|
+
def evaluate(_fact, maps, _fb)
|
|
25
|
+
k = @operands[0]
|
|
26
|
+
raise "A symbol is expected, but '#{k}' provided" unless k.is_a?(Symbol)
|
|
27
|
+
sum = 0
|
|
28
|
+
maps.each do |m|
|
|
29
|
+
vv = m[k.to_s]
|
|
30
|
+
next if vv.nil?
|
|
31
|
+
vv = [vv] unless vv.respond_to?(:to_a)
|
|
32
|
+
vv.each do |v|
|
|
33
|
+
sum += v
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
sum
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/factbase/version.rb
CHANGED
data/lib/factbase.rb
CHANGED
|
@@ -164,17 +164,8 @@ class Factbase
|
|
|
164
164
|
#
|
|
165
165
|
# @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
|
|
166
166
|
def txn
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@maps.map do |m|
|
|
170
|
-
n = m.transform_values(&:dup)
|
|
171
|
-
# rubocop:disable Lint/HashCompareByIdentity
|
|
172
|
-
pairs[n.object_id] = m.object_id
|
|
173
|
-
# rubocop:enable Lint/HashCompareByIdentity
|
|
174
|
-
n
|
|
175
|
-
end
|
|
176
|
-
require_relative 'factbase/taped'
|
|
177
|
-
taped = Factbase::Taped.new(before)
|
|
167
|
+
require_relative 'factbase/lazy_taped'
|
|
168
|
+
taped = Factbase::LazyTaped.new(@maps)
|
|
178
169
|
require_relative 'factbase/churn'
|
|
179
170
|
churn = Factbase::Churn.new
|
|
180
171
|
catch :commit do
|
|
@@ -188,30 +179,32 @@ class Factbase
|
|
|
188
179
|
rescue Factbase::Rollback
|
|
189
180
|
return churn
|
|
190
181
|
end
|
|
191
|
-
seen =
|
|
192
|
-
garbage =
|
|
182
|
+
seen = {}.compare_by_identity
|
|
183
|
+
garbage = {}.compare_by_identity
|
|
184
|
+
pairs = taped.pairs
|
|
193
185
|
taped.deleted.each do |oid|
|
|
194
|
-
|
|
195
|
-
|
|
186
|
+
original = @maps.find { |m| m.object_id == oid }
|
|
187
|
+
next if original.nil?
|
|
188
|
+
garbage[original] = true
|
|
196
189
|
churn.append(0, 1, 0)
|
|
197
190
|
end
|
|
198
191
|
taped.inserted.each do |oid|
|
|
199
|
-
next if seen.include?(oid)
|
|
200
192
|
b = taped.find_by_object_id(oid)
|
|
201
193
|
next if b.nil?
|
|
202
|
-
|
|
194
|
+
next if seen.key?(b)
|
|
195
|
+
seen[b] = true
|
|
203
196
|
@maps << b
|
|
204
197
|
churn.append(1, 0, 0)
|
|
205
198
|
end
|
|
206
199
|
taped.added.each do |oid|
|
|
207
|
-
next if seen.include?(oid)
|
|
208
200
|
b = taped.find_by_object_id(oid)
|
|
209
201
|
next if b.nil?
|
|
210
|
-
|
|
202
|
+
next if seen.key?(b)
|
|
203
|
+
garbage[pairs[b]] = true
|
|
211
204
|
@maps << b
|
|
212
205
|
churn.append(0, 0, 1)
|
|
213
206
|
end
|
|
214
|
-
@maps.delete_if { |m| garbage.
|
|
207
|
+
@maps.delete_if { |m| garbage.key?(m) } unless garbage.empty?
|
|
215
208
|
churn
|
|
216
209
|
end
|
|
217
210
|
|
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.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yegor Bugayenko
|
|
@@ -195,6 +195,9 @@ files:
|
|
|
195
195
|
- lib/factbase/indexed/indexed_term.rb
|
|
196
196
|
- lib/factbase/indexed/indexed_unique.rb
|
|
197
197
|
- lib/factbase/inv.rb
|
|
198
|
+
- lib/factbase/lazy_taped.rb
|
|
199
|
+
- lib/factbase/lazy_taped_array.rb
|
|
200
|
+
- lib/factbase/lazy_taped_hash.rb
|
|
198
201
|
- lib/factbase/light.rb
|
|
199
202
|
- lib/factbase/logged.rb
|
|
200
203
|
- lib/factbase/pre.rb
|
|
@@ -208,13 +211,14 @@ files:
|
|
|
208
211
|
- lib/factbase/tee.rb
|
|
209
212
|
- lib/factbase/term.rb
|
|
210
213
|
- lib/factbase/terms/absent.rb
|
|
211
|
-
- lib/factbase/terms/
|
|
214
|
+
- lib/factbase/terms/agg.rb
|
|
212
215
|
- lib/factbase/terms/always.rb
|
|
213
216
|
- lib/factbase/terms/and.rb
|
|
214
217
|
- lib/factbase/terms/arithmetic.rb
|
|
215
218
|
- lib/factbase/terms/as.rb
|
|
216
219
|
- lib/factbase/terms/assert.rb
|
|
217
220
|
- lib/factbase/terms/base.rb
|
|
221
|
+
- lib/factbase/terms/best.rb
|
|
218
222
|
- lib/factbase/terms/boolean.rb
|
|
219
223
|
- lib/factbase/terms/compare.rb
|
|
220
224
|
- lib/factbase/terms/concat.rb
|
|
@@ -222,6 +226,7 @@ files:
|
|
|
222
226
|
- lib/factbase/terms/defn.rb
|
|
223
227
|
- lib/factbase/terms/div.rb
|
|
224
228
|
- lib/factbase/terms/either.rb
|
|
229
|
+
- lib/factbase/terms/empty.rb
|
|
225
230
|
- lib/factbase/terms/env.rb
|
|
226
231
|
- lib/factbase/terms/eq.rb
|
|
227
232
|
- lib/factbase/terms/exists.rb
|
|
@@ -231,23 +236,26 @@ files:
|
|
|
231
236
|
- lib/factbase/terms/head.rb
|
|
232
237
|
- lib/factbase/terms/inverted.rb
|
|
233
238
|
- lib/factbase/terms/join.rb
|
|
234
|
-
- lib/factbase/terms/logical.rb
|
|
235
239
|
- lib/factbase/terms/lt.rb
|
|
236
240
|
- lib/factbase/terms/lte.rb
|
|
237
241
|
- lib/factbase/terms/many.rb
|
|
238
242
|
- lib/factbase/terms/matches.rb
|
|
243
|
+
- lib/factbase/terms/max.rb
|
|
244
|
+
- lib/factbase/terms/min.rb
|
|
239
245
|
- lib/factbase/terms/minus.rb
|
|
240
246
|
- lib/factbase/terms/never.rb
|
|
241
247
|
- lib/factbase/terms/nil.rb
|
|
242
248
|
- lib/factbase/terms/not.rb
|
|
249
|
+
- lib/factbase/terms/nth.rb
|
|
243
250
|
- lib/factbase/terms/one.rb
|
|
244
251
|
- lib/factbase/terms/or.rb
|
|
245
252
|
- lib/factbase/terms/plus.rb
|
|
246
253
|
- lib/factbase/terms/prev.rb
|
|
247
|
-
- lib/factbase/terms/
|
|
254
|
+
- lib/factbase/terms/simplified.rb
|
|
248
255
|
- lib/factbase/terms/size.rb
|
|
249
256
|
- lib/factbase/terms/sorted.rb
|
|
250
257
|
- lib/factbase/terms/sprintf.rb
|
|
258
|
+
- lib/factbase/terms/sum.rb
|
|
251
259
|
- lib/factbase/terms/times.rb
|
|
252
260
|
- lib/factbase/terms/to_float.rb
|
|
253
261
|
- lib/factbase/terms/to_integer.rb
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
-
# SPDX-License-Identifier: MIT
|
|
5
|
-
|
|
6
|
-
require_relative '../../factbase'
|
|
7
|
-
|
|
8
|
-
# Aggregating terms.
|
|
9
|
-
#
|
|
10
|
-
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
11
|
-
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
12
|
-
# License:: MIT
|
|
13
|
-
module Factbase::Aggregates
|
|
14
|
-
def min(_fact, maps, _fb)
|
|
15
|
-
assert_args(1)
|
|
16
|
-
_best(maps) { |v, b| v < b }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def max(_fact, maps, _fb)
|
|
20
|
-
assert_args(1)
|
|
21
|
-
_best(maps) { |v, b| v > b }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def nth(_fact, maps, _fb)
|
|
25
|
-
assert_args(2)
|
|
26
|
-
pos = @operands[0]
|
|
27
|
-
raise "An integer is expected, but #{pos} provided" unless pos.is_a?(Integer)
|
|
28
|
-
k = @operands[1]
|
|
29
|
-
raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
|
|
30
|
-
m = maps[pos]
|
|
31
|
-
return nil if m.nil?
|
|
32
|
-
m[k.to_s]
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def sum(_fact, maps, _fb)
|
|
36
|
-
k = @operands[0]
|
|
37
|
-
raise "A symbol is expected, but '#{k}' provided" unless k.is_a?(Symbol)
|
|
38
|
-
sum = 0
|
|
39
|
-
maps.each do |m|
|
|
40
|
-
vv = m[k.to_s]
|
|
41
|
-
next if vv.nil?
|
|
42
|
-
vv = [vv] unless vv.respond_to?(:to_a)
|
|
43
|
-
vv.each do |v|
|
|
44
|
-
sum += v
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
sum
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def agg(fact, maps, fb)
|
|
51
|
-
assert_args(2)
|
|
52
|
-
selector = @operands[0]
|
|
53
|
-
raise "A term is expected, but '#{selector}' provided" unless selector.is_a?(Factbase::Term)
|
|
54
|
-
term = @operands[1]
|
|
55
|
-
raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
|
56
|
-
subset = fb.query(selector, maps).each(fb, fact).to_a
|
|
57
|
-
term.evaluate(nil, subset, fb)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def empty(fact, maps, fb)
|
|
61
|
-
assert_args(1)
|
|
62
|
-
term = @operands[0]
|
|
63
|
-
raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
|
64
|
-
# rubocop:disable Lint/UnreachableLoop
|
|
65
|
-
fb.query(term, maps).each(fb, fact) do
|
|
66
|
-
return false
|
|
67
|
-
end
|
|
68
|
-
# rubocop:enable Lint/UnreachableLoop
|
|
69
|
-
true
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def _best(maps)
|
|
73
|
-
k = @operands[0]
|
|
74
|
-
raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
|
|
75
|
-
best = nil
|
|
76
|
-
maps.each do |m|
|
|
77
|
-
vv = m[k.to_s]
|
|
78
|
-
next if vv.nil?
|
|
79
|
-
vv = [vv] unless vv.respond_to?(:to_a)
|
|
80
|
-
vv.each do |v|
|
|
81
|
-
best = v if best.nil? || yield(v, best)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
best
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
-
# SPDX-License-Identifier: MIT
|
|
5
|
-
|
|
6
|
-
require_relative '../../factbase'
|
|
7
|
-
|
|
8
|
-
# Logical terms.
|
|
9
|
-
#
|
|
10
|
-
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
11
|
-
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
12
|
-
# License:: MIT
|
|
13
|
-
module Factbase::Logical
|
|
14
|
-
# Simplifies AND or OR expressions by removing duplicates
|
|
15
|
-
# @return [Factbase::Term] Simplified term
|
|
16
|
-
def and_or_simplify
|
|
17
|
-
strs = []
|
|
18
|
-
ops = []
|
|
19
|
-
@operands.each do |o|
|
|
20
|
-
o = o.simplify
|
|
21
|
-
s = o.to_s
|
|
22
|
-
next if strs.include?(s)
|
|
23
|
-
strs << s
|
|
24
|
-
ops << o
|
|
25
|
-
end
|
|
26
|
-
return ops[0] if ops.size == 1
|
|
27
|
-
self.class.new(@op, ops)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Simplifies AND expressions by removing duplicates
|
|
31
|
-
# @return [Factbase::Term] Simplified term
|
|
32
|
-
def and_simplify
|
|
33
|
-
and_or_simplify
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Simplifies OR expressions by removing duplicates
|
|
37
|
-
# @return [Factbase::Term] Simplified term
|
|
38
|
-
def or_simplify
|
|
39
|
-
and_or_simplify
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
-
# SPDX-License-Identifier: MIT
|
|
5
|
-
|
|
6
|
-
# This module provides shared methods for Factbase terms, including argument validation,
|
|
7
|
-
# symbol-based lookups, and handling of operand values.
|
|
8
|
-
# @todo #302:30min Remove this module and move its methods to Factbase::TermBase.
|
|
9
|
-
# Currently, we use it because we are required to inject all thesse methods into Factbase::Term.
|
|
10
|
-
# When all the terms will inherit from Factbase::TermBase, we can remove this module.
|
|
11
|
-
module Factbase::TermShared
|
|
12
|
-
# Turns it into a string.
|
|
13
|
-
# @return [String] The string of it
|
|
14
|
-
def to_s
|
|
15
|
-
@to_s ||=
|
|
16
|
-
begin
|
|
17
|
-
items = []
|
|
18
|
-
items << @op
|
|
19
|
-
items +=
|
|
20
|
-
@operands.map do |o|
|
|
21
|
-
if o.is_a?(String)
|
|
22
|
-
"'#{o.gsub("'", "\\\\'").gsub('"', '\\\\"')}'"
|
|
23
|
-
elsif o.is_a?(Time)
|
|
24
|
-
o.utc.iso8601
|
|
25
|
-
else
|
|
26
|
-
o.to_s
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
"(#{items.join(' ')})"
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def assert_args(num)
|
|
36
|
-
c = @operands.size
|
|
37
|
-
raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
|
|
38
|
-
raise "Too few (#{c}) operands for '#{@op}' (#{num} expected)" if c < num
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def _by_symbol(pos, fact)
|
|
42
|
-
o = @operands[pos]
|
|
43
|
-
raise "A symbol expected at ##{pos}, but '#{o}' (#{o.class}) provided" unless o.is_a?(Symbol)
|
|
44
|
-
k = o.to_s
|
|
45
|
-
fact[k]
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# @return [Array|nil] Either array of values or NIL
|
|
49
|
-
def _values(pos, fact, maps, fb)
|
|
50
|
-
v = @operands[pos]
|
|
51
|
-
v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::Term)
|
|
52
|
-
v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::TermBase)
|
|
53
|
-
v = fact[v.to_s] if v.is_a?(Symbol)
|
|
54
|
-
return v if v.nil?
|
|
55
|
-
unless v.is_a?(Array)
|
|
56
|
-
v =
|
|
57
|
-
if v.respond_to?(:each)
|
|
58
|
-
v.to_a
|
|
59
|
-
else
|
|
60
|
-
[v]
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
raise 'Why not array?' unless v.is_a?(Array)
|
|
64
|
-
unless v.all? { |i| [Float, Integer, String, Time, TrueClass, FalseClass].any? { |t| i.is_a?(t) } }
|
|
65
|
-
raise 'Wrong type inside'
|
|
66
|
-
end
|
|
67
|
-
v
|
|
68
|
-
end
|
|
69
|
-
end
|