factbase 0.0.50 → 0.0.52
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 +4 -4
- data/Gemfile.lock +19 -26
- data/README.md +57 -35
- data/factbase.gemspec +1 -0
- data/lib/factbase/{tuples.rb → accum.rb} +40 -40
- data/lib/factbase/fact.rb +4 -9
- data/lib/factbase/looged.rb +18 -5
- data/lib/factbase/query.rb +27 -5
- data/lib/factbase/rules.rb +6 -2
- data/lib/factbase/syntax.rb +2 -2
- data/lib/factbase/tee.rb +59 -0
- data/lib/factbase/term.rb +32 -278
- data/lib/factbase/terms/aggregates.rb +109 -0
- data/lib/factbase/terms/aliases.rb +60 -0
- data/lib/factbase/terms/debug.rb +39 -0
- data/lib/factbase/terms/defn.rb +54 -0
- data/lib/factbase/terms/logical.rb +95 -0
- data/lib/factbase/terms/math.rb +91 -0
- data/lib/factbase/terms/meta.rb +73 -0
- data/lib/factbase/terms/ordering.rb +51 -0
- data/lib/factbase/terms/strings.rb +41 -0
- data/lib/factbase/to_xml.rb +6 -2
- data/lib/factbase.rb +3 -1
- data/test/factbase/terms/test_aggregates.rb +76 -0
- data/test/factbase/terms/test_aliases.rb +79 -0
- data/test/factbase/terms/test_math.rb +99 -0
- data/test/factbase/test_accum.rb +70 -0
- data/test/factbase/test_looged.rb +8 -0
- data/test/factbase/test_query.rb +51 -8
- data/test/factbase/test_rules.rb +8 -1
- data/test/factbase/test_syntax.rb +1 -1
- data/test/factbase/test_tee.rb +53 -0
- data/test/factbase/test_term.rb +7 -76
- data/test/factbase/test_to_xml.rb +26 -2
- data/test/test_factbase.rb +3 -3
- metadata +32 -4
- data/test/factbase/test_tuples.rb +0 -106
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a70fcc45481bd1611d6bf2ed5c41ef40fe6f1ce67494abfa769c3b859b215960
|
4
|
+
data.tar.gz: 609bd76eaca6f4668bbbf5678514503edb1f9184e9abd62321dc02cdd614f80c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97e8cc3c9f53349aaf85aa3f95ba9f0e2525dd85f242ade079083b4ffab042dbb0b3f1ad776573646f7fae0107e709404b70e340fc9660b99ef8770a313aa8a9
|
7
|
+
data.tar.gz: e4d9404781d0314d4e6aa22f93affc3260fc053121c9e70204f8690d30de34feb26533254f15519b9e2562a5562e93398779d890ab78c3acc3afcd8ed8fba782
|
data/Gemfile
CHANGED
@@ -23,12 +23,12 @@
|
|
23
23
|
source 'https://rubygems.org'
|
24
24
|
gemspec
|
25
25
|
|
26
|
-
gem 'minitest', '5.
|
26
|
+
gem 'minitest', '5.24.0', require: false
|
27
27
|
gem 'rake', '13.2.1', require: false
|
28
|
-
gem 'rspec-rails', '6.1.
|
28
|
+
gem 'rspec-rails', '6.1.3', require: false
|
29
29
|
gem 'rubocop', '1.64.1', require: false
|
30
|
-
gem 'rubocop-performance', '1.21.
|
31
|
-
gem 'rubocop-rspec', '
|
30
|
+
gem 'rubocop-performance', '1.21.1', require: false
|
31
|
+
gem 'rubocop-rspec', '3.0.1', require: false
|
32
32
|
gem 'simplecov', '0.22.0', require: false
|
33
33
|
gem 'simplecov-cobertura', '2.1.0', require: false
|
34
34
|
gem 'yard', '0.9.36', require: false
|
data/Gemfile.lock
CHANGED
@@ -2,6 +2,7 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
factbase (0.0.0)
|
5
|
+
backtrace (~> 0.3)
|
5
6
|
json (~> 2.7)
|
6
7
|
loog (~> 0.2)
|
7
8
|
nokogiri (~> 1.10)
|
@@ -38,6 +39,7 @@ GEM
|
|
38
39
|
mutex_m
|
39
40
|
tzinfo (~> 2.0)
|
40
41
|
ast (2.4.2)
|
42
|
+
backtrace (0.4.0)
|
41
43
|
base64 (0.2.0)
|
42
44
|
bigdecimal (3.1.8)
|
43
45
|
builder (3.3.0)
|
@@ -47,11 +49,11 @@ GEM
|
|
47
49
|
diff-lcs (1.5.1)
|
48
50
|
docile (1.4.0)
|
49
51
|
drb (2.2.1)
|
50
|
-
erubi (1.
|
52
|
+
erubi (1.13.0)
|
51
53
|
i18n (1.14.5)
|
52
54
|
concurrent-ruby (~> 1.0)
|
53
55
|
io-console (0.7.2)
|
54
|
-
irb (1.13.
|
56
|
+
irb (1.13.2)
|
55
57
|
rdoc (>= 4.0.0)
|
56
58
|
reline (>= 0.4.2)
|
57
59
|
json (2.7.2)
|
@@ -60,15 +62,15 @@ GEM
|
|
60
62
|
crass (~> 1.0.2)
|
61
63
|
nokogiri (>= 1.12.0)
|
62
64
|
loog (0.5.1)
|
63
|
-
minitest (5.
|
65
|
+
minitest (5.24.0)
|
64
66
|
mutex_m (0.2.0)
|
65
|
-
nokogiri (1.16.
|
67
|
+
nokogiri (1.16.6-arm64-darwin)
|
66
68
|
racc (~> 1.4)
|
67
|
-
nokogiri (1.16.
|
69
|
+
nokogiri (1.16.6-x64-mingw-ucrt)
|
68
70
|
racc (~> 1.4)
|
69
|
-
nokogiri (1.16.
|
71
|
+
nokogiri (1.16.6-x86_64-darwin)
|
70
72
|
racc (~> 1.4)
|
71
|
-
nokogiri (1.16.
|
73
|
+
nokogiri (1.16.6-x86_64-linux)
|
72
74
|
racc (~> 1.4)
|
73
75
|
parallel (1.25.1)
|
74
76
|
parser (3.3.3.0)
|
@@ -111,13 +113,13 @@ GEM
|
|
111
113
|
strscan
|
112
114
|
rspec-core (3.13.0)
|
113
115
|
rspec-support (~> 3.13.0)
|
114
|
-
rspec-expectations (3.13.
|
116
|
+
rspec-expectations (3.13.1)
|
115
117
|
diff-lcs (>= 1.2.0, < 2.0)
|
116
118
|
rspec-support (~> 3.13.0)
|
117
119
|
rspec-mocks (3.13.1)
|
118
120
|
diff-lcs (>= 1.2.0, < 2.0)
|
119
121
|
rspec-support (~> 3.13.0)
|
120
|
-
rspec-rails (6.1.
|
122
|
+
rspec-rails (6.1.3)
|
121
123
|
actionpack (>= 6.1)
|
122
124
|
activesupport (>= 6.1)
|
123
125
|
railties (>= 6.1)
|
@@ -139,19 +141,10 @@ GEM
|
|
139
141
|
unicode-display_width (>= 2.4.0, < 3.0)
|
140
142
|
rubocop-ast (1.31.3)
|
141
143
|
parser (>= 3.3.1.0)
|
142
|
-
rubocop-
|
143
|
-
rubocop (~> 1.41)
|
144
|
-
rubocop-factory_bot (2.26.1)
|
145
|
-
rubocop (~> 1.61)
|
146
|
-
rubocop-performance (1.21.0)
|
144
|
+
rubocop-performance (1.21.1)
|
147
145
|
rubocop (>= 1.48.1, < 2.0)
|
148
146
|
rubocop-ast (>= 1.31.1, < 2.0)
|
149
|
-
rubocop-rspec (
|
150
|
-
rubocop (~> 1.40)
|
151
|
-
rubocop-capybara (~> 2.17)
|
152
|
-
rubocop-factory_bot (~> 2.22)
|
153
|
-
rubocop-rspec_rails (~> 2.28)
|
154
|
-
rubocop-rspec_rails (2.29.1)
|
147
|
+
rubocop-rspec (3.0.1)
|
155
148
|
rubocop (~> 1.61)
|
156
149
|
ruby-progressbar (1.13.0)
|
157
150
|
simplecov (0.22.0)
|
@@ -163,7 +156,7 @@ GEM
|
|
163
156
|
simplecov (~> 0.19)
|
164
157
|
simplecov-html (0.12.3)
|
165
158
|
simplecov_json_formatter (0.1.4)
|
166
|
-
stringio (3.1.
|
159
|
+
stringio (3.1.1)
|
167
160
|
strscan (3.1.0)
|
168
161
|
tago (0.0.2)
|
169
162
|
thor (1.3.1)
|
@@ -173,7 +166,7 @@ GEM
|
|
173
166
|
webrick (1.8.1)
|
174
167
|
yaml (0.3.0)
|
175
168
|
yard (0.9.36)
|
176
|
-
zeitwerk (2.6.
|
169
|
+
zeitwerk (2.6.16)
|
177
170
|
|
178
171
|
PLATFORMS
|
179
172
|
arm64-darwin-22
|
@@ -183,12 +176,12 @@ PLATFORMS
|
|
183
176
|
|
184
177
|
DEPENDENCIES
|
185
178
|
factbase!
|
186
|
-
minitest (= 5.
|
179
|
+
minitest (= 5.24.0)
|
187
180
|
rake (= 13.2.1)
|
188
|
-
rspec-rails (= 6.1.
|
181
|
+
rspec-rails (= 6.1.3)
|
189
182
|
rubocop (= 1.64.1)
|
190
|
-
rubocop-performance (= 1.21.
|
191
|
-
rubocop-rspec (=
|
183
|
+
rubocop-performance (= 1.21.1)
|
184
|
+
rubocop-rspec (= 3.0.1)
|
192
185
|
simplecov (= 0.22.0)
|
193
186
|
simplecov-cobertura (= 2.1.0)
|
194
187
|
yard (= 0.9.36)
|
data/README.md
CHANGED
@@ -55,53 +55,58 @@ assert(f2.query('(eq foo 42)').each.to_a.size == 1)
|
|
55
55
|
```
|
56
56
|
|
57
57
|
There are some boolean terms available in a query
|
58
|
-
(they return either
|
59
|
-
|
60
|
-
* `(always)` and `(never)` are
|
61
|
-
* `(
|
62
|
-
* `(
|
63
|
-
* `(
|
64
|
-
* `(
|
65
|
-
* `(
|
66
|
-
|
67
|
-
* `(
|
68
|
-
* `(
|
69
|
-
* `(
|
70
|
-
* `(
|
71
|
-
* `(
|
72
|
-
* `(
|
58
|
+
(they return either `true` or `false`):
|
59
|
+
|
60
|
+
* `(always)` and `(never)` are `true` and `false`
|
61
|
+
* `(nil v)` is `true` if `v` is `nil`
|
62
|
+
* `(not b)` is the inverse of `b`
|
63
|
+
* `(or b1 b2 ...)` is `true` if at least one argument is `true`
|
64
|
+
* `(and b1 b2 ...)` — if all arguments are `true`
|
65
|
+
* `(when b1 b2)` — if `b1` is `true` and `b2` is `true`
|
66
|
+
or `b1` is `false`
|
67
|
+
* `(exists p)` — if `p` property exists
|
68
|
+
* `(absent p)` — if `p` property is absent
|
69
|
+
* `(zero v)` — if any `v` equals to zero
|
70
|
+
* `(eq v1 v2)` — if any `v1` equals to any `v2`
|
71
|
+
* `(lt v1 v2)` — if any `v1` is less than any `v2`
|
72
|
+
* `(gt v1 v2)` — if any `v1` is greater than any `v2`
|
73
|
+
* `(many v)` — if `v` has many values
|
74
|
+
* `(one v)` — if `v` has one value
|
75
|
+
* `(matches v s)` — if any `v` matches the `s` regular expression
|
73
76
|
|
74
77
|
There are a few terms that return non-boolean values:
|
75
78
|
|
76
|
-
* `(at i
|
77
|
-
* `(size
|
78
|
-
* `(type
|
79
|
-
|
79
|
+
* `(at i v)` is the `i`-th value of `v`
|
80
|
+
* `(size v)` is the cardinality of `v` (zero if `v` is `nil`)
|
81
|
+
* `(type v)` is the type of `v`
|
82
|
+
(`"String"`, `"Integer"`, `"Float"`, `"Time"`, or `"Array"`)
|
83
|
+
* `(either v1 v1)` is `v2` if `v1` is `nil`
|
84
|
+
|
85
|
+
It's possible to modify the facts retrieved, on fly:
|
86
|
+
|
87
|
+
* `(as p v)` adds property `p` with the value `v`
|
88
|
+
* `(join s t)` adds properties named by the `s` mask with the values retrieved
|
89
|
+
by the `t` term, for example, `(join "x<=foo,y<=bar" (gt x 5))` will add
|
90
|
+
`x` and `y` properties, setting them to values found in the `foo` and `bar`
|
91
|
+
properties in the facts that match `(gt x 5)`
|
80
92
|
|
81
93
|
Also, some simple arithmetic:
|
82
94
|
|
83
|
-
* `(plus
|
84
|
-
* `(minus
|
85
|
-
* `(times
|
86
|
-
* `(div
|
95
|
+
* `(plus v1 v2)` is a sum of `∑v1` and `∑v2`
|
96
|
+
* `(minus v1 v2)` is a deducation of `∑v2` from `∑v1`
|
97
|
+
* `(times v1 v2)` is a multiplication of `∏v1` and `∏v2`
|
98
|
+
* `(div v1 v2)` is a division of `∏v1` by `∏v2`
|
87
99
|
|
88
100
|
One term is for meta-programming:
|
89
101
|
|
90
|
-
* `(defn
|
91
|
-
* `(undef
|
102
|
+
* `(defn f "self.to_s")` defines a new term using Ruby syntax and returns `true`
|
103
|
+
* `(undef f)` undefines a term (nothing happens if it's not defined yet),
|
104
|
+
returns `true`
|
92
105
|
|
93
106
|
There are terms that are history of search aware:
|
94
107
|
|
95
|
-
* `(prev
|
96
|
-
* `(unique
|
97
|
-
|
98
|
-
There are also terms that match the entire factbase
|
99
|
-
and must be used inside the `(agg ..)` term:
|
100
|
-
|
101
|
-
* `(count)` returns the tally of facts
|
102
|
-
* `(max k)` returns the maximum value of the `k` property in all facts
|
103
|
-
* `(min k)` returns the minimum
|
104
|
-
* `(sum k)` returns the arithmetic sum of all values of the `k` property
|
108
|
+
* `(prev p)` returns the value of `p` property in the previously seen fact
|
109
|
+
* `(unique p)` returns true if the value of `p` property hasn't been seen yet
|
105
110
|
|
106
111
|
The `agg` term enables sub-queries by evaluating the first argument (term)
|
107
112
|
over all available facts, passing the entire subset to the second argument,
|
@@ -110,6 +115,23 @@ and then returning the result as an atomic value:
|
|
110
115
|
* `(lt age (agg (eq gender 'F') (max age)))` selects all facts where
|
111
116
|
the `age` is smaller than the maximum `age` of all women
|
112
117
|
* `(eq id (agg (always) (max id)))` selects the fact with the largest `id`
|
118
|
+
* `(eq salary (agg (eq dept $dept) (avg salary)))` selects the facts
|
119
|
+
with the salary average in their departments
|
120
|
+
|
121
|
+
There are also terms that match the entire factbase
|
122
|
+
and must be used primarily inside the `(agg ..)` term:
|
123
|
+
|
124
|
+
* `(nth v p)` returns the `p` property of the _v_-th fact (must be
|
125
|
+
a positive integer)
|
126
|
+
* `(first p)` returns the `p` property of the first fact
|
127
|
+
* `(count)` returns the tally of facts
|
128
|
+
* `(max p)` returns the maximum value of the `p` property in all facts
|
129
|
+
* `(min p)` returns the minimum
|
130
|
+
* `(sum p)` returns the arithmetic sum of all values of the `p` property
|
131
|
+
|
132
|
+
It's also possible to use a sub-query in a shorter form than with the `agg`:
|
133
|
+
|
134
|
+
* `(empty q)` is true if the subquery `q` is empty
|
113
135
|
|
114
136
|
## How to contribute
|
115
137
|
|
data/factbase.gemspec
CHANGED
@@ -42,6 +42,7 @@ Gem::Specification.new do |s|
|
|
42
42
|
s.files = `git ls-files`.split($RS)
|
43
43
|
s.rdoc_options = ['--charset=UTF-8']
|
44
44
|
s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
|
45
|
+
s.add_runtime_dependency 'backtrace', '~>0.3'
|
45
46
|
s.add_runtime_dependency 'json', '~>2.7'
|
46
47
|
s.add_runtime_dependency 'loog', '~>0.2'
|
47
48
|
s.add_runtime_dependency 'nokogiri', '~>1.10'
|
@@ -22,55 +22,55 @@
|
|
22
22
|
|
23
23
|
require_relative '../factbase'
|
24
24
|
|
25
|
-
#
|
26
|
-
# from a factbase at a time, which depend on each other. For example,
|
27
|
-
# it's necessary to find a fact where the +name+ is set and then find
|
28
|
-
# another fact, where the salary is the +salary+ is the same as in the
|
29
|
-
# first found fact. Here is how:
|
30
|
-
#
|
31
|
-
# Factbase::Tuples.new(qt, ['(exists name)', '(eq salary, {f0.salary})']).each do |a, b|
|
32
|
-
# puts a.name
|
33
|
-
# puts b.salary
|
34
|
-
# end
|
35
|
-
#
|
36
|
-
# Here, the +{f0.salary}+ is a special substitution place, which is replaced
|
37
|
-
# by the +salary+ of the fact that is found by the previous query.
|
38
|
-
#
|
39
|
-
# The indexing of queries starts from zero.
|
25
|
+
# Accumulator of props.
|
40
26
|
#
|
41
27
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
42
28
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
43
29
|
# License:: MIT
|
44
|
-
class Factbase::
|
45
|
-
|
46
|
-
|
47
|
-
|
30
|
+
class Factbase::Accum
|
31
|
+
# Ctor.
|
32
|
+
# @param [Factbase::Fact] fact The fact to decorate
|
33
|
+
# @param [Hash] props Hash of props that were set
|
34
|
+
# @param [Boolean] pass TRUE if all "set" operations must go through, to the +fact+
|
35
|
+
def initialize(fact, props, pass)
|
36
|
+
@fact = fact
|
37
|
+
@props = props
|
38
|
+
@pass = pass
|
48
39
|
end
|
49
40
|
|
50
|
-
|
51
|
-
|
52
|
-
# @return [Integer] Total number of arrays yielded
|
53
|
-
def each(&)
|
54
|
-
return to_enum(__method__) unless block_given?
|
55
|
-
each_rec([], @queries, &)
|
41
|
+
def to_s
|
42
|
+
"#{@fact} + #{@props}"
|
56
43
|
end
|
57
44
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
45
|
+
def method_missing(*args)
|
46
|
+
k = args[0].to_s
|
47
|
+
if k.end_with?('=')
|
48
|
+
kk = k[0..-2]
|
49
|
+
@props[kk] = [] if @props[kk].nil?
|
50
|
+
@props[kk] << args[1]
|
51
|
+
@fact.method_missing(*args) if @pass
|
52
|
+
return
|
66
53
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
54
|
+
if k == '[]'
|
55
|
+
kk = args[1].to_s
|
56
|
+
vv = @props[kk].nil? ? [] : @props[kk]
|
57
|
+
vvv = @fact.method_missing(*args)
|
58
|
+
vvv = [vvv] unless vvv.nil? || vvv.is_a?(Array)
|
59
|
+
vv += vvv unless vvv.nil?
|
60
|
+
vv.uniq!
|
61
|
+
return vv.empty? ? nil : vv
|
74
62
|
end
|
63
|
+
return @props[k][0] unless @props[k].nil?
|
64
|
+
@fact.method_missing(*args)
|
65
|
+
end
|
66
|
+
|
67
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
68
|
+
def respond_to?(_method, _include_private = false)
|
69
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def respond_to_missing?(_method, _include_private = false)
|
74
|
+
true
|
75
75
|
end
|
76
76
|
end
|
data/lib/factbase/fact.rb
CHANGED
@@ -69,14 +69,9 @@ class Factbase::Fact
|
|
69
69
|
raise "Prop type '#{v.class}' is not allowed" unless [String, Integer, Float, Time].include?(v.class)
|
70
70
|
v = v.utc if v.is_a?(Time)
|
71
71
|
@mutex.synchronize do
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
@map[kk] = [v]
|
76
|
-
else
|
77
|
-
@map[kk] << v
|
78
|
-
@map[kk].uniq!
|
79
|
-
end
|
72
|
+
@map[kk] = [] if @map[kk].nil?
|
73
|
+
@map[kk] << v
|
74
|
+
@map[kk].uniq!
|
80
75
|
end
|
81
76
|
nil
|
82
77
|
elsif k == '[]'
|
@@ -87,7 +82,7 @@ class Factbase::Fact
|
|
87
82
|
raise "Can't get '#{k}', the fact is empty" if @map.empty?
|
88
83
|
raise "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]"
|
89
84
|
end
|
90
|
-
v
|
85
|
+
v[0]
|
91
86
|
end
|
92
87
|
end
|
93
88
|
|
data/lib/factbase/looged.rb
CHANGED
@@ -55,7 +55,6 @@ class Factbase::Looged
|
|
55
55
|
|
56
56
|
def txn(this = self, &)
|
57
57
|
start = Time.now
|
58
|
-
before = @fb.size
|
59
58
|
id = nil
|
60
59
|
rollback = false
|
61
60
|
r = @fb.txn(this) do |fbt|
|
@@ -68,7 +67,7 @@ class Factbase::Looged
|
|
68
67
|
if rollback
|
69
68
|
@loog.debug("Txn ##{id} rolled back in #{start.ago}")
|
70
69
|
else
|
71
|
-
@loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'}
|
70
|
+
@loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'} the factbase in #{start.ago}")
|
72
71
|
end
|
73
72
|
r
|
74
73
|
end
|
@@ -130,12 +129,26 @@ class Factbase::Looged
|
|
130
129
|
@loog = loog
|
131
130
|
end
|
132
131
|
|
133
|
-
def
|
132
|
+
def one(params = {})
|
133
|
+
q = Factbase::Syntax.new(@expr).to_term.to_s
|
134
|
+
r = nil
|
135
|
+
tail = Factbase::Looged.elapsed do
|
136
|
+
r = @fb.query(@expr).one(params)
|
137
|
+
end
|
138
|
+
if r.nil?
|
139
|
+
@loog.debug("Nothing found by '#{q}' #{tail}")
|
140
|
+
else
|
141
|
+
@loog.debug("Found #{r} (#{r.class}) by '#{q}' #{tail}")
|
142
|
+
end
|
143
|
+
r
|
144
|
+
end
|
145
|
+
|
146
|
+
def each(params = {}, &)
|
134
147
|
q = Factbase::Syntax.new(@expr).to_term.to_s
|
135
148
|
if block_given?
|
136
149
|
r = nil
|
137
150
|
tail = Factbase::Looged.elapsed do
|
138
|
-
r = @fb.query(@expr).each(&)
|
151
|
+
r = @fb.query(@expr).each(params, &)
|
139
152
|
end
|
140
153
|
raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
|
141
154
|
if r.zero?
|
@@ -147,7 +160,7 @@ class Factbase::Looged
|
|
147
160
|
else
|
148
161
|
array = []
|
149
162
|
tail = Factbase::Looged.elapsed do
|
150
|
-
@fb.query(@expr).each do |f|
|
163
|
+
@fb.query(@expr).each(params) do |f|
|
151
164
|
array << f
|
152
165
|
end
|
153
166
|
end
|
data/lib/factbase/query.rb
CHANGED
@@ -23,12 +23,16 @@
|
|
23
23
|
require_relative '../factbase'
|
24
24
|
require_relative 'syntax'
|
25
25
|
require_relative 'fact'
|
26
|
+
require_relative 'accum'
|
27
|
+
require_relative 'tee'
|
26
28
|
|
27
29
|
# Query.
|
28
30
|
#
|
29
31
|
# This is an internal class, it is not supposed to be instantiated directly. It
|
30
32
|
# is created by the +query()+ method of the +Factbase+ class.
|
31
33
|
#
|
34
|
+
# It is NOT thread-safe!
|
35
|
+
#
|
32
36
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
33
37
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
34
38
|
# License:: MIT
|
@@ -43,26 +47,44 @@ class Factbase::Query
|
|
43
47
|
@query = query
|
44
48
|
end
|
45
49
|
|
46
|
-
# Iterate
|
50
|
+
# Iterate facts one by one.
|
51
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
47
52
|
# @yield [Fact] Facts one-by-one
|
48
53
|
# @return [Integer] Total number of facts yielded
|
49
|
-
def each
|
50
|
-
return to_enum(__method__) unless block_given?
|
54
|
+
def each(params = {})
|
55
|
+
return to_enum(__method__, params) unless block_given?
|
51
56
|
term = Factbase::Syntax.new(@query).to_term
|
52
57
|
yielded = 0
|
53
58
|
@maps.each do |m|
|
59
|
+
extras = {}
|
54
60
|
f = Factbase::Fact.new(@mutex, m)
|
55
|
-
|
61
|
+
params = params.transform_keys(&:to_s) if params.is_a?(Hash)
|
62
|
+
f = Factbase::Tee.new(f, params)
|
63
|
+
a = Factbase::Accum.new(f, extras, false)
|
64
|
+
r = term.evaluate(a, @maps)
|
56
65
|
unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
|
57
66
|
raise "Unexpected evaluation result (#{r.class}), must be Boolean at #{@query}"
|
58
67
|
end
|
59
68
|
next unless r
|
60
|
-
yield f
|
69
|
+
yield Factbase::Accum.new(f, extras, true)
|
61
70
|
yielded += 1
|
62
71
|
end
|
63
72
|
yielded
|
64
73
|
end
|
65
74
|
|
75
|
+
# Read a single value.
|
76
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
77
|
+
# @return The value evaluated
|
78
|
+
def one(params = {})
|
79
|
+
term = Factbase::Syntax.new(@query).to_term
|
80
|
+
params = params.transform_keys(&:to_s) if params.is_a?(Hash)
|
81
|
+
r = term.evaluate(Factbase::Tee.new(nil, params), @maps)
|
82
|
+
unless %w[String Integer Float Time Array NilClass].include?(r.class.to_s)
|
83
|
+
raise "Incorrect type #{r.class} returned by #{@query}"
|
84
|
+
end
|
85
|
+
r
|
86
|
+
end
|
87
|
+
|
66
88
|
# Delete all facts that match the query.
|
67
89
|
# @return [Integer] Total number of facts deleted
|
68
90
|
def delete!
|
data/lib/factbase/rules.rb
CHANGED
@@ -126,8 +126,12 @@ class Factbase::Rules
|
|
126
126
|
@check = check
|
127
127
|
end
|
128
128
|
|
129
|
-
def
|
130
|
-
|
129
|
+
def one(params = {})
|
130
|
+
@query.one(params)
|
131
|
+
end
|
132
|
+
|
133
|
+
def each(params = {})
|
134
|
+
return to_enum(__method__, params) unless block_given?
|
131
135
|
@query.each do |f|
|
132
136
|
yield Fact.new(f, @check)
|
133
137
|
end
|
data/lib/factbase/syntax.rb
CHANGED
@@ -44,7 +44,7 @@ class Factbase::Syntax
|
|
44
44
|
def to_term
|
45
45
|
build.simplify
|
46
46
|
rescue StandardError => e
|
47
|
-
err = "#{e.message} in #{@query}"
|
47
|
+
err = "#{e.message} in \"#{@query}\""
|
48
48
|
err = "#{err}, tokens: #{@tokens}" unless @tokens.nil?
|
49
49
|
raise err
|
50
50
|
end
|
@@ -142,7 +142,7 @@ class Factbase::Syntax
|
|
142
142
|
elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
|
143
143
|
Time.parse(t)
|
144
144
|
else
|
145
|
-
raise "Wrong symbol format (#{t})" unless t.match?(/^[_a-z][a-zA-Z0-9_]*$/)
|
145
|
+
raise "Wrong symbol format (#{t})" unless t.match?(/^[_a-z\$][a-zA-Z0-9_]*$/)
|
146
146
|
t.to_sym
|
147
147
|
end
|
148
148
|
end
|
data/lib/factbase/tee.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2024 Yegor Bugayenko
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the 'Software'), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
require_relative '../factbase'
|
24
|
+
|
25
|
+
# Tee of two facts.
|
26
|
+
#
|
27
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
28
|
+
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
29
|
+
# License:: MIT
|
30
|
+
class Factbase::Tee
|
31
|
+
# Ctor.
|
32
|
+
# @param [Factbase::Fact] fact Primary fact to use for reading
|
33
|
+
# @param [Factbase::Fact] upper Fact to access with a "$" prefix
|
34
|
+
def initialize(fact, upper)
|
35
|
+
@fact = fact
|
36
|
+
@upper = upper
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
@fact.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def method_missing(*args)
|
44
|
+
return @fact.method_missing(*args) unless args[0].to_s == '[]' && args[1].to_s.start_with?('$')
|
45
|
+
n = args[1].to_s
|
46
|
+
n = n[1..] unless @upper.is_a?(Factbase::Tee)
|
47
|
+
@upper[n]
|
48
|
+
end
|
49
|
+
|
50
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
51
|
+
def respond_to?(_method, _include_private = false)
|
52
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def respond_to_missing?(_method, _include_private = false)
|
57
|
+
true
|
58
|
+
end
|
59
|
+
end
|