factbase 0.0.49 → 0.0.51

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19ec04ca7c6bf83d50a09c6325e396685e6eb0d2590a4ffca5ccee1fcafa45f1
4
- data.tar.gz: 3550dd7bff7f9ad854689b5c78571211454bf34c09ec0b32fd4b240fc509939c
3
+ metadata.gz: a5185906fd48ccc7df8bfd07a8dd88ae4e7ce4636ec40b3cc3fc174cdeb347d1
4
+ data.tar.gz: a0bc08ef0001d876b9cd26c31fd4a59f094f9ad88dbbb002e345ca4c47cc1a7f
5
5
  SHA512:
6
- metadata.gz: 5d2aa256c677f5d93fb9d41cfbe2548d83ebce3490da521d4a920f06ec774305c5850dc4dea71c76df2e2cda827db3c0292b21f0dae8dc9cee136e6e4cdfb3d3
7
- data.tar.gz: 0e96d7968129d750e5e71940ff5f114bdd920b7ae417ad87cbcea1ef7746c66142c9f1e312980a08cdf2e4ec259747ea6da7f268b6f70c4292225a3b4e6d775d
6
+ metadata.gz: ad553e4da47bb2a6af81a07a64d627dc03ebfe68f96000bc1862df8a25e5bd3cc1563d93ef3a184932af6af286e47912789ca232154cadef512cfc97a3341387
7
+ data.tar.gz: b85a0a97e043e3bc4fa03396a3073c63f18cc268bf296174df66045b7ce47ccd082eb2cbe92d1cd1b07c7a43225ebcd830f624abccdd7d71bfae78825c05dcc5
@@ -32,7 +32,7 @@ jobs:
32
32
  strategy:
33
33
  matrix:
34
34
  os: [ubuntu-20.04, macos-12, windows-2022]
35
- ruby: [3.2, 3.3]
35
+ ruby: [3.2]
36
36
  runs-on: ${{ matrix.os }}
37
37
  steps:
38
38
  - uses: actions/checkout@v4
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,22 +64,22 @@ 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
- parser (3.3.2.0)
76
+ parser (3.3.3.0)
75
77
  ast (~> 2.4.1)
76
78
  racc
77
79
  psych (5.1.2)
78
80
  stringio
79
81
  racc (1.8.0)
80
- rack (3.1.0)
82
+ rack (3.1.3)
81
83
  rack-session (2.0.0)
82
84
  rack (>= 3.0.0)
83
85
  rack-test (2.1.0)
@@ -105,13 +107,13 @@ GEM
105
107
  rdoc (6.7.0)
106
108
  psych (>= 4.0.0)
107
109
  regexp_parser (2.9.2)
108
- reline (0.5.8)
110
+ reline (0.5.9)
109
111
  io-console (~> 0.5)
110
112
  rexml (3.3.0)
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,20 +141,11 @@ 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.0)
145
- rubocop (~> 1.41)
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.0)
155
- rubocop (~> 1.40)
147
+ rubocop-rspec (3.0.1)
148
+ rubocop (~> 1.61)
156
149
  ruby-progressbar (1.13.0)
157
150
  simplecov (0.22.0)
158
151
  docile (~> 1.1)
@@ -163,9 +156,9 @@ 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
- tago (0.0.1)
161
+ tago (0.0.2)
169
162
  thor (1.3.1)
170
163
  tzinfo (2.0.6)
171
164
  concurrent-ruby (~> 1.0)
@@ -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
 
@@ -54,17 +54,21 @@ class Factbase::Looged
54
54
  end
55
55
 
56
56
  def txn(this = self, &)
57
- before = @fb.size
58
- tail = nil
57
+ start = Time.now
58
+ id = nil
59
+ rollback = false
59
60
  r = @fb.txn(this) do |fbt|
60
- tail = Factbase::Looged.elapsed do
61
- yield fbt
62
- end
61
+ id = fbt.object_id
62
+ yield fbt
63
63
  rescue Factbase::Rollback => e
64
- @loog.debug('Txn rolled back')
64
+ rollback = true
65
65
  raise e
66
66
  end
67
- @loog.debug("Txn #{r ? 'modified' : 'didn\'t touch'} #{before} facts #{tail}")
67
+ if rollback
68
+ @loog.debug("Txn ##{id} rolled back in #{start.ago}")
69
+ else
70
+ @loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'} the factbase in #{start.ago}")
71
+ end
68
72
  r
69
73
  end
70
74
 
@@ -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
@@ -24,18 +24,30 @@ require_relative '../factbase'
24
24
  require_relative '../factbase/syntax'
25
25
 
26
26
  # A decorator of a Factbase, that checks rules on every set.
27
+ #
28
+ # Say, you want every fact to have +foo+ property. You want any attempt
29
+ # to insert a fact without this property to lead to a runtime error. Here is how:
30
+ #
31
+ # fb = Factbase.new
32
+ # fb = Factabase::Rules.new(fb, '(exists foo)')
33
+ # fb.txn do |fbt|
34
+ # f = fbt.insert
35
+ # f.bar = 3
36
+ # end # Runtime exception here (transaction won't commit)
37
+ #
27
38
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
28
39
  # Copyright:: Copyright (c) 2024 Yegor Bugayenko
29
40
  # License:: MIT
30
41
  class Factbase::Rules
31
- def initialize(fb, rules, check = Check.new(rules))
42
+ def initialize(fb, rules, check = Check.new(rules), uid: nil)
32
43
  @fb = fb
33
44
  @rules = rules
34
45
  @check = check
46
+ @uid = uid
35
47
  end
36
48
 
37
49
  def dup
38
- Factbase::Rules.new(@fb.dup, @rules, @check)
50
+ Factbase::Rules.new(@fb.dup, @rules, @check, uid: @uid)
39
51
  end
40
52
 
41
53
  def size
@@ -52,15 +64,16 @@ class Factbase::Rules
52
64
 
53
65
  def txn(this = self, &)
54
66
  before = @check
55
- @check = Blind.new
56
- modified = @fb.txn(this, &)
57
- @check = before
58
- if modified
59
- @fb.query('(always)').each do |f|
67
+ later = Later.new(@uid)
68
+ @check = later
69
+ @fb.txn(this) do |fbt|
70
+ yield fbt
71
+ @check = before
72
+ fbt.query('(always)').each do |f|
73
+ next unless later.include?(f)
60
74
  @check.it(f)
61
75
  end
62
76
  end
63
- modified
64
77
  end
65
78
 
66
79
  def export
@@ -88,7 +101,7 @@ class Factbase::Rules
88
101
  def method_missing(*args)
89
102
  r = @fact.method_missing(*args)
90
103
  k = args[0].to_s
91
- @check.it(self) if k.end_with?('=')
104
+ @check.it(@fact) if k.end_with?('=')
92
105
  r
93
106
  end
94
107
 
@@ -143,9 +156,19 @@ class Factbase::Rules
143
156
  # Check one fact (never complaining).
144
157
  #
145
158
  # This is an internal class, it is not supposed to be instantiated directly.
146
- class Blind
147
- def it(_fact)
148
- true
159
+ class Later
160
+ def initialize(uid)
161
+ @uid = uid
162
+ @facts = Set.new
163
+ end
164
+
165
+ def it(fact)
166
+ @facts << fact.send(@uid) unless @uid.nil?
167
+ end
168
+
169
+ def include?(fact)
170
+ return true if @uid.nil?
171
+ @facts.include?(fact.send(@uid))
149
172
  end
150
173
  end
151
174
  end
@@ -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