factbase 0.0.13 → 0.0.15

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: 973358a29608045c0279b7c0840607a325d90f3d5dc77c90c733d1f3936723d4
4
- data.tar.gz: 49e0510527f0cb13bb8b5ea11528392897173ee665343c71bc6e368c32a75d35
3
+ metadata.gz: 8ea11eaa2c72f162214fffedf64db4674ef27aef158f613ecea3304e35d528c3
4
+ data.tar.gz: b2c11cab690d59314a866a668b18913df9856ff8dcd8e0fd1ff906dc6601bd41
5
5
  SHA512:
6
- metadata.gz: 47b1925a557068e94a59f6f7638a5196a5802a8df1ecfb99fb8fe87b40e2efda43c88c6939acf0851efde6c1f491e55d63175c7680cd96853f9390e52b1a90ee
7
- data.tar.gz: 61d88a0052bdd2c8ecdc7bad27a1161b481433cae0e7ad1cf32813a7842dc9e6db5ff8f6b7553a55e9abd93b9a9ac124c275bd639451a34c8a4491ca98b753c8
6
+ metadata.gz: d8a5a44265b0f9c6e32e669802831fec089e04049e2871167bbda0361ea99ad3b464f16cda94c5fd7b32b2cc539379b5e33acfc8e26c2488cfe605c61d3fe537
7
+ data.tar.gz: 010122d90a6564bc5d7041ed1d3107449062810e494504bc14bb6c64df71f0e7bbf077fc0300301e31d2078a3efc34ce8b5c0b722eefeb288befd465b139eb27
data/factbase.gemspec CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |s|
26
26
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
27
27
  s.required_ruby_version = '>=2.3'
28
28
  s.name = 'factbase'
29
- s.version = '0.0.13'
29
+ s.version = '0.0.15'
30
30
  s.license = 'MIT'
31
31
  s.summary = 'Factbase'
32
32
  s.description = 'Fact base in memory and on disc'
data/lib/factbase/fact.rb CHANGED
@@ -43,7 +43,9 @@ class Factbase::Fact
43
43
  kk = k[0..-2]
44
44
  raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-z][_a-zA-Z0-9]*$/)
45
45
  v = args[1]
46
+ raise "Prop value can't be nil" if v.nil?
46
47
  raise "Prop value can't be empty" if v == ''
48
+ raise "Prop type '#{v.class}' is not allowed" unless [String, Integer, Float, Time].include?(v.class)
47
49
  @mutex.synchronize do
48
50
  before = @map[kk]
49
51
  return if before == v
@@ -37,12 +37,36 @@ class Factbase::Query
37
37
 
38
38
  # Iterate them one by one.
39
39
  # @yield [Fact] Facts one-by-one
40
+ # @return [Integer] Total number of facts yielded
40
41
  def each
42
+ return to_enum(__method__) unless block_given?
41
43
  term = Factbase::Syntax.new(@query).to_term
44
+ yielded = 0
42
45
  @maps.each do |m|
43
46
  f = Factbase::Fact.new(@mutex, m)
44
47
  next unless term.matches?(f)
45
48
  yield f
49
+ yielded += 1
46
50
  end
51
+ yielded
52
+ end
53
+
54
+ # Delete all facts that match the query.
55
+ # @return [Integer] Total number of facts deleted
56
+ def delete!
57
+ term = Factbase::Syntax.new(@query).to_term
58
+ deleted = 0
59
+ @mutex.synchronize do
60
+ @maps.delete_if do |m|
61
+ f = Factbase::Fact.new(@mutex, m)
62
+ if term.matches?(f)
63
+ deleted += 1
64
+ true
65
+ else
66
+ false
67
+ end
68
+ end
69
+ end
70
+ deleted
47
71
  end
48
72
  end
@@ -114,6 +114,10 @@ class Factbase::Syntax
114
114
  t[1..-2]
115
115
  elsif t.match?(/^[0-9]+$/)
116
116
  t.to_i
117
+ elsif t.match?(/^[0-9]+\.[0-9]+(e\+[0-9]+)?$/)
118
+ t.to_f
119
+ elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
120
+ Time.parse(t)
117
121
  else
118
122
  t.to_sym
119
123
  end
data/lib/factbase/term.rb CHANGED
@@ -48,6 +48,8 @@ class Factbase::Term
48
48
  items += @operands.map do |o|
49
49
  if o.is_a?(String)
50
50
  "'#{o.gsub("'", "\\\\'").gsub('"', '\\\\"')}'"
51
+ elsif o.is_a?(Time)
52
+ o.utc.iso8601
51
53
  else
52
54
  o.to_s
53
55
  end
@@ -90,36 +92,33 @@ class Factbase::Term
90
92
  def absent(fact)
91
93
  assert_args(1)
92
94
  k = @operands[0].to_s
93
- fact[k].empty?
95
+ fact[k].nil?
94
96
  end
95
97
 
96
98
  def eq(fact)
97
- assert_args(2)
98
- k = @operands[0].to_s
99
- v = fact[k]
100
- return true if v == @operands[1]
101
- return v.include?(@operands[1]) if v.is_a?(Array)
102
- false
99
+ arithmetic(:==, fact)
103
100
  end
104
101
 
105
102
  def lt(fact)
106
- assert_args(2)
107
- k = @operands[0].to_s
108
- v = fact[k]
109
- return false if v.empty?
110
- v[0] < @operands[1]
103
+ arithmetic(:<, fact)
111
104
  end
112
105
 
113
106
  def gt(fact)
107
+ arithmetic(:>, fact)
108
+ end
109
+
110
+ def arithmetic(op, fact)
114
111
  assert_args(2)
115
112
  k = @operands[0].to_s
116
113
  v = fact[k]
117
114
  return false if v.nil?
118
- v[0] > @operands[1]
115
+ v = [v] unless v.is_a?(Array)
116
+ v.any? { |vv| vv.send(op, @operands[1]) }
119
117
  end
120
118
 
121
119
  def assert_args(num)
122
- raise "Too many operands for '#{@op}'" if @operands.size > num
123
- raise "Too few operands for '#{@op}'" if @operands.size < num
120
+ c = @operands.size
121
+ raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
122
+ raise "Too few (#{c}) operands for '#{@op}' (#{num} expected)" if c < num
124
123
  end
125
124
  end
@@ -46,6 +46,20 @@ class TestFact < Minitest::Test
46
46
  end
47
47
  end
48
48
 
49
+ def test_fails_when_setting_nil
50
+ f = Factbase::Fact.new(Mutex.new, {})
51
+ assert_raises do
52
+ f.foo = nil
53
+ end
54
+ end
55
+
56
+ def test_fails_when_setting_empty
57
+ f = Factbase::Fact.new(Mutex.new, {})
58
+ assert_raises do
59
+ f.foo = ''
60
+ end
61
+ end
62
+
49
63
  def test_fails_when_not_found
50
64
  f = Factbase::Fact.new(Mutex.new, {})
51
65
  f.first = 42
@@ -33,8 +33,48 @@ class TestQuery < Minitest::Test
33
33
  maps = []
34
34
  maps << { 'foo' => [42] }
35
35
  q = Factbase::Query.new(maps, Mutex.new, '(eq foo 42)')
36
- q.each do |f|
37
- assert_equal(42, f.foo)
36
+ assert_equal(
37
+ 1,
38
+ q.each do |f|
39
+ assert_equal(42, f.foo)
40
+ end
41
+ )
42
+ end
43
+
44
+ def test_complex_parsing
45
+ maps = []
46
+ maps << { 'num' => 42 }
47
+ maps << { 'pi' => 3.14, 'num' => [42, 66, 0] }
48
+ maps << { 'time' => Time.now - 100, 'num' => 0 }
49
+ {
50
+ '(eq num 444)' => 0,
51
+ '(eq time 0)' => 0,
52
+ '(gt num 60)' => 1,
53
+ '(and (lt pi 100) (gt num 1000))' => 0,
54
+ '(exists pi)' => 1,
55
+ '(not (exists hello))' => 3,
56
+ '(absent time)' => 2,
57
+ '(and (absent time) (exists pi))' => 1,
58
+ '(and (exists time) (not (exists pi)))' => 1,
59
+ "(or (eq num 66) (lt time #{(Time.now - 200).utc.iso8601}))" => 1
60
+ }.each do |q, r|
61
+ assert_equal(r, Factbase::Query.new(maps, Mutex.new, q).each.to_a.size, q)
38
62
  end
39
63
  end
64
+
65
+ def test_simple_deleting
66
+ maps = []
67
+ maps << { 'foo' => [42] }
68
+ maps << { 'bar' => [4, 5] }
69
+ maps << { 'bar' => 5 }
70
+ q = Factbase::Query.new(maps, Mutex.new, '(eq bar 5)')
71
+ assert_equal(2, q.delete!)
72
+ assert_equal(1, maps.size)
73
+ end
74
+
75
+ def test_to_array
76
+ maps = []
77
+ maps << { 'foo' => [42] }
78
+ assert_equal(1, Factbase::Query.new(maps, Mutex.new, '(eq foo 42)').each.to_a.size)
79
+ end
40
80
  end
@@ -61,6 +61,9 @@ class TestSyntax < Minitest::Test
61
61
  '(foo x y z)',
62
62
  "(foo x y z t f 42 'Hi!' 33)",
63
63
  '(foo (x) y z)',
64
+ '(eq t 2024-05-25T19:43:48Z)',
65
+ '(eq t 3.1415926)',
66
+ '(eq t 3.0e+21)',
64
67
  "(foo (x (f (t (y 42 'Hey you'))) (f) (r 3)) y z)"
65
68
  ].each do |q|
66
69
  assert_equal(q, Factbase::Syntax.new(q).to_term.to_s)
@@ -41,18 +41,38 @@ class TestTerm < Minitest::Test
41
41
  assert(t.matches?(fact('foo' => [10, 5, 6, -8, 'hey', 42, 9, 'fdsf'])))
42
42
  assert(!t.matches?(fact('foo' => [100])))
43
43
  assert(!t.matches?(fact('foo' => [])))
44
+ assert(!t.matches?(fact('bar' => [])))
44
45
  end
45
46
 
46
47
  def test_lt_matching
47
48
  t = Factbase::Term.new(:lt, ['foo', 42])
48
49
  assert(t.matches?(fact('foo' => [10])))
49
50
  assert(!t.matches?(fact('foo' => [100])))
51
+ assert(!t.matches?(fact('foo' => 100)))
52
+ assert(!t.matches?(fact('bar' => 100)))
50
53
  end
51
54
 
52
55
  def test_gt_matching
53
56
  t = Factbase::Term.new(:gt, ['foo', 42])
54
57
  assert(t.matches?(fact('foo' => [100])))
58
+ assert(t.matches?(fact('foo' => 100)))
55
59
  assert(!t.matches?(fact('foo' => [10])))
60
+ assert(!t.matches?(fact('foo' => 10)))
61
+ assert(!t.matches?(fact('bar' => 10)))
62
+ end
63
+
64
+ def test_lt_matching_time
65
+ t = Factbase::Term.new(:lt, ['foo', Time.now])
66
+ assert(t.matches?(fact('foo' => [Time.now - 100])))
67
+ assert(!t.matches?(fact('foo' => [Time.now + 100])))
68
+ assert(!t.matches?(fact('bar' => [100])))
69
+ end
70
+
71
+ def test_gt_matching_time
72
+ t = Factbase::Term.new(:gt, ['foo', Time.now])
73
+ assert(t.matches?(fact('foo' => [Time.now + 100])))
74
+ assert(!t.matches?(fact('foo' => [Time.now - 100])))
75
+ assert(!t.matches?(fact('bar' => [100])))
56
76
  end
57
77
 
58
78
  def test_not_matching
@@ -60,12 +80,24 @@ class TestTerm < Minitest::Test
60
80
  assert(!t.matches?(fact('foo' => [100])))
61
81
  end
62
82
 
63
- def test_not_exists_matching
83
+ def test_not_eq_matching
64
84
  t = Factbase::Term.new(:not, [Factbase::Term.new(:eq, ['foo', 100])])
65
85
  assert(t.matches?(fact('foo' => [42, 12, -90])))
66
86
  assert(!t.matches?(fact('foo' => 100)))
67
87
  end
68
88
 
89
+ def test_exists_matching
90
+ t = Factbase::Term.new(:exists, ['foo'])
91
+ assert(t.matches?(fact('foo' => [42, 12, -90])))
92
+ assert(!t.matches?(fact('bar' => 100)))
93
+ end
94
+
95
+ def test_absent_matching
96
+ t = Factbase::Term.new(:absent, ['foo'])
97
+ assert(t.matches?(fact('z' => [42, 12, -90])))
98
+ assert(!t.matches?(fact('foo' => 100)))
99
+ end
100
+
69
101
  def test_or_matching
70
102
  t = Factbase::Term.new(
71
103
  :or,
@@ -54,7 +54,7 @@ class TestFactbase < Minitest::Test
54
54
  File.write(f.path, f1.export)
55
55
  f2.import(File.read(f.path))
56
56
  end
57
- assert_equal(1, f2.query('(eq foo 42)').extend(Enumerable).to_a.count)
57
+ assert_equal(1, f2.query('(eq foo 42)').each.to_a.count)
58
58
  end
59
59
 
60
60
  def test_to_json
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.13
4
+ version: 0.0.15
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-13 00:00:00.000000000 Z
11
+ date: 2024-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json