factbase 0.0.28 → 0.0.30

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: ffdf62af221a65ab9efa6097b01dab23edba0399bc8551757d450ff828f14367
4
- data.tar.gz: 175dd8ead3a933305d46c8c4bb657a46efc507db3ad1045bb7495614dedfc1c2
3
+ metadata.gz: 58a162c50ba7d2b9d21ca0ff2d62e34acd0c46d2666b596ceffcceb2dc4215c0
4
+ data.tar.gz: 61514fa0fa904ea71540c0f9d4776ea75d5405aa5a1b73d32e051c29606b5fdb
5
5
  SHA512:
6
- metadata.gz: ccade318b2db8568b72fcd29dd8d8cc301a791911a749bba8154485e8e95d1a00c2466477ff7e798f8a5146e87e03c69af8493fd05ae38c9463a8ffc9e29da62
7
- data.tar.gz: 30730a5e848923b73c98f188142f30f56fd8eec95d8839561a6be01956922f60d9fe46a9dcf361b2f6b4bffca993fd20471bd51f1e2840659ba7bc252c9853b2
6
+ metadata.gz: 7b3841073dba72e3fcc6e1fd089235d93829264504447557b3b23f6eb3fa74a4ebccad0d27062af84dc00ab115ca06d9dcc9621bf245d54e09b121bb21983737
7
+ data.tar.gz: aed653f93b7505db410e81d9fefd8fe9d38fc6b89947f89c952992bcdd76dab1d5f9e4a8cd941df426cb7b200855f26fb2981a9698ac35e8e6ea682c693787e7
data/.rubocop.yml CHANGED
@@ -41,9 +41,9 @@ Metrics/AbcSize:
41
41
  Metrics/BlockLength:
42
42
  Max: 30
43
43
  Metrics/CyclomaticComplexity:
44
- Max: 20
44
+ Max: 25
45
45
  Metrics/PerceivedComplexity:
46
- Max: 20
46
+ Max: 25
47
47
  Metrics/ClassLength:
48
48
  Enabled: false
49
49
  Layout/EmptyLineAfterGuardClause:
data/README.md CHANGED
@@ -12,15 +12,26 @@
12
12
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/factbase/blob/master/LICENSE.txt)
13
13
 
14
14
  This Ruby gem manages an in-memory database of facts.
15
+ A fact is simply a map of properties and values.
16
+ The values are either atomic literals or non-empty sets of literals.
17
+ It is possible to delete a fact, but impossible to delete a property from a fact.
18
+
19
+ **ATTENTION**: The current implemention is naive and,
20
+ because of that, very slow. I will be very happy
21
+ if you suggest a better implementation without the change of the interface.
22
+ The `Factbase::query()` method is what mostly needs performance optimization:
23
+ currently it simply iterates through all facts in the factbase in order
24
+ to find those that match the provided terms. Obviously,
25
+ even a simple indexing may significantly increase performance.
15
26
 
16
27
  Here is how you use it (it's thread-safe, by the way):
17
28
 
18
29
  ```ruby
19
30
  fb = Factbase.new
20
31
  f = fb.insert
21
- f.type = 'book'
32
+ f.kind = 'book'
22
33
  f.title = 'Object Thinking'
23
- fb.query('(eq type "book")').each do |f|
34
+ fb.query('(eq kind "book")').each do |f|
24
35
  f.seen = true
25
36
  end
26
37
  fb.insert
@@ -29,17 +40,42 @@ fb.query('(not (exists seen))').each do |f|
29
40
  end
30
41
  ```
31
42
 
32
- You can save the factbase to disc and load it back:
43
+ You can save the factbase to the disc and then load it back:
33
44
 
34
45
  ```ruby
35
46
  file = '/tmp/simple.fb'
36
47
  f1 = Factbase.new
37
- f1.insert
48
+ f = f1.insert
49
+ f.foo = 42
38
50
  File.save(file, f1.export)
39
51
  f2 = Factbase.new
40
52
  f2.import(File.read(file))
53
+ assert(f2.query('(eq foo 42)').each.to_a.size == 1)
41
54
  ```
42
55
 
56
+ All terms available in a query:
57
+
58
+ * `()` is true
59
+ * `(nil)` is false
60
+ * `(not t)` inverses the `t` if it's boolean (exception otherwise)
61
+ * `(or t1 t2 ...)` returns true if at least one argument is true
62
+ * `(and t1 t2 ...)` returns true if all arguments are true
63
+ * `(when t1 t2)` returns true if `t1` is true and `t2` is true or `t1` is false
64
+ * `(exists k)` returns true if `k` property exists in the fact
65
+ * `(absent k)` returns true if `k` property is absent
66
+ * `(eq a b)` returns true if `a` equals to `b`
67
+ * `(lt a b)` returns true if `a` is less than `b`
68
+ * `(gt a b)` returns true if `a` is greater than `b`
69
+ * `(size k)` returns cardinality of `k` property (zero if property is absent)
70
+ * `(type a)` returns type of `a` ("String", "Integer", "Float", or "Time")
71
+ * `(matches a re)` returns true when `a` matches regular expression `re`
72
+ * `(defn foo "self.to_s")` defines a new term using Ruby syntax and returns true
73
+
74
+ There are also terms that match the entire factbase:
75
+
76
+ * `(max k)` returns true if the value of `k` property is the largest in the entire factbase
77
+ * `(min k)` returns true if the value of `k` is the smallest
78
+
43
79
  ## How to contribute
44
80
 
45
81
  Read [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
data/lib/factbase/fact.rb CHANGED
@@ -61,6 +61,7 @@ class Factbase::Fact
61
61
  end
62
62
  @map[kk] = [@map[kk]] unless @map[kk].is_a?(Array)
63
63
  @map[kk] << v
64
+ @map[kk].uniq!
64
65
  end
65
66
  nil
66
67
  elsif k == '[]'
@@ -69,7 +70,7 @@ class Factbase::Fact
69
70
  v = @map[k]
70
71
  if v.nil?
71
72
  raise "Can't get '#{k}', the fact is empty" if @map.empty?
72
- raise "Can't find '#{k}' attribute in [#{@map.keys.join(', ')}]"
73
+ raise "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]"
73
74
  end
74
75
  v.is_a?(Array) ? v[0] : v
75
76
  end
@@ -42,7 +42,7 @@ class Factbase::Looged
42
42
 
43
43
  def insert
44
44
  f = @fb.insert
45
- @loog.debug('Inserted new fact')
45
+ @loog.debug("Inserted new fact ##{@fb.size}")
46
46
  Fact.new(f, @loog)
47
47
  end
48
48
 
@@ -79,6 +79,7 @@ class Factbase::Looged
79
79
  v = args[1]
80
80
  s = v.is_a?(Time) ? v.utc.iso8601 : v.to_s
81
81
  s = v.to_s.inspect if v.is_a?(String)
82
+ s = "#{s[0..40]}...#{s[-40..]}" if s.length > 80
82
83
  @loog.debug("Set '#{k[0..-2]}' to #{s} (#{v.class})") if k.end_with?('=')
83
84
  r
84
85
  end
@@ -103,13 +104,14 @@ class Factbase::Looged
103
104
  end
104
105
 
105
106
  def each(&)
107
+ q = Factbase::Syntax.new(@expr).to_term.to_s
106
108
  if block_given?
107
109
  r = @query.each(&)
108
110
  raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
109
111
  if r.zero?
110
- @loog.debug("Nothing found by '#{@expr}'")
112
+ @loog.debug("Nothing found by '#{q}'")
111
113
  else
112
- @loog.debug("Found #{r} fact(s) by '#{@expr}'")
114
+ @loog.debug("Found #{r} fact(s) by '#{q}'")
113
115
  end
114
116
  r
115
117
  else
@@ -120,9 +122,9 @@ class Factbase::Looged
120
122
  end
121
123
  # rubocop:enable Style/MapIntoArray
122
124
  if array.empty?
123
- @loog.debug("Nothing found by '#{@expr}'")
125
+ @loog.debug("Nothing found by '#{q}'")
124
126
  else
125
- @loog.debug("Found #{array.size} fact(s) by '#{@expr}'")
127
+ @loog.debug("Found #{array.size} fact(s) by '#{q}'")
126
128
  end
127
129
  array
128
130
  end
@@ -40,11 +40,11 @@ class Factbase::Query
40
40
  # @return [Integer] Total number of facts yielded
41
41
  def each
42
42
  return to_enum(__method__) unless block_given?
43
- term = Factbase::Syntax.new(@query).to_term
43
+ term = Factbase::Syntax.new(@query).to_term.on(@maps)
44
44
  yielded = 0
45
45
  @maps.each do |m|
46
46
  f = Factbase::Fact.new(@mutex, m)
47
- next unless term.eval(f)
47
+ next unless term.evaluate(f)
48
48
  yield f
49
49
  yielded += 1
50
50
  end
@@ -59,7 +59,7 @@ class Factbase::Query
59
59
  @mutex.synchronize do
60
60
  @maps.delete_if do |m|
61
61
  f = Factbase::Fact.new(@mutex, m)
62
- if term.eval(f)
62
+ if term.evaluate(f)
63
63
  deleted += 1
64
64
  true
65
65
  else
@@ -117,7 +117,7 @@ class Factbase::Rules
117
117
  end
118
118
 
119
119
  def it(fact)
120
- return if Factbase::Syntax.new(@expr).to_term.eval(fact)
120
+ return if Factbase::Syntax.new(@expr).to_term.evaluate(fact)
121
121
  raise "The fact is in invalid state: #{fact}"
122
122
  end
123
123
  end
@@ -41,9 +41,10 @@ class Factbase::Syntax
41
41
  def to_term
42
42
  @tokens ||= to_tokens
43
43
  @ast ||= to_ast(@tokens, 0)
44
+ raise "Too many terms: #{@query}" if @ast[1] != @tokens.size
44
45
  term = @ast[0]
45
- raise 'No terms found' if term.nil?
46
- raise 'Not a term' unless term.is_a?(Factbase::Term)
46
+ raise "No terms found: #{@query}" if term.nil?
47
+ raise "Not a term: #{@query}" unless term.is_a?(Factbase::Term)
47
48
  term
48
49
  end
49
50
 
@@ -57,7 +58,7 @@ class Factbase::Syntax
57
58
  # is the term/literal and the second one is the position where the
58
59
  # scanning should continue.
59
60
  def to_ast(tokens, at)
60
- raise "Closing too soon at ##{at}" if tokens[at] == :close
61
+ raise "Closing too soon at ##{at}: #{@query}" if tokens[at] == :close
61
62
  return [tokens[at], at + 1] unless tokens[at] == :open
62
63
  at += 1
63
64
  op = tokens[at]
@@ -65,11 +66,11 @@ class Factbase::Syntax
65
66
  operands = []
66
67
  at += 1
67
68
  loop do
68
- raise "End of token stream at ##{at}" if tokens[at].nil?
69
+ raise "End of token stream at ##{at}: #{@query}" if tokens[at].nil?
69
70
  break if tokens[at] == :close
70
71
  (operand, at1) = to_ast(tokens, at)
71
- raise "Stuck at position ##{at}" if at == at1
72
- raise "Jump back at position ##{at}" if at1 < at
72
+ raise "Stuck at position ##{at}: #{@query}" if at == at1
73
+ raise "Jump back at position ##{at}: #{@query}" if at1 < at
73
74
  at = at1
74
75
  operands << operand
75
76
  break if tokens[at] == :close
@@ -109,12 +110,12 @@ class Factbase::Syntax
109
110
  acc += c
110
111
  end
111
112
  end
112
- raise 'String not closed' if string
113
+ raise "String not closed: : #{@query}" if string
113
114
  list.map do |t|
114
115
  if t.is_a?(Symbol)
115
116
  t
116
117
  elsif t.start_with?('\'', '"')
117
- raise 'String literal can\'t be empty' if t.length <= 2
118
+ raise "String literal can't be empty: #{@query}" if t.length <= 2
118
119
  t[1..-2]
119
120
  elsif t.match?(/^[0-9]+$/)
120
121
  t.to_i
@@ -123,6 +124,7 @@ class Factbase::Syntax
123
124
  elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
124
125
  Time.parse(t)
125
126
  else
127
+ raise "Wrong symbol format (#{t}): #{@query}" unless t.match?(/^[a-z][a-zA-Z0-9_]*$/)
126
128
  t.to_sym
127
129
  end
128
130
  end
data/lib/factbase/term.rb CHANGED
@@ -41,10 +41,22 @@ class Factbase::Term
41
41
  # Does it match the fact?
42
42
  # @param [Factbase::Fact] fact The fact
43
43
  # @return [bool] TRUE if matches
44
- def eval(fact)
44
+ def evaluate(fact)
45
45
  send(@op, fact)
46
46
  end
47
47
 
48
+ # Put it into the context: let it see the entire array of maps.
49
+ # @param [Array] maps The maps
50
+ # @return [Factbase::Term] Itself
51
+ def on(maps)
52
+ m = "#{@op}_on"
53
+ send(m, maps) if respond_to?(m, true)
54
+ @operands.each do |o|
55
+ o.on(maps) if o.is_a?(Factbase::Term)
56
+ end
57
+ self
58
+ end
59
+
48
60
  # Turns it into a string.
49
61
  # @return [String] The string of it
50
62
  def to_s
@@ -71,19 +83,19 @@ class Factbase::Term
71
83
 
72
84
  def not(fact)
73
85
  assert_args(1)
74
- !@operands[0].eval(fact)
86
+ !only_bool(the_value(0, fact))
75
87
  end
76
88
 
77
89
  def or(fact)
78
- @operands.each do |o|
79
- return true if o.eval(fact)
90
+ (0..@operands.size - 1).each do |i|
91
+ return true if only_bool(the_value(i, fact))
80
92
  end
81
93
  false
82
94
  end
83
95
 
84
96
  def and(fact)
85
- @operands.each do |o|
86
- return false unless o.eval(fact)
97
+ (0..@operands.size - 1).each do |i|
98
+ return false unless only_bool(the_value(i, fact))
87
99
  end
88
100
  true
89
101
  end
@@ -92,7 +104,7 @@ class Factbase::Term
92
104
  assert_args(2)
93
105
  a = @operands[0]
94
106
  b = @operands[1]
95
- !a.eval(fact) || (a.eval(fact) && b.eval(fact))
107
+ !a.evaluate(fact) || (a.evaluate(fact) && b.evaluate(fact))
96
108
  end
97
109
 
98
110
  def exists(fact)
@@ -132,6 +144,17 @@ class Factbase::Term
132
144
  v.class.to_s
133
145
  end
134
146
 
147
+ def matches(fact)
148
+ assert_args(2)
149
+ str = the_value(0, fact)
150
+ raise 'String is nil' if str.nil?
151
+ raise 'Exactly one string expected' unless str.size == 1
152
+ re = the_value(1, fact)
153
+ raise 'Regexp is nil' if re.nil?
154
+ raise 'Exactly one regexp expected' unless re.size == 1
155
+ str[0].to_s.match?(re[0])
156
+ end
157
+
135
158
  def arithmetic(op, fact)
136
159
  assert_args(2)
137
160
  lefts = the_value(0, fact)
@@ -147,6 +170,36 @@ class Factbase::Term
147
170
  end
148
171
  end
149
172
 
173
+ def defn(_fact)
174
+ fn = @operands[0]
175
+ raise 'A symbol expected as first argument of defn' unless fn.is_a?(Symbol)
176
+ e = "class Factbase::Term\nprivate\ndef #{fn}(fact)\n#{@operands[1]}\nend\nend"
177
+ # rubocop:disable Security/Eval
178
+ eval(e)
179
+ # rubocop:enable Security/Eval
180
+ true
181
+ end
182
+
183
+ def min(fact)
184
+ vv = the_value(0, fact)
185
+ return nil if vv.nil?
186
+ vv.any? { |v| v == @min }
187
+ end
188
+
189
+ def max(fact)
190
+ vv = the_value(0, fact)
191
+ return nil if vv.nil?
192
+ vv.any? { |v| v == @max }
193
+ end
194
+
195
+ def max_on(maps)
196
+ @max = best(maps) { |v, b| v > b }
197
+ end
198
+
199
+ def min_on(maps)
200
+ @min = best(maps) { |v, b| v < b }
201
+ end
202
+
150
203
  def assert_args(num)
151
204
  c = @operands.size
152
205
  raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
@@ -162,10 +215,32 @@ class Factbase::Term
162
215
 
163
216
  def the_value(pos, fact)
164
217
  v = @operands[pos]
165
- v = v.eval(fact) if v.is_a?(Factbase::Term)
218
+ v = v.evaluate(fact) if v.is_a?(Factbase::Term)
166
219
  v = fact[v.to_s] if v.is_a?(Symbol)
167
220
  return v if v.nil?
168
221
  v = [v] unless v.is_a?(Array)
169
222
  v
170
223
  end
224
+
225
+ def only_bool(val)
226
+ val = val[0] if val.is_a?(Array)
227
+ return false if val.nil?
228
+ raise "Boolean expected, while #{val.class} received" unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
229
+ val
230
+ end
231
+
232
+ def best(maps)
233
+ k = @operands[0]
234
+ raise "A symbol expected, but provided: #{k}" unless k.is_a?(Symbol)
235
+ best = nil
236
+ maps.each do |m|
237
+ vv = m[k.to_s]
238
+ next if vv.nil?
239
+ vv = [vv] unless vv.is_a?(Array)
240
+ vv.each do |v|
241
+ best = v if best.nil? || yield(v, best)
242
+ end
243
+ end
244
+ best
245
+ end
171
246
  end
@@ -37,11 +37,13 @@ class Factbase::ToXML
37
37
  # Convert the entire factbase into XML.
38
38
  # @return [String] The factbase in XML format
39
39
  def xml
40
+ bytes = @fb.export
41
+ maps = Marshal.load(bytes)
40
42
  meta = {
41
- factbase_version: Factbase::VERSION,
42
- dob: Time.now.utc.iso8601
43
+ version: Factbase::VERSION,
44
+ dob: Time.now.utc.iso8601,
45
+ size: bytes.size
43
46
  }
44
- maps = Marshal.load(@fb.export)
45
47
  Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
46
48
  xml.fb(meta) do
47
49
  maps.each do |m|
data/lib/factbase.rb CHANGED
@@ -29,7 +29,7 @@ require 'yaml'
29
29
  # License:: MIT
30
30
  class Factbase
31
31
  # Current version of the gem (changed by .rultor.yml on every release)
32
- VERSION = '0.0.28'
32
+ VERSION = '0.0.30'
33
33
 
34
34
  # Constructor.
35
35
  def initialize(facts = [])
@@ -39,6 +39,16 @@ class TestFact < Minitest::Test
39
39
  assert_equal([42, 256], f['foo'], f.to_s)
40
40
  end
41
41
 
42
+ def test_keeps_values_unique
43
+ map = {}
44
+ f = Factbase::Fact.new(Mutex.new, map)
45
+ f.foo = 42
46
+ f.foo = 'Hello'
47
+ assert_equal(2, map['foo'].size)
48
+ f.foo = 42
49
+ assert_equal(2, map['foo'].size)
50
+ end
51
+
42
52
  def test_fails_when_empty
43
53
  f = Factbase::Fact.new(Mutex.new, {})
44
54
  assert_raises do
@@ -74,6 +84,14 @@ class TestFact < Minitest::Test
74
84
  assert_equal(42, f.foo_bar, f.to_s)
75
85
  end
76
86
 
87
+ def test_set_twice_same_value
88
+ map = {}
89
+ f = Factbase::Fact.new(Mutex.new, map)
90
+ f.foo = 42
91
+ f.foo = 42
92
+ assert_equal(42, map['foo'])
93
+ end
94
+
77
95
  def test_time_in_utc
78
96
  f = Factbase::Fact.new(Mutex.new, {})
79
97
  t = Time.now
@@ -69,13 +69,30 @@ class TestLooged < Minitest::Test
69
69
  fb.insert
70
70
  fb.insert.bar = 3
71
71
  fb.insert
72
+ fb.insert.str =
73
+ "Он поскорей звонит. Вбегает
74
+ К нему слуга француз Гильо,
75
+ Халат и туфли предлагает
76
+ И подает ему белье.
77
+ Спешит Онегин одеваться,
78
+ Слуге велит приготовляться
79
+ С ним вместе ехать и с собой
80
+ Взять также ящик боевой.
81
+ Готовы санки беговые.
82
+ Он сел, на мельницу летит.
83
+ Примчались. Он слуге велит
84
+ Лепажа стволы роковые
85
+ Нести за ним, а лошадям
86
+ Отъехать в поле к двум дубкам."
72
87
  fb.query('(exists bar)').each(&:to_s)
73
88
  fb.query('(not (exists bar))').delete!
74
89
  [
75
- 'Inserted new fact',
90
+ 'Inserted new fact #1',
91
+ 'Inserted new fact #2',
76
92
  'Set \'bar\' to 3 (Integer)',
93
+ 'Set \'str\' to "Он поскорей звонит. Вбегает\n К нем...м\n Отъехать в поле к двум дубкам." (String)',
77
94
  'Found 1 fact(s) by \'(exists bar)\'',
78
- 'Deleted 2 fact(s) by \'(not (exists bar))\''
95
+ 'Deleted 3 fact(s) by \'(not (exists bar))\''
79
96
  ].each do |s|
80
97
  assert(log.to_s.include?("#{s}\n"), "#{log}\n")
81
98
  end
@@ -60,6 +60,10 @@ class TestQuery < Minitest::Test
60
60
  '(eq (size hello) 0)' => 3,
61
61
  '(eq num pi)' => 0,
62
62
  '(absent time)' => 2,
63
+ '(max num)' => 1,
64
+ '(and (exists time) (max num))' => 0,
65
+ '(and (exists pi) (max num))' => 1,
66
+ '(min time)' => 1,
63
67
  '(and (absent time) (exists pi))' => 1,
64
68
  "(and (exists time) (not (\t\texists pi)))" => 1,
65
69
  "(or (eq num 66) (lt time #{(Time.now - 200).utc.iso8601}))" => 1
@@ -45,8 +45,9 @@ class TestSyntax < Minitest::Test
45
45
  '(foo)',
46
46
  '(foo (bar) (zz 77) )',
47
47
  "(eq foo \n\n 'Hello, world!'\n)\n",
48
+ "(eq x 'Hello, \\' \n) \\' ( world!')",
48
49
  "# this is a comment\n(eq foo # test\n 42)\n\n# another comment\n",
49
- "(or ( a 4) (b 5) () (and () (c 5) \t\t(r 7 8s 8is 'Foo')))"
50
+ "(or ( a 4) (b 5) () (and () (c 5) \t\t(r 7 w8s w8is 'Foo')))"
50
51
  ].each do |q|
51
52
  Factbase::Syntax.new(q).to_term
52
53
  end
@@ -83,7 +84,7 @@ class TestSyntax < Minitest::Test
83
84
  '(or (eq bar 888) (eq z 1))' => true,
84
85
  "(or (gt bar 100) (eq foo 'Hello, world!'))" => true
85
86
  }.each do |k, v|
86
- assert_equal(v, Factbase::Syntax.new(k).to_term.eval(m), k)
87
+ assert_equal(v, Factbase::Syntax.new(k).to_term.evaluate(m), k)
87
88
  end
88
89
  end
89
90
 
@@ -91,11 +92,13 @@ class TestSyntax < Minitest::Test
91
92
  [
92
93
  '',
93
94
  '(foo',
95
+ '(foo 1) (bar 2)',
94
96
  'some text',
95
97
  '"hello, world!',
96
98
  '(foo 7',
97
99
  "(foo 7 'Dude'",
98
100
  '(foo x y z (',
101
+ '(bad-term-name 42)',
99
102
  '(foo x y (z t (f 42 ',
100
103
  ')foo ) y z)',
101
104
  '(x "")',
@@ -103,7 +106,7 @@ class TestSyntax < Minitest::Test
103
106
  ')',
104
107
  '"'
105
108
  ].each do |q|
106
- assert_raises do
109
+ assert_raises(q) do
107
110
  Factbase::Syntax.new(q).to_term
108
111
  end
109
112
  end
@@ -30,95 +30,102 @@ require_relative '../../lib/factbase/term'
30
30
  class TestTerm < Minitest::Test
31
31
  def test_simple_matching
32
32
  t = Factbase::Term.new(:eq, [:foo, 42])
33
- assert(t.eval(fact('foo' => [42])))
34
- assert(!t.eval(fact('foo' => 'Hello!')))
35
- assert(!t.eval(fact('bar' => ['Hello!'])))
33
+ assert(t.evaluate(fact('foo' => [42])))
34
+ assert(!t.evaluate(fact('foo' => 'Hello!')))
35
+ assert(!t.evaluate(fact('bar' => ['Hello!'])))
36
36
  end
37
37
 
38
38
  def test_eq_matching
39
39
  t = Factbase::Term.new(:eq, [:foo, 42])
40
- assert(t.eval(fact('foo' => 42)))
41
- assert(t.eval(fact('foo' => [10, 5, 6, -8, 'hey', 42, 9, 'fdsf'])))
42
- assert(!t.eval(fact('foo' => [100])))
43
- assert(!t.eval(fact('foo' => [])))
44
- assert(!t.eval(fact('bar' => [])))
40
+ assert(t.evaluate(fact('foo' => 42)))
41
+ assert(t.evaluate(fact('foo' => [10, 5, 6, -8, 'hey', 42, 9, 'fdsf'])))
42
+ assert(!t.evaluate(fact('foo' => [100])))
43
+ assert(!t.evaluate(fact('foo' => [])))
44
+ assert(!t.evaluate(fact('bar' => [])))
45
45
  end
46
46
 
47
47
  def test_eq_matching_time
48
48
  now = Time.now
49
49
  t = Factbase::Term.new(:eq, [:foo, Time.parse(now.iso8601)])
50
- assert(t.eval(fact('foo' => now)))
51
- assert(t.eval(fact('foo' => [now, Time.now])))
50
+ assert(t.evaluate(fact('foo' => now)))
51
+ assert(t.evaluate(fact('foo' => [now, Time.now])))
52
52
  end
53
53
 
54
54
  def test_lt_matching
55
55
  t = Factbase::Term.new(:lt, [:foo, 42])
56
- assert(t.eval(fact('foo' => [10])))
57
- assert(!t.eval(fact('foo' => [100])))
58
- assert(!t.eval(fact('foo' => 100)))
59
- assert(!t.eval(fact('bar' => 100)))
56
+ assert(t.evaluate(fact('foo' => [10])))
57
+ assert(!t.evaluate(fact('foo' => [100])))
58
+ assert(!t.evaluate(fact('foo' => 100)))
59
+ assert(!t.evaluate(fact('bar' => 100)))
60
60
  end
61
61
 
62
62
  def test_gt_matching
63
63
  t = Factbase::Term.new(:gt, [:foo, 42])
64
- assert(t.eval(fact('foo' => [100])))
65
- assert(t.eval(fact('foo' => 100)))
66
- assert(!t.eval(fact('foo' => [10])))
67
- assert(!t.eval(fact('foo' => 10)))
68
- assert(!t.eval(fact('bar' => 10)))
64
+ assert(t.evaluate(fact('foo' => [100])))
65
+ assert(t.evaluate(fact('foo' => 100)))
66
+ assert(!t.evaluate(fact('foo' => [10])))
67
+ assert(!t.evaluate(fact('foo' => 10)))
68
+ assert(!t.evaluate(fact('bar' => 10)))
69
69
  end
70
70
 
71
71
  def test_lt_matching_time
72
72
  t = Factbase::Term.new(:lt, [:foo, Time.now])
73
- assert(t.eval(fact('foo' => [Time.now - 100])))
74
- assert(!t.eval(fact('foo' => [Time.now + 100])))
75
- assert(!t.eval(fact('bar' => [100])))
73
+ assert(t.evaluate(fact('foo' => [Time.now - 100])))
74
+ assert(!t.evaluate(fact('foo' => [Time.now + 100])))
75
+ assert(!t.evaluate(fact('bar' => [100])))
76
76
  end
77
77
 
78
78
  def test_gt_matching_time
79
79
  t = Factbase::Term.new(:gt, [:foo, Time.now])
80
- assert(t.eval(fact('foo' => [Time.now + 100])))
81
- assert(!t.eval(fact('foo' => [Time.now - 100])))
82
- assert(!t.eval(fact('bar' => [100])))
80
+ assert(t.evaluate(fact('foo' => [Time.now + 100])))
81
+ assert(!t.evaluate(fact('foo' => [Time.now - 100])))
82
+ assert(!t.evaluate(fact('bar' => [100])))
83
83
  end
84
84
 
85
85
  def test_not_matching
86
86
  t = Factbase::Term.new(:not, [Factbase::Term.new(:nil, [])])
87
- assert(!t.eval(fact('foo' => [100])))
87
+ assert(!t.evaluate(fact('foo' => [100])))
88
88
  end
89
89
 
90
90
  def test_not_eq_matching
91
91
  t = Factbase::Term.new(:not, [Factbase::Term.new(:eq, [:foo, 100])])
92
- assert(t.eval(fact('foo' => [42, 12, -90])))
93
- assert(!t.eval(fact('foo' => 100)))
92
+ assert(t.evaluate(fact('foo' => [42, 12, -90])))
93
+ assert(!t.evaluate(fact('foo' => 100)))
94
94
  end
95
95
 
96
96
  def test_size_matching
97
97
  t = Factbase::Term.new(:size, [:foo])
98
- assert_equal(3, t.eval(fact('foo' => [42, 12, -90])))
99
- assert_equal(0, t.eval(fact('bar' => 100)))
98
+ assert_equal(3, t.evaluate(fact('foo' => [42, 12, -90])))
99
+ assert_equal(0, t.evaluate(fact('bar' => 100)))
100
100
  end
101
101
 
102
102
  def test_exists_matching
103
103
  t = Factbase::Term.new(:exists, [:foo])
104
- assert(t.eval(fact('foo' => [42, 12, -90])))
105
- assert(!t.eval(fact('bar' => 100)))
104
+ assert(t.evaluate(fact('foo' => [42, 12, -90])))
105
+ assert(!t.evaluate(fact('bar' => 100)))
106
106
  end
107
107
 
108
108
  def test_absent_matching
109
109
  t = Factbase::Term.new(:absent, [:foo])
110
- assert(t.eval(fact('z' => [42, 12, -90])))
111
- assert(!t.eval(fact('foo' => 100)))
110
+ assert(t.evaluate(fact('z' => [42, 12, -90])))
111
+ assert(!t.evaluate(fact('foo' => 100)))
112
112
  end
113
113
 
114
114
  def test_type_matching
115
115
  t = Factbase::Term.new(:type, [:foo])
116
- assert_equal('Integer', t.eval(fact('foo' => 42)))
117
- assert_equal('Array', t.eval(fact('foo' => [1, 2, 3])))
118
- assert_equal('String', t.eval(fact('foo' => 'Hello, world!')))
119
- assert_equal('Float', t.eval(fact('foo' => 3.14)))
120
- assert_equal('Time', t.eval(fact('foo' => Time.now)))
121
- assert_equal('nil', t.eval(fact))
116
+ assert_equal('Integer', t.evaluate(fact('foo' => 42)))
117
+ assert_equal('Array', t.evaluate(fact('foo' => [1, 2, 3])))
118
+ assert_equal('String', t.evaluate(fact('foo' => 'Hello, world!')))
119
+ assert_equal('Float', t.evaluate(fact('foo' => 3.14)))
120
+ assert_equal('Time', t.evaluate(fact('foo' => Time.now)))
121
+ assert_equal('nil', t.evaluate(fact))
122
+ end
123
+
124
+ def test_regexp_matching
125
+ t = Factbase::Term.new(:matches, [:foo, '[a-z]+'])
126
+ assert(t.evaluate(fact('foo' => 'hello')))
127
+ assert(t.evaluate(fact('foo' => 'hello 42')))
128
+ assert(!t.evaluate(fact('foo' => 42)))
122
129
  end
123
130
 
124
131
  def test_or_matching
@@ -129,9 +136,9 @@ class TestTerm < Minitest::Test
129
136
  Factbase::Term.new(:eq, [:bar, 5])
130
137
  ]
131
138
  )
132
- assert(t.eval(fact('foo' => [4])))
133
- assert(t.eval(fact('bar' => [5])))
134
- assert(!t.eval(fact('bar' => [42])))
139
+ assert(t.evaluate(fact('foo' => [4])))
140
+ assert(t.evaluate(fact('bar' => [5])))
141
+ assert(!t.evaluate(fact('bar' => [42])))
135
142
  end
136
143
 
137
144
  def test_when_matching
@@ -142,9 +149,16 @@ class TestTerm < Minitest::Test
142
149
  Factbase::Term.new(:eq, [:bar, 5])
143
150
  ]
144
151
  )
145
- assert(t.eval(fact('foo' => 4, 'bar' => 5)))
146
- assert(!t.eval(fact('foo' => 4)))
147
- assert(t.eval(fact('foo' => 5, 'bar' => 5)))
152
+ assert(t.evaluate(fact('foo' => 4, 'bar' => 5)))
153
+ assert(!t.evaluate(fact('foo' => 4)))
154
+ assert(t.evaluate(fact('foo' => 5, 'bar' => 5)))
155
+ end
156
+
157
+ def test_defn_simple
158
+ t = Factbase::Term.new(:defn, [:foo, 'self.to_s'])
159
+ assert_equal(true, t.evaluate(fact('foo' => 4)))
160
+ t1 = Factbase::Term.new(:foo, ['hello, world!'])
161
+ assert_equal('(foo \'hello, world!\')', t1.evaluate(fact))
148
162
  end
149
163
 
150
164
  private
@@ -57,7 +57,8 @@ class TestToXML < Minitest::Test
57
57
  to = Factbase::ToXML.new(fb)
58
58
  xml = Nokogiri::XML.parse(to.xml)
59
59
  assert(!xml.xpath('/fb[@dob]').empty?)
60
- assert(!xml.xpath('/fb[@factbase_version]').empty?)
60
+ assert(!xml.xpath('/fb[@version]').empty?)
61
+ assert(!xml.xpath('/fb[@size]').empty?)
61
62
  end
62
63
 
63
64
  def test_to_xml_with_short_names
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factbase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.28
4
+ version: 0.0.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-19 00:00:00.000000000 Z
11
+ date: 2024-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json