factbase 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.0pdd.yml +1 -1
  3. data/.github/workflows/actionlint.yml +1 -1
  4. data/.github/workflows/benchmark.yml +64 -0
  5. data/.github/workflows/codecov.yml +4 -2
  6. data/.github/workflows/copyrights.yml +2 -2
  7. data/.github/workflows/markdown-lint.yml +1 -1
  8. data/.github/workflows/pdd.yml +1 -1
  9. data/.github/workflows/rake.yml +2 -2
  10. data/.github/workflows/xcop.yml +1 -1
  11. data/.github/workflows/yamllint.yml +1 -1
  12. data/.gitignore +1 -1
  13. data/.rubocop.yml +6 -1
  14. data/.rultor.yml +2 -2
  15. data/.simplecov +1 -1
  16. data/.yamllint.yml +9 -4
  17. data/Gemfile +9 -7
  18. data/Gemfile.lock +98 -78
  19. data/LICENSE.txt +1 -1
  20. data/README.md +33 -0
  21. data/Rakefile +6 -1
  22. data/benchmarks/simple.rb +96 -0
  23. data/factbase.gemspec +1 -1
  24. data/lib/factbase/accum.rb +2 -2
  25. data/lib/factbase/fact.rb +6 -4
  26. data/lib/factbase/flatten.rb +2 -2
  27. data/lib/factbase/inv.rb +2 -2
  28. data/lib/factbase/looged.rb +6 -5
  29. data/lib/factbase/pre.rb +2 -2
  30. data/lib/factbase/query.rb +16 -8
  31. data/lib/factbase/query_once.rb +71 -0
  32. data/lib/factbase/rules.rb +3 -3
  33. data/lib/factbase/syntax.rb +21 -11
  34. data/lib/factbase/tee.rb +2 -2
  35. data/lib/factbase/term.rb +33 -5
  36. data/lib/factbase/term_once.rb +84 -0
  37. data/lib/factbase/terms/aggregates.rb +4 -4
  38. data/lib/factbase/terms/aliases.rb +5 -5
  39. data/lib/factbase/terms/casting.rb +2 -2
  40. data/lib/factbase/terms/debug.rb +2 -2
  41. data/lib/factbase/terms/defn.rb +2 -2
  42. data/lib/factbase/terms/logical.rb +3 -3
  43. data/lib/factbase/terms/math.rb +2 -2
  44. data/lib/factbase/terms/meta.rb +2 -2
  45. data/lib/factbase/terms/ordering.rb +2 -2
  46. data/lib/factbase/terms/strings.rb +2 -2
  47. data/lib/factbase/terms/system.rb +2 -2
  48. data/lib/factbase/to_json.rb +2 -2
  49. data/lib/factbase/to_xml.rb +2 -2
  50. data/lib/factbase/to_yaml.rb +2 -2
  51. data/lib/factbase.rb +16 -6
  52. data/test/factbase/terms/test_aggregates.rb +6 -6
  53. data/test/factbase/terms/test_aliases.rb +6 -6
  54. data/test/factbase/terms/test_casting.rb +6 -6
  55. data/test/factbase/terms/test_debug.rb +53 -0
  56. data/test/factbase/terms/test_defn.rb +15 -15
  57. data/test/factbase/terms/test_logical.rb +15 -13
  58. data/test/factbase/terms/test_math.rb +42 -42
  59. data/test/factbase/terms/test_meta.rb +87 -0
  60. data/test/factbase/terms/test_ordering.rb +45 -0
  61. data/test/factbase/terms/test_strings.rb +8 -8
  62. data/test/factbase/terms/test_system.rb +6 -6
  63. data/test/factbase/test_accum.rb +9 -9
  64. data/test/factbase/test_fact.rb +22 -22
  65. data/test/factbase/test_flatten.rb +6 -6
  66. data/test/factbase/test_inv.rb +3 -3
  67. data/test/factbase/test_looged.rb +13 -13
  68. data/test/factbase/test_pre.rb +2 -2
  69. data/test/factbase/test_query.rb +17 -17
  70. data/test/factbase/test_rules.rb +8 -8
  71. data/test/factbase/test_syntax.rb +30 -9
  72. data/test/factbase/test_tee.rb +11 -11
  73. data/test/factbase/test_term.rb +18 -18
  74. data/test/factbase/test_to_json.rb +4 -4
  75. data/test/factbase/test_to_xml.rb +12 -14
  76. data/test/factbase/test_to_yaml.rb +3 -3
  77. data/test/test__helper.rb +2 -2
  78. data/test/test_factbase.rb +200 -18
  79. metadata +12 -5
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright (c) 2024-2025 Yegor Bugayenko
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the 'Software'), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+
24
+ require 'benchmark'
25
+ require 'time'
26
+ require_relative '../lib/factbase'
27
+
28
+ def insert(fb, total)
29
+ time =
30
+ Benchmark.measure do
31
+ total.times do |i|
32
+ fact = fb.insert
33
+ fact.id = i
34
+ fact.title = "Object Thinking #{i}"
35
+ fact.time = Time.now.iso8601
36
+ fact.cost = rand(1..100)
37
+ fact.foo = rand(0.0..100.0).round(3)
38
+ fact.bar = rand(100..300)
39
+ fact.seenBy = "User#{i}" if i.even?
40
+ fact.zzz = "Extra#{i}" if (i % 10).zero?
41
+ end
42
+ end
43
+ {
44
+ title: '`fb.insert()`',
45
+ time: time.real,
46
+ details: "Inserted #{total} facts"
47
+ }
48
+ end
49
+
50
+ def query(fb, query)
51
+ total = 0
52
+ runs = 10
53
+ time =
54
+ Benchmark.measure do
55
+ runs.times do
56
+ total = fb.query(query).each.to_a.size
57
+ end
58
+ end
59
+ {
60
+ title: "`#{query}`",
61
+ time: (time.real / runs).round(6),
62
+ details: "Found #{total} fact(s)"
63
+ }
64
+ end
65
+
66
+ def impex(fb)
67
+ size = 0
68
+ time =
69
+ Benchmark.measure do
70
+ bin = fb.export
71
+ size = bin.size
72
+ fb2 = Factbase.new
73
+ fb2.import(bin)
74
+ end
75
+ {
76
+ title: '`.export()` + `.import()`',
77
+ time: time.real,
78
+ details: "#{size} bytes"
79
+ }
80
+ end
81
+
82
+ fb = Factbase.new
83
+ rows = [
84
+ insert(fb, 100_000),
85
+ query(fb, '(gt time \'2024-03-23T03:21:43Z\')'),
86
+ query(fb, '(gt cost 50)'),
87
+ query(fb, '(eq title \'Object Thinking 5000\')'),
88
+ query(fb, '(and (eq foo 42.998) (or (gt bar 200) (absent zzz)))'),
89
+ query(fb, '(eq id (agg (always) (max id)))'),
90
+ query(fb, '(join "c<=cost,b<=bar" (eq id (agg (always) (max id))))'),
91
+ impex(fb)
92
+ ].map { |r| "| #{r[:title]} | #{format('%0.3f', r[:time])} | #{r[:details]} |" }
93
+
94
+ puts '| Action | Seconds | Details |'
95
+ puts '| --- | --: | --- |'
96
+ rows.each { |row| puts row }
data/factbase.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -26,7 +26,7 @@ require_relative '../factbase'
26
26
  # Accumulator of props, a decorator of +Factbase::Fact+.
27
27
  #
28
28
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
29
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
29
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
30
30
  # License:: MIT
31
31
  class Factbase::Accum
32
32
  # Ctor.
data/lib/factbase/fact.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -33,20 +33,21 @@ require_relative '../factbase'
33
33
  # fact with a single key/value pair inside:
34
34
  #
35
35
  # require 'factbase/fact'
36
- # f = Factbase::Fact.new(Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
36
+ # f = Factbase::Fact.new(Factbase.new, Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
37
37
  # assert_equal(42, f.foo)
38
38
  #
39
39
  # A fact is basically a key/value hash map, where values are non-empty
40
40
  # sets of values.
41
41
  #
42
42
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
43
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
43
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
44
44
  # License:: MIT
45
45
  class Factbase::Fact
46
46
  # Ctor.
47
47
  # @param [Mutex] mutex A mutex to use for maps synchronization
48
48
  # @param [Hash] map A map of key/value pairs
49
- def initialize(mutex, map)
49
+ def initialize(fb, mutex, map)
50
+ @fb = fb
50
51
  @mutex = mutex
51
52
  @map = map
52
53
  end
@@ -80,6 +81,7 @@ class Factbase::Fact
80
81
  @map[kk] << v
81
82
  @map[kk].uniq!
82
83
  end
84
+ @fb.cache.clear
83
85
  nil
84
86
  elsif k == '[]'
85
87
  @map[args[1].to_s]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -25,7 +25,7 @@ require_relative '../factbase'
25
25
  # Make maps suitable for printing.
26
26
  #
27
27
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
28
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
28
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
29
29
  # License:: MIT
30
30
  class Factbase::Flatten
31
31
  # Constructor.
data/lib/factbase/inv.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -26,7 +26,7 @@ require_relative '../factbase'
26
26
 
27
27
  # A decorator of a Factbase, that checks invariants on every set.
28
28
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
29
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
29
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
30
30
  # License:: MIT
31
31
  class Factbase::Inv
32
32
  decoor(:fb)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -29,7 +29,7 @@ require_relative 'syntax'
29
29
 
30
30
  # A decorator of a Factbase, that logs all operations.
31
31
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
32
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
32
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
33
33
  # License:: MIT
34
34
  class Factbase::Looged
35
35
  def initialize(fb, loog)
@@ -46,8 +46,9 @@ class Factbase::Looged
46
46
  end
47
47
 
48
48
  def insert
49
+ start = Time.now
49
50
  f = @fb.insert
50
- @loog.debug("Inserted new fact ##{@fb.size}")
51
+ @loog.debug("Inserted new fact ##{@fb.size} in #{start.ago}")
51
52
  Fact.new(f, @loog)
52
53
  end
53
54
 
@@ -119,7 +120,7 @@ class Factbase::Looged
119
120
  end
120
121
 
121
122
  def one(params = {})
122
- q = Factbase::Syntax.new(@expr).to_term.to_s
123
+ q = Factbase::Syntax.new(@fb, @expr).to_term.to_s
123
124
  r = nil
124
125
  tail =
125
126
  Factbase::Looged.elapsed do
@@ -134,7 +135,7 @@ class Factbase::Looged
134
135
  end
135
136
 
136
137
  def each(params = {}, &)
137
- q = Factbase::Syntax.new(@expr).to_term.to_s
138
+ q = Factbase::Syntax.new(@fb, @expr).to_term.to_s
138
139
  if block_given?
139
140
  r = nil
140
141
  tail =
data/lib/factbase/pre.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -37,7 +37,7 @@ require_relative '../factbase'
37
37
  # one is the fact just inserted.
38
38
  #
39
39
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
40
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
40
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
41
41
  # License:: MIT
42
42
  class Factbase::Pre
43
43
  decoor(:fb)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -34,30 +34,38 @@ require_relative 'tee'
34
34
  # It is NOT thread-safe!
35
35
  #
36
36
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
37
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
37
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
38
38
  # License:: MIT
39
39
  class Factbase::Query
40
40
  # Constructor.
41
+ # @param [Factbase] fb Factbase
41
42
  # @param [Array<Fact>] maps Array of facts to start with
42
43
  # @param [Mutex] mutex Mutex to sync all modifications to the +maps+
43
44
  # @param [String] query The query as a string
44
- def initialize(maps, mutex, query)
45
+ def initialize(fb, maps, mutex, query)
46
+ @fb = fb
45
47
  @maps = maps
46
48
  @mutex = mutex
47
49
  @query = query
48
50
  end
49
51
 
52
+ # Print it as a string.
53
+ # @return [String] The query as a string
54
+ def to_s
55
+ @query.to_s
56
+ end
57
+
50
58
  # Iterate facts one by one.
51
59
  # @param [Hash] params Optional params accessible in the query via the "$" symbol
52
60
  # @yield [Fact] Facts one-by-one
53
61
  # @return [Integer] Total number of facts yielded
54
62
  def each(params = {})
55
63
  return to_enum(__method__, params) unless block_given?
56
- term = Factbase::Syntax.new(@query).to_term
64
+ term = Factbase::Syntax.new(@fb, @query).to_term
57
65
  yielded = 0
58
66
  @maps.each do |m|
59
67
  extras = {}
60
- f = Factbase::Fact.new(@mutex, m)
68
+ f = Factbase::Fact.new(@fb, @mutex, m)
61
69
  params = params.transform_keys(&:to_s) if params.is_a?(Hash)
62
70
  f = Factbase::Tee.new(f, params)
63
71
  a = Factbase::Accum.new(f, extras, false)
@@ -76,7 +84,7 @@ class Factbase::Query
76
84
  # @param [Hash] params Optional params accessible in the query via the "$" symbol
77
85
  # @return The value evaluated
78
86
  def one(params = {})
79
- term = Factbase::Syntax.new(@query).to_term
87
+ term = Factbase::Syntax.new(@fb, @query).to_term
80
88
  params = params.transform_keys(&:to_s) if params.is_a?(Hash)
81
89
  r = term.evaluate(Factbase::Tee.new(nil, params), @maps)
82
90
  unless %w[String Integer Float Time Array NilClass].include?(r.class.to_s)
@@ -88,11 +96,11 @@ class Factbase::Query
88
96
  # Delete all facts that match the query.
89
97
  # @return [Integer] Total number of facts deleted
90
98
  def delete!
91
- term = Factbase::Syntax.new(@query).to_term
99
+ term = Factbase::Syntax.new(@fb, @query).to_term
92
100
  deleted = 0
93
101
  @mutex.synchronize do
94
102
  @maps.delete_if do |m|
95
- f = Factbase::Fact.new(@mutex, m)
103
+ f = Factbase::Fact.new(@fb, @mutex, m)
96
104
  if term.evaluate(f, @maps)
97
105
  deleted += 1
98
106
  true
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2024-2025 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
+ # Query with a cache, a decorator of another query.
26
+ #
27
+ # It is NOT thread-safe!
28
+ #
29
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
30
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
31
+ # License:: MIT
32
+ class Factbase::QueryOnce
33
+ # Constructor.
34
+ # @param [Factbase] fb Factbase
35
+ # @param [Factbase::Query] query Original query
36
+ # @param [Array<Hash>] maps Where to search
37
+ def initialize(fb, query, maps)
38
+ @fb = fb
39
+ @query = query
40
+ @maps = maps
41
+ end
42
+
43
+ # Iterate facts one by one.
44
+ # @param [Hash] params Optional params accessible in the query via the "$" symbol
45
+ # @yield [Fact] Facts one-by-one
46
+ # @return [Integer] Total number of facts yielded
47
+ def each(params = {}, &)
48
+ unless block_given?
49
+ return to_enum(__method__, params) if Factbase::Syntax.new(@fb, @query).to_term.abstract?
50
+ key = [@query.to_s, @maps.object_id]
51
+ before = @fb.cache[key]
52
+ @fb.cache[key] = to_enum(__method__, params).to_a if before.nil?
53
+ return @fb.cache[key]
54
+ end
55
+ @query.each(params, &)
56
+ end
57
+
58
+ # Read a single value.
59
+ # @param [Hash] params Optional params accessible in the query via the "$" symbol
60
+ # @return The value evaluated
61
+ def one(params = {})
62
+ @query.one(params)
63
+ end
64
+
65
+ # Delete all facts that match the query.
66
+ # @return [Integer] Total number of facts deleted
67
+ def delete!
68
+ @fb.cache.clear
69
+ @query.delete!
70
+ end
71
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -38,7 +38,7 @@ require_relative '../factbase/syntax'
38
38
  # end # Runtime exception here (transaction won't commit)
39
39
  #
40
40
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
41
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
41
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
42
42
  # License:: MIT
43
43
  class Factbase::Rules
44
44
  decoor(:fb)
@@ -134,7 +134,7 @@ class Factbase::Rules
134
134
  end
135
135
 
136
136
  def it(fact)
137
- return if Factbase::Syntax.new(@expr).to_term.evaluate(fact, [])
137
+ return if Factbase::Syntax.new(Factbase.new, @expr).to_term.evaluate(fact, [])
138
138
  e = "#{@expr[0..32]}..." if @expr.length > 32
139
139
  raise "The fact doesn't match the #{e.inspect} rule: #{fact}"
140
140
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -24,6 +24,7 @@ require 'time'
24
24
  require_relative '../factbase'
25
25
  require_relative 'fact'
26
26
  require_relative 'term'
27
+ require_relative 'term_once'
27
28
 
28
29
  # Syntax.
29
30
  #
@@ -34,19 +35,26 @@ require_relative 'term'
34
35
  # every term it meets in the query:
35
36
  #
36
37
  # require 'factbase/syntax'
37
- # t = Factbase::Syntax.new('(hello world)', MyTerm).to_term
38
+ # t = Factbase::Syntax.new(Factbase.new, '(hello world)', MyTerm).to_term
38
39
  #
39
40
  # The +MyTerm+ class should have a constructor with two arguments:
40
41
  # the operator and the list of operands (Array).
41
42
  #
42
43
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
43
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
44
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
44
45
  # License:: MIT
45
46
  class Factbase::Syntax
46
47
  # Ctor.
48
+ #
49
+ # The class provided as the +term+ argument must have a three-argument
50
+ # constructor, similar to the class +Factbase::Term+. Also, it must be
51
+ # a child of +Factbase::Term+.
52
+ #
53
+ # @param [Factbase] fb Factbase
47
54
  # @param [String] query The query, for example "(eq id 42)"
48
55
  # @param [Class] term The class to instantiate to make every term
49
- def initialize(query, term: Factbase::Term)
56
+ def initialize(fb, query, term: Factbase::Term)
57
+ @fb = fb
50
58
  @query = query
51
59
  @term = term
52
60
  end
@@ -61,7 +69,7 @@ class Factbase::Syntax
61
69
  t
62
70
  end
63
71
  rescue StandardError => e
64
- err = "#{e.message} in \"#{@query}\""
72
+ err = "#{e.message} (#{e.backtrace.take(5).join('; ')}) in \"#{@query}\""
65
73
  err = "#{err}, tokens: #{@tokens}" unless @tokens.nil?
66
74
  raise err
67
75
  end
@@ -74,10 +82,10 @@ class Factbase::Syntax
74
82
  @tokens ||= to_tokens
75
83
  raise 'No tokens' if @tokens.empty?
76
84
  @ast ||= to_ast(@tokens, 0)
77
- raise 'Too many terms' if @ast[1] != @tokens.size
85
+ raise "Too many terms (#{@ast[1]} != #{@tokens.size})" if @ast[1] != @tokens.size
78
86
  term = @ast[0]
79
87
  raise 'No terms found' if term.nil?
80
- raise 'Not a term' unless term.is_a?(@term)
88
+ raise "Not a term: #{@term.class.name.inspect}" unless term.is_a?(@term)
81
89
  term
82
90
  end
83
91
 
@@ -106,20 +114,22 @@ class Factbase::Syntax
106
114
  operands << operand
107
115
  break if tokens[at] == :close
108
116
  end
109
- [@term.new(op, operands), at + 1]
117
+ [Factbase::TermOnce.new(@term.new(@fb, op, operands), @fb.cache), at + 1]
110
118
  end
111
119
 
112
120
  # Turns a query into an array of tokens.
113
121
  def to_tokens
114
122
  list = []
115
123
  acc = ''
124
+ quotes = ['\'', '"']
125
+ spaces = [' ', ')']
116
126
  string = false
117
127
  comment = false
118
128
  @query.to_s.chars.each do |c|
119
129
  comment = true if !string && c == '#'
120
130
  comment = false if comment && c == "\n"
121
131
  next if comment
122
- if ['\'', '"'].include?(c)
132
+ if quotes.include?(c)
123
133
  if string && acc[acc.length - 1] == '\\'
124
134
  acc = acc[0..-2]
125
135
  else
@@ -130,7 +140,7 @@ class Factbase::Syntax
130
140
  acc += c
131
141
  next
132
142
  end
133
- if !acc.empty? && [' ', ')'].include?(c)
143
+ if !acc.empty? && spaces.include?(c)
134
144
  list << acc
135
145
  acc = ''
136
146
  end
@@ -159,7 +169,7 @@ class Factbase::Syntax
159
169
  elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
160
170
  Time.parse(t)
161
171
  else
162
- raise "Wrong symbol format (#{t})" unless t.match?(/^[_a-z\$][a-zA-Z0-9_]*$/)
172
+ raise "Wrong symbol format (#{t})" unless t.match?(/^([_a-z][a-zA-Z0-9_]*|\$[a-z]+)$/)
163
173
  t.to_sym
164
174
  end
165
175
  end
data/lib/factbase/tee.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -26,7 +26,7 @@ require_relative '../factbase'
26
26
  # Tee of two facts.
27
27
  #
28
28
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
29
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
29
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
30
30
  # License:: MIT
31
31
  class Factbase::Tee
32
32
  # Ctor.
data/lib/factbase/term.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2024 Yegor Bugayenko
3
+ # Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the 'Software'), to deal
@@ -34,8 +34,8 @@ require_relative 'tee'
34
34
  #
35
35
  # require 'factbase/fact'
36
36
  # require 'factbase/term'
37
- # f = Factbase::Fact.new(Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
38
- # t = Factbase::Term.new(:lt, [:foo, 50])
37
+ # f = Factbase::Fact.new(Factbase.new, Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
38
+ # t = Factbase::Term.new(Factbase.new, :lt, [:foo, 50])
39
39
  # assert(t.evaluate(f))
40
40
  #
41
41
  # The design of this class may look ugly, since it has a large number of
@@ -48,7 +48,7 @@ require_relative 'tee'
48
48
  # and currently we implement most of them.
49
49
  #
50
50
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
51
- # Copyright:: Copyright (c) 2024 Yegor Bugayenko
51
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
52
52
  # License:: MIT
53
53
  class Factbase::Term
54
54
  attr_reader :op, :operands
@@ -87,9 +87,11 @@ class Factbase::Term
87
87
  include Factbase::Term::Debug
88
88
 
89
89
  # Ctor.
90
+ # @param [Factbase] fb Factbase
90
91
  # @param [Symbol] operator Operator
91
92
  # @param [Array] operands Operands
92
- def initialize(operator, operands)
93
+ def initialize(fb, operator, operands)
94
+ @fb = fb
93
95
  @op = operator
94
96
  @operands = operands
95
97
  end
@@ -117,6 +119,32 @@ class Factbase::Term
117
119
  end
118
120
  end
119
121
 
122
+ # Does it have any dependencies on a fact?
123
+ #
124
+ # If a term is static, it will return the same value for +evaluate+,
125
+ # no matter what is the fact given.
126
+ #
127
+ # @return [Boolean] TRUE if static
128
+ def static?
129
+ return true if @op == :agg
130
+ @operands.each do |o|
131
+ return false if o.is_a?(Factbase::Term) && !o.static?
132
+ return false if o.is_a?(Symbol) && !o.to_s.start_with?('$')
133
+ end
134
+ true
135
+ end
136
+
137
+ # Does it have any variables (+$foo+, for example) inside?
138
+ #
139
+ # @return [Boolean] TRUE if abstract
140
+ def abstract?
141
+ @operands.each do |o|
142
+ return true if o.is_a?(Factbase::Term) && o.abstract?
143
+ return true if o.is_a?(Symbol) && o.to_s.start_with?('$')
144
+ end
145
+ false
146
+ end
147
+
120
148
  # Turns it into a string.
121
149
  # @return [String] The string of it
122
150
  def to_s