factbase 0.0.30 → 0.0.32

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: 58a162c50ba7d2b9d21ca0ff2d62e34acd0c46d2666b596ceffcceb2dc4215c0
4
- data.tar.gz: 61514fa0fa904ea71540c0f9d4776ea75d5405aa5a1b73d32e051c29606b5fdb
3
+ metadata.gz: 53e563c36bf3d61f977bbae719a1b6069973a40b28f37ba4e3222f7fbae7091c
4
+ data.tar.gz: ecd5e6b589ceda68f8a25369e6d7c169500dab90ac6bfca26af5384d0e74bdc9
5
5
  SHA512:
6
- metadata.gz: 7b3841073dba72e3fcc6e1fd089235d93829264504447557b3b23f6eb3fa74a4ebccad0d27062af84dc00ab115ca06d9dcc9621bf245d54e09b121bb21983737
7
- data.tar.gz: aed653f93b7505db410e81d9fefd8fe9d38fc6b89947f89c952992bcdd76dab1d5f9e4a8cd941df426cb7b200855f26fb2981a9698ac35e8e6ea682c693787e7
6
+ metadata.gz: 7fc18cb91cd74af7e9e87b0740b10d1409278ce0dc8a7ea589ef8c45a5b61501dbe0706faf012a49862c911c25859d85a93ad1e6e275421af4c3255bcd5d1b2b
7
+ data.tar.gz: bf4d812092097c9edb40ccd4aeac447b96906bbdbb515696199f9fa229d8e620ba36b2c80435e1d75115c07e5f2bac6029215df0657d03d51ceb42d053f6652c
@@ -0,0 +1,57 @@
1
+ # Copyright (c) 2024 Yegor Bugayenko
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the 'Software'), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+ ---
21
+ name: license
22
+ 'on':
23
+ push:
24
+ branches:
25
+ - master
26
+ pull_request:
27
+ branches:
28
+ - master
29
+ jobs:
30
+ license:
31
+ runs-on: ubuntu-22.04
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - shell: bash
35
+ run: |
36
+ header="Copyright (c) $(date +%Y) Yegor Bugayenko"
37
+ failed="false"
38
+ while IFS= read -r file; do
39
+ if ! grep -q "${header}" "${file}"; then
40
+ failed="true"
41
+ echo "⚠️ Copyright header is not found in: ${file}"
42
+ else
43
+ echo "File looks good: ${file}"
44
+ fi
45
+ done < <(find . -type f \( \
46
+ -name "Dockerfile" -o \
47
+ -name "LICENSE.txt" -o \
48
+ -name "Makefile" -o \
49
+ -name "Rakefile" -o \
50
+ -name "*.sh" -o \
51
+ -name "*.rb" -o \
52
+ -name "*.fe" -o \
53
+ -name "*.yml" \
54
+ \) -print)
55
+ if [ "${failed}" = "true" ]; then
56
+ exit 1
57
+ fi
data/Gemfile CHANGED
@@ -23,10 +23,10 @@
23
23
  source 'https://rubygems.org'
24
24
  gemspec
25
25
 
26
- gem 'minitest', '5.23.0', require: false
26
+ gem 'minitest', '5.23.1', require: false
27
27
  gem 'rake', '13.2.1', require: false
28
28
  gem 'rspec-rails', '6.1.2', require: false
29
- gem 'rubocop', '1.63.5', require: false
29
+ gem 'rubocop', '1.64.0', require: false
30
30
  gem 'rubocop-performance', '1.21.0', require: false
31
31
  gem 'rubocop-rspec', '2.29.2', require: false
32
32
  gem 'simplecov', '0.22.0', require: false
data/Gemfile.lock CHANGED
@@ -59,7 +59,7 @@ GEM
59
59
  crass (~> 1.0.2)
60
60
  nokogiri (>= 1.12.0)
61
61
  loog (0.5.1)
62
- minitest (5.23.0)
62
+ minitest (5.23.1)
63
63
  mutex_m (0.2.0)
64
64
  nokogiri (1.16.5-arm64-darwin)
65
65
  racc (~> 1.4)
@@ -75,7 +75,7 @@ GEM
75
75
  racc
76
76
  psych (5.1.2)
77
77
  stringio
78
- racc (1.7.3)
78
+ racc (1.8.0)
79
79
  rack (3.0.11)
80
80
  rack-session (2.0.0)
81
81
  rack (>= 3.0.0)
@@ -125,7 +125,7 @@ GEM
125
125
  rspec-mocks (~> 3.13)
126
126
  rspec-support (~> 3.13)
127
127
  rspec-support (3.13.1)
128
- rubocop (1.63.5)
128
+ rubocop (1.64.0)
129
129
  json (~> 2.3)
130
130
  language_server-protocol (>= 3.17.0)
131
131
  parallel (~> 1.10)
@@ -181,10 +181,10 @@ PLATFORMS
181
181
 
182
182
  DEPENDENCIES
183
183
  factbase!
184
- minitest (= 5.23.0)
184
+ minitest (= 5.23.1)
185
185
  rake (= 13.2.1)
186
186
  rspec-rails (= 6.1.2)
187
- rubocop (= 1.63.5)
187
+ rubocop (= 1.64.0)
188
188
  rubocop-performance (= 1.21.0)
189
189
  rubocop-rspec (= 2.29.2)
190
190
  simplecov (= 0.22.0)
data/README.md CHANGED
@@ -14,7 +14,8 @@
14
14
  This Ruby gem manages an in-memory database of facts.
15
15
  A fact is simply a map of properties and values.
16
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.
17
+ It is possible to delete a fact, but impossible to delete a property
18
+ from a fact.
18
19
 
19
20
  **ATTENTION**: The current implemention is naive and,
20
21
  because of that, very slow. I will be very happy
@@ -55,8 +56,7 @@ assert(f2.query('(eq foo 42)').each.to_a.size == 1)
55
56
 
56
57
  All terms available in a query:
57
58
 
58
- * `()` is true
59
- * `(nil)` is false
59
+ * `(always)` and `(never)` are "true" and "false"
60
60
  * `(not t)` inverses the `t` if it's boolean (exception otherwise)
61
61
  * `(or t1 t2 ...)` returns true if at least one argument is true
62
62
  * `(and t1 t2 ...)` returns true if all arguments are true
@@ -73,12 +73,14 @@ All terms available in a query:
73
73
 
74
74
  There are also terms that match the entire factbase:
75
75
 
76
- * `(max k)` returns true if the value of `k` property is the largest in the entire factbase
76
+ * `(max k)` returns true if the value of `k` property
77
+ is the largest in the entire factbase
77
78
  * `(min k)` returns true if the value of `k` is the smallest
78
79
 
79
80
  ## How to contribute
80
81
 
81
- Read [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
82
+ Read
83
+ [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
82
84
  Make sure you build is green before you contribute
83
85
  your pull request. You will need to have
84
86
  [Ruby](https://www.ruby-lang.org/en/) 3.2+ and
@@ -39,17 +39,26 @@ class Factbase::Syntax
39
39
  # Convert it to a term.
40
40
  # @return [Term] The term detected
41
41
  def to_term
42
+ build.simplify
43
+ rescue StandardError => e
44
+ raise "#{e.message} in \"#{@query}\""
45
+ end
46
+
47
+ private
48
+
49
+ # Convert it to a term.
50
+ # @return [Term] The term detected
51
+ def build
42
52
  @tokens ||= to_tokens
53
+ raise 'No tokens' if @tokens.empty?
43
54
  @ast ||= to_ast(@tokens, 0)
44
- raise "Too many terms: #{@query}" if @ast[1] != @tokens.size
55
+ raise 'Too many terms' if @ast[1] != @tokens.size
45
56
  term = @ast[0]
46
- raise "No terms found: #{@query}" if term.nil?
47
- raise "Not a term: #{@query}" unless term.is_a?(Factbase::Term)
57
+ raise 'No terms found' if term.nil?
58
+ raise 'Not a term' unless term.is_a?(Factbase::Term)
48
59
  term
49
60
  end
50
61
 
51
- private
52
-
53
62
  # Reads the stream of tokens, starting at the +at+ position. If the
54
63
  # token at the position is not a literal (like 42 or "Hello") but a term,
55
64
  # the function recursively calls itself.
@@ -58,19 +67,19 @@ class Factbase::Syntax
58
67
  # is the term/literal and the second one is the position where the
59
68
  # scanning should continue.
60
69
  def to_ast(tokens, at)
61
- raise "Closing too soon at ##{at}: #{@query}" if tokens[at] == :close
70
+ raise "Closing too soon at ##{at}" if tokens[at] == :close
62
71
  return [tokens[at], at + 1] unless tokens[at] == :open
63
72
  at += 1
64
73
  op = tokens[at]
65
- return [Factbase::Term.new(:nil, []), at + 1] if op == :close
74
+ raise 'No token found' if op == :close
66
75
  operands = []
67
76
  at += 1
68
77
  loop do
69
- raise "End of token stream at ##{at}: #{@query}" if tokens[at].nil?
78
+ raise "End of token stream at ##{at}" if tokens[at].nil?
70
79
  break if tokens[at] == :close
71
80
  (operand, at1) = to_ast(tokens, at)
72
- raise "Stuck at position ##{at}: #{@query}" if at == at1
73
- raise "Jump back at position ##{at}: #{@query}" if at1 < at
81
+ raise "Stuck at position ##{at}" if at == at1
82
+ raise "Jump back at position ##{at}" if at1 < at
74
83
  at = at1
75
84
  operands << operand
76
85
  break if tokens[at] == :close
@@ -110,12 +119,12 @@ class Factbase::Syntax
110
119
  acc += c
111
120
  end
112
121
  end
113
- raise "String not closed: : #{@query}" if string
122
+ raise 'String not closed' if string
114
123
  list.map do |t|
115
124
  if t.is_a?(Symbol)
116
125
  t
117
126
  elsif t.start_with?('\'', '"')
118
- raise "String literal can't be empty: #{@query}" if t.length <= 2
127
+ raise 'String literal can\'t be empty' if t.length <= 2
119
128
  t[1..-2]
120
129
  elsif t.match?(/^[0-9]+$/)
121
130
  t.to_i
@@ -124,7 +133,7 @@ class Factbase::Syntax
124
133
  elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
125
134
  Time.parse(t)
126
135
  else
127
- raise "Wrong symbol format (#{t}): #{@query}" unless t.match?(/^[a-z][a-zA-Z0-9_]*$/)
136
+ raise "Wrong symbol format (#{t})" unless t.match?(/^[a-z][a-zA-Z0-9_]*$/)
128
137
  t.to_sym
129
138
  end
130
139
  end
data/lib/factbase/term.rb CHANGED
@@ -43,6 +43,8 @@ class Factbase::Term
43
43
  # @return [bool] TRUE if matches
44
44
  def evaluate(fact)
45
45
  send(@op, fact)
46
+ rescue NoMethodError => _e
47
+ raise "Term '#{@op}' is not defined"
46
48
  end
47
49
 
48
50
  # Put it into the context: let it see the entire array of maps.
@@ -57,6 +59,17 @@ class Factbase::Term
57
59
  self
58
60
  end
59
61
 
62
+ # Simplify it if possible.
63
+ # @return [Factbase::Term] New term or itself
64
+ def simplify
65
+ m = "#{@op}_simplify"
66
+ if respond_to?(m, true)
67
+ send(m)
68
+ else
69
+ self
70
+ end
71
+ end
72
+
60
73
  # Turns it into a string.
61
74
  # @return [String] The string of it
62
75
  def to_s
@@ -76,11 +89,16 @@ class Factbase::Term
76
89
 
77
90
  private
78
91
 
79
- def nil(_fact)
92
+ def always(_fact)
80
93
  assert_args(0)
81
94
  true
82
95
  end
83
96
 
97
+ def never(_fact)
98
+ assert_args(0)
99
+ false
100
+ end
101
+
84
102
  def not(fact)
85
103
  assert_args(1)
86
104
  !only_bool(the_value(0, fact))
@@ -100,6 +118,28 @@ class Factbase::Term
100
118
  true
101
119
  end
102
120
 
121
+ def and_or_simplify
122
+ strs = []
123
+ ops = []
124
+ @operands.each do |o|
125
+ o = o.simplify
126
+ s = o.to_s
127
+ next if strs.include?(s)
128
+ strs << s
129
+ ops << o
130
+ end
131
+ return ops[0] if ops.size == 1
132
+ Factbase::Term.new(@op, ops)
133
+ end
134
+
135
+ def and_simplify
136
+ and_or_simplify
137
+ end
138
+
139
+ def or_simplify
140
+ and_or_simplify
141
+ end
142
+
103
143
  def when(fact)
104
144
  assert_args(2)
105
145
  a = @operands[0]
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.30'
32
+ VERSION = '0.0.32'
33
33
 
34
34
  # Constructor.
35
35
  def initialize(facts = [])
@@ -42,7 +42,7 @@ class TestInv < Minitest::Test
42
42
  end
43
43
  f.c = 256
44
44
  assert_equal(42, f.a)
45
- assert_equal(1, fb.query('()').each.to_a.size)
45
+ assert_equal(1, fb.query('(always)').each.to_a.size)
46
46
  end
47
47
 
48
48
  def test_pre_and_inv
@@ -48,19 +48,19 @@ class TestLooged < Minitest::Test
48
48
  fb = Factbase.new
49
49
  fb.insert
50
50
  fb.insert
51
- assert_equal(2, Factbase::Looged.new(fb, Loog::NULL).query('()').each(&:to_s))
51
+ assert_equal(2, Factbase::Looged.new(fb, Loog::NULL).query('(always)').each(&:to_s))
52
52
  end
53
53
 
54
54
  def test_returns_int_when_empty
55
55
  fb = Factbase.new
56
- assert_equal(0, Factbase::Looged.new(fb, Loog::NULL).query('()').each(&:to_s))
56
+ assert_equal(0, Factbase::Looged.new(fb, Loog::NULL).query('(always)').each(&:to_s))
57
57
  end
58
58
 
59
59
  def test_logs_when_enumerator
60
60
  fb = Factbase::Looged.new(Factbase.new, Loog::NULL)
61
- assert_equal(0, fb.query('()').each.to_a.size)
61
+ assert_equal(0, fb.query('(always)').each.to_a.size)
62
62
  fb.insert
63
- assert_equal(1, fb.query('()').each.to_a.size)
63
+ assert_equal(1, fb.query('(always)').each.to_a.size)
64
64
  end
65
65
 
66
66
  def test_proper_logging
@@ -33,6 +33,6 @@ class TestPre < Minitest::Test
33
33
  fb = Factbase::Pre.new(Factbase.new) { |f| f.foo = 42 }
34
34
  f = fb.insert
35
35
  assert_equal(42, f.foo)
36
- assert_equal(1, fb.query('()').each.to_a.size)
36
+ assert_equal(1, fb.query('(always)').each.to_a.size)
37
37
  end
38
38
  end
@@ -90,6 +90,16 @@ class TestQuery < Minitest::Test
90
90
  assert_equal(1, maps.size)
91
91
  end
92
92
 
93
+ def test_deleting_nothing
94
+ maps = []
95
+ maps << { 'foo' => [42] }
96
+ maps << { 'bar' => [4, 5] }
97
+ maps << { 'bar' => 5 }
98
+ q = Factbase::Query.new(maps, Mutex.new, '(never)')
99
+ assert_equal(0, q.delete!)
100
+ assert_equal(3, maps.size)
101
+ end
102
+
93
103
  def test_to_array
94
104
  maps = []
95
105
  maps << { 'foo' => [42] }
@@ -41,13 +41,12 @@ class TestSyntax < Minitest::Test
41
41
 
42
42
  def test_simple_parsing
43
43
  [
44
- '()',
45
44
  '(foo)',
46
45
  '(foo (bar) (zz 77) )',
47
46
  "(eq foo \n\n 'Hello, world!'\n)\n",
48
47
  "(eq x 'Hello, \\' \n) \\' ( world!')",
49
48
  "# this is a comment\n(eq foo # test\n 42)\n\n# another comment\n",
50
- "(or ( a 4) (b 5) () (and () (c 5) \t\t(r 7 w8s w8is 'Foo')))"
49
+ "(or ( a 4) (b 5) (always) (and (always) (c 5) \t\t(r 7 w8s w8is 'Foo')))"
51
50
  ].each do |q|
52
51
  Factbase::Syntax.new(q).to_term
53
52
  end
@@ -67,7 +66,7 @@ class TestSyntax < Minitest::Test
67
66
  '(eq t 2024-05-25T19:43:48Z)',
68
67
  '(eq t 3.1415926)',
69
68
  '(eq t 3.0e+21)',
70
- "(foo (x (f (t (y 42 'Hey you'))) (f) (r 3)) y z)"
69
+ "(foo (x (f (t (y 42 'Hey you'))) (never) (r 3)) y z)"
71
70
  ].each do |q|
72
71
  assert_equal(q, Factbase::Syntax.new(q).to_term.to_s)
73
72
  end
@@ -91,6 +90,7 @@ class TestSyntax < Minitest::Test
91
90
  def test_broken_syntax
92
91
  [
93
92
  '',
93
+ '()',
94
94
  '(foo',
95
95
  '(foo 1) (bar 2)',
96
96
  'some text',
@@ -106,9 +106,21 @@ class TestSyntax < Minitest::Test
106
106
  ')',
107
107
  '"'
108
108
  ].each do |q|
109
- assert_raises(q) do
110
- Factbase::Syntax.new(q).to_term
111
- end
109
+ assert(
110
+ assert_raises(q) do
111
+ Factbase::Syntax.new(q).to_term
112
+ end.message.include?(q)
113
+ )
114
+ end
115
+ end
116
+
117
+ def test_simplification
118
+ {
119
+ '(foo)' => '(foo)',
120
+ '(and (foo) (foo))' => '(foo)',
121
+ '(and (foo) (or (and (eq a 1))) (eq a 1) (foo))' => '(and (foo) (eq a 1))'
122
+ }.each do |s, t|
123
+ assert_equal(t, Factbase::Syntax.new(s).to_term.to_s)
112
124
  end
113
125
  end
114
126
  end
@@ -82,8 +82,13 @@ class TestTerm < Minitest::Test
82
82
  assert(!t.evaluate(fact('bar' => [100])))
83
83
  end
84
84
 
85
+ def test_false_matching
86
+ t = Factbase::Term.new(:never, [])
87
+ assert(!t.evaluate(fact('foo' => [100])))
88
+ end
89
+
85
90
  def test_not_matching
86
- t = Factbase::Term.new(:not, [Factbase::Term.new(:nil, [])])
91
+ t = Factbase::Term.new(:not, [Factbase::Term.new(:always, [])])
87
92
  assert(!t.evaluate(fact('foo' => [100])))
88
93
  end
89
94
 
@@ -107,7 +107,7 @@ class TestFactbase < Minitest::Test
107
107
 
108
108
  def test_all_decorators
109
109
  [
110
- Factbase::Rules.new(Factbase.new, '()'),
110
+ Factbase::Rules.new(Factbase.new, '(always)'),
111
111
  Factbase::Inv.new(Factbase.new) { |_, _| true },
112
112
  Factbase::Pre.new(Factbase.new) { |_| true },
113
113
  Factbase::Looged.new(Factbase.new, Loog::NULL),
@@ -126,7 +126,7 @@ class TestFactbase < Minitest::Test
126
126
  end
127
127
  d.import(d.export)
128
128
  assert_equal(4, d.size)
129
- assert_equal(4, d.query('()').each.to_a.size)
129
+ assert_equal(4, d.query('(always)').each.to_a.size)
130
130
  end
131
131
  end
132
132
  end
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.30
4
+ version: 0.0.32
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-21 00:00:00.000000000 Z
11
+ date: 2024-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -78,6 +78,7 @@ files:
78
78
  - ".gitattributes"
79
79
  - ".github/workflows/actionlint.yml"
80
80
  - ".github/workflows/codecov.yml"
81
+ - ".github/workflows/license.yml"
81
82
  - ".github/workflows/markdown-lint.yml"
82
83
  - ".github/workflows/pdd.yml"
83
84
  - ".github/workflows/rake.yml"