factbase 0.0.50 → 0.0.51

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: f62ac48c3d2c181b0064e519947fa2fed0ecb0b81660663c91319233eb2fc529
4
- data.tar.gz: f3131120095c0ece6cb01313bad9f55d58145ee289dfc4a4f8bd7e949778521b
3
+ metadata.gz: a5185906fd48ccc7df8bfd07a8dd88ae4e7ce4636ec40b3cc3fc174cdeb347d1
4
+ data.tar.gz: a0bc08ef0001d876b9cd26c31fd4a59f094f9ad88dbbb002e345ca4c47cc1a7f
5
5
  SHA512:
6
- metadata.gz: d7325ad6538622ef5e8764547314ac2970827b44eb70dd37acde333505bd630370bf4ccfe7d1b35916592b89b4dd346c82bcb0c2f2d7532994cfbef0d2a23467
7
- data.tar.gz: eb8554fb580ec058551d6f4f43e47dac22ff13ee427195595a1d9f40caf7f51c59c0c697a849136618a625addf82f726b658752caf755b7eab84b3482a0a8b28
6
+ metadata.gz: ad553e4da47bb2a6af81a07a64d627dc03ebfe68f96000bc1862df8a25e5bd3cc1563d93ef3a184932af6af286e47912789ca232154cadef512cfc97a3341387
7
+ data.tar.gz: b85a0a97e043e3bc4fa03396a3073c63f18cc268bf296174df66045b7ce47ccd082eb2cbe92d1cd1b07c7a43225ebcd830f624abccdd7d71bfae78825c05dcc5
data/Gemfile CHANGED
@@ -28,7 +28,7 @@ gem 'rake', '13.2.1', require: false
28
28
  gem 'rspec-rails', '6.1.2', require: false
29
29
  gem 'rubocop', '1.64.1', require: false
30
30
  gem 'rubocop-performance', '1.21.0', require: false
31
- gem 'rubocop-rspec', '2.31.0', 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.12.0)
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.1)
56
+ irb (1.13.2)
55
57
  rdoc (>= 4.0.0)
56
58
  reline (>= 0.4.2)
57
59
  json (2.7.2)
@@ -62,13 +64,13 @@ GEM
62
64
  loog (0.5.1)
63
65
  minitest (5.23.1)
64
66
  mutex_m (0.2.0)
65
- nokogiri (1.16.5-arm64-darwin)
67
+ nokogiri (1.16.6-arm64-darwin)
66
68
  racc (~> 1.4)
67
- nokogiri (1.16.5-x64-mingw-ucrt)
69
+ nokogiri (1.16.6-x64-mingw-ucrt)
68
70
  racc (~> 1.4)
69
- nokogiri (1.16.5-x86_64-darwin)
71
+ nokogiri (1.16.6-x86_64-darwin)
70
72
  racc (~> 1.4)
71
- nokogiri (1.16.5-x86_64-linux)
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,7 +113,7 @@ GEM
111
113
  strscan
112
114
  rspec-core (3.13.0)
113
115
  rspec-support (~> 3.13.0)
114
- rspec-expectations (3.13.0)
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)
@@ -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-capybara (2.21.0)
143
- rubocop (~> 1.41)
144
- rubocop-factory_bot (2.26.1)
145
- rubocop (~> 1.61)
146
144
  rubocop-performance (1.21.0)
147
145
  rubocop (>= 1.48.1, < 2.0)
148
146
  rubocop-ast (>= 1.31.1, < 2.0)
149
- rubocop-rspec (2.31.0)
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.0)
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.15)
169
+ zeitwerk (2.6.16)
177
170
 
178
171
  PLATFORMS
179
172
  arm64-darwin-22
@@ -188,7 +181,7 @@ DEPENDENCIES
188
181
  rspec-rails (= 6.1.2)
189
182
  rubocop (= 1.64.1)
190
183
  rubocop-performance (= 1.21.0)
191
- rubocop-rspec (= 2.31.0)
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,57 @@ 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 TRUE or FALSE):
59
-
60
- * `(always)` and `(never)` are "true" and "false"
61
- * `(not t)` inverses the `t` if it's boolean (exception otherwise)
62
- * `(or t1 t2 ...)` returns true if at least one argument is true
63
- * `(and t1 t2 ...)` returns true if all arguments are true
64
- * `(when t1 t2)` returns true if `t1` is true and `t2` is true or `t1` is false
65
- * `(exists k)` returns true if `k` property exists in the fact
66
- * `(absent k)` returns true if `k` property is absent
67
- * `(eq a b)` returns true if `a` equals to `b`
68
- * `(lt a b)` returns true if `a` is less than `b`
69
- * `(gt a b)` returns true if `a` is greater than `b`
70
- * `(many a)` return true if there are many values in the `a` property
71
- * `(one a)` returns true if there is only one value in the `a` property
72
- * `(matches a re)` returns true when `a` matches regular expression `re`
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 a)` returns the `i`-th value of the `a` property
77
- * `(size k)` returns cardinality of `k` property (zero if property is absent)
78
- * `(type a)` returns type of `a` ("String", "Integer", "Float", or "Time")
79
- * `(either a b)` returns `b` if `a` is `nil`
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 "foo_*" (gt x 5))` will add `foo_x` and
90
+ all other properties found in the facts that match `(gt x 5)`
80
91
 
81
92
  Also, some simple arithmetic:
82
93
 
83
- * `(plus a b)` is a sum of `a` and `b`
84
- * `(minus a b)` is a deducation of `b` from `a`
85
- * `(times a b)` is a multiplication of `a` and `b`
86
- * `(div a b)` is a division of `a` by `b`
94
+ * `(plus v1 v2)` is a sum of `∑v1` and `∑v2`
95
+ * `(minus v1 v2)` is a deducation of `∑v2` from `∑v1`
96
+ * `(times v1 v2)` is a multiplication of `∏v1` and `∏v2`
97
+ * `(div v1 v2)` is a division of `∏v1` by `∏v2`
87
98
 
88
99
  One term is for meta-programming:
89
100
 
90
- * `(defn foo "self.to_s")` defines a new term using Ruby syntax and returns true
91
- * `(undef foo)` undefines a term (nothing happens if it's not defined yet)
101
+ * `(defn f "self.to_s")` defines a new term using Ruby syntax and returns `true`
102
+ * `(undef f)` undefines a term (nothing happens if it's not defined yet),
103
+ returns `true`
92
104
 
93
105
  There are terms that are history of search aware:
94
106
 
95
- * `(prev a)` returns the value of `a` in the previously seen fact
96
- * `(unique k)` returns true if the value of `k` property hasn't been seen yet
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
107
+ * `(prev p)` returns the value of `p` property in the previously seen fact
108
+ * `(unique p)` returns true if the value of `p` property hasn't been seen yet
105
109
 
106
110
  The `agg` term enables sub-queries by evaluating the first argument (term)
107
111
  over all available facts, passing the entire subset to the second argument,
@@ -110,6 +114,19 @@ and then returning the result as an atomic value:
110
114
  * `(lt age (agg (eq gender 'F') (max age)))` selects all facts where
111
115
  the `age` is smaller than the maximum `age` of all women
112
116
  * `(eq id (agg (always) (max id)))` selects the fact with the largest `id`
117
+ * `(eq salary (agg (eq dept $dept) (avg salary)))` selects the facts
118
+ with the salary average in their departments
119
+
120
+ There are also terms that match the entire factbase
121
+ and must be used primarily inside the `(agg ..)` term:
122
+
123
+ * `(nth v p)` returns the `p` property of the _v_-th fact (must be
124
+ a positive integer)
125
+ * `(first p)` returns the `p` property of the first fact
126
+ * `(count)` returns the tally of facts
127
+ * `(max p)` returns the maximum value of the `p` property in all facts
128
+ * `(min p)` returns the minimum
129
+ * `(sum p)` returns the arithmetic sum of all values of the `p` property
113
130
 
114
131
  ## How to contribute
115
132
 
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,54 @@
22
22
 
23
23
  require_relative '../factbase'
24
24
 
25
- # With the help of this class, it's possible to select a few facts
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::Tuples
45
- def initialize(fb, queries)
46
- @fb = fb
47
- @queries = queries
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
- # Iterate them one by one.
51
- # @yield [Array<Fact>] Arrays of facts one-by-one
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.to_s
56
43
  end
57
44
 
58
- private
59
-
60
- def each_rec(facts, tail, &)
61
- qq = tail.dup
62
- q = qq.shift
63
- return if q.nil?
64
- qt = q.gsub(/\{f([0-9]+).([a-z0-9_]+)\}/) do
65
- facts[Regexp.last_match[1].to_i].send(Regexp.last_match[2])
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
- @fb.query(qt).each do |f|
68
- fs = facts + [f]
69
- if qq.empty?
70
- yield fs
71
- else
72
- each_rec(fs, qq, &)
73
- end
54
+ if k == '[]'
55
+ kk = args[1].to_s
56
+ vv = @props[kk].nil? ? [] : @props[kk]
57
+ vvv = @fact.method_missing(*args)
58
+ vv += vvv unless vvv.nil?
59
+ vv.uniq!
60
+ return vv.empty? ? nil : vv
74
61
  end
62
+ return @props[k][0] unless @props[k].nil?
63
+ @fact.method_missing(*args)
64
+ end
65
+
66
+ # rubocop:disable Style/OptionalBooleanParameter
67
+ def respond_to?(_method, _include_private = false)
68
+ # rubocop:enable Style/OptionalBooleanParameter
69
+ true
70
+ end
71
+
72
+ def respond_to_missing?(_method, _include_private = false)
73
+ true
75
74
  end
76
75
  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
- before = @map[kk]
73
- return if before == v
74
- if before.nil?
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.is_a?(Array) ? v[0] : v
85
+ v[0]
91
86
  end
92
87
  end
93
88
 
@@ -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'} #{before} facts in #{start.ago}")
70
+ @loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'} the factbase in #{start.ago}")
72
71
  end
73
72
  r
74
73
  end
@@ -23,12 +23,15 @@
23
23
  require_relative '../factbase'
24
24
  require_relative 'syntax'
25
25
  require_relative 'fact'
26
+ require_relative 'accum'
26
27
 
27
28
  # Query.
28
29
  #
29
30
  # This is an internal class, it is not supposed to be instantiated directly. It
30
31
  # is created by the +query()+ method of the +Factbase+ class.
31
32
  #
33
+ # It is NOT thread-safe!
34
+ #
32
35
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
33
36
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
34
37
  # License:: MIT
@@ -51,13 +54,15 @@ class Factbase::Query
51
54
  term = Factbase::Syntax.new(@query).to_term
52
55
  yielded = 0
53
56
  @maps.each do |m|
57
+ extras = {}
54
58
  f = Factbase::Fact.new(@mutex, m)
55
- r = term.evaluate(f, @maps)
59
+ a = Factbase::Accum.new(f, extras, false)
60
+ r = term.evaluate(a, @maps)
56
61
  unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
57
62
  raise "Unexpected evaluation result (#{r.class}), must be Boolean at #{@query}"
58
63
  end
59
64
  next unless r
60
- yield f
65
+ yield Factbase::Accum.new(f, extras, true)
61
66
  yielded += 1
62
67
  end
63
68
  yielded
@@ -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
@@ -0,0 +1,57 @@
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 @upper[args[1].to_s[1..]] if args[0].to_s == '[]' && args[1].to_s.start_with?('$')
45
+ @fact.method_missing(*args)
46
+ end
47
+
48
+ # rubocop:disable Style/OptionalBooleanParameter
49
+ def respond_to?(_method, _include_private = false)
50
+ # rubocop:enable Style/OptionalBooleanParameter
51
+ true
52
+ end
53
+
54
+ def respond_to_missing?(_method, _include_private = false)
55
+ true
56
+ end
57
+ end