factbase 0.0.13 → 0.0.15

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: 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