factbase 0.4.0 → 0.5.1

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