factbase 0.19.10 → 0.19.12

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -4
  3. data/Gemfile.lock +18 -14
  4. data/README.md +127 -20
  5. data/Rakefile +2 -7
  6. data/factbase.gemspec +11 -11
  7. data/lib/factbase/accum.rb +1 -1
  8. data/lib/factbase/cached/cached_fact.rb +1 -2
  9. data/lib/factbase/cached/cached_factbase.rb +3 -3
  10. data/lib/factbase/cached/cached_query.rb +4 -6
  11. data/lib/factbase/cached/cached_term.rb +1 -2
  12. data/lib/factbase/churn.rb +4 -8
  13. data/lib/factbase/fact.rb +12 -9
  14. data/lib/factbase/flatten.rb +2 -2
  15. data/lib/factbase/impatient.rb +14 -13
  16. data/lib/factbase/indexed/indexed_and.rb +14 -20
  17. data/lib/factbase/indexed/indexed_eq.rb +5 -1
  18. data/lib/factbase/indexed/indexed_fact.rb +1 -4
  19. data/lib/factbase/indexed/indexed_factbase.rb +4 -4
  20. data/lib/factbase/indexed/indexed_gt.rb +3 -1
  21. data/lib/factbase/indexed/indexed_gte.rb +51 -0
  22. data/lib/factbase/indexed/indexed_lt.rb +3 -1
  23. data/lib/factbase/indexed/indexed_lte.rb +51 -0
  24. data/lib/factbase/indexed/indexed_not.rb +1 -1
  25. data/lib/factbase/indexed/indexed_or.rb +2 -2
  26. data/lib/factbase/indexed/indexed_query.rb +6 -7
  27. data/lib/factbase/indexed/indexed_term.rb +10 -6
  28. data/lib/factbase/indexed/indexed_unique.rb +4 -2
  29. data/lib/factbase/inv.rb +3 -3
  30. data/lib/factbase/lazy_taped.rb +10 -13
  31. data/lib/factbase/lazy_taped_hash.rb +2 -1
  32. data/lib/factbase/light.rb +1 -1
  33. data/lib/factbase/logged.rb +37 -34
  34. data/lib/factbase/pre.rb +3 -3
  35. data/lib/factbase/query.rb +4 -5
  36. data/lib/factbase/rules.rb +8 -8
  37. data/lib/factbase/sync/sync_factbase.rb +2 -2
  38. data/lib/factbase/syntax.rb +19 -20
  39. data/lib/factbase/tallied.rb +7 -8
  40. data/lib/factbase/taped.rb +5 -11
  41. data/lib/factbase/tee.rb +2 -2
  42. data/lib/factbase/term.rb +58 -59
  43. data/lib/factbase/terms/agg.rb +3 -4
  44. data/lib/factbase/terms/arithmetic.rb +7 -7
  45. data/lib/factbase/terms/as.rb +2 -2
  46. data/lib/factbase/terms/assert.rb +5 -13
  47. data/lib/factbase/terms/base.rb +7 -10
  48. data/lib/factbase/terms/best.rb +1 -1
  49. data/lib/factbase/terms/boolean.rb +1 -1
  50. data/lib/factbase/terms/compare.rb +17 -1
  51. data/lib/factbase/terms/contains.rb +28 -0
  52. data/lib/factbase/terms/defn.rb +8 -6
  53. data/lib/factbase/terms/empty.rb +1 -1
  54. data/lib/factbase/terms/ends_with.rb +27 -0
  55. data/lib/factbase/terms/first.rb +2 -2
  56. data/lib/factbase/terms/head.rb +3 -3
  57. data/lib/factbase/terms/inverted.rb +2 -2
  58. data/lib/factbase/terms/join.rb +8 -7
  59. data/lib/factbase/terms/matches.rb +14 -4
  60. data/lib/factbase/terms/max.rb +1 -1
  61. data/lib/factbase/terms/min.rb +1 -1
  62. data/lib/factbase/terms/nth.rb +3 -3
  63. data/lib/factbase/terms/plus.rb +1 -1
  64. data/lib/factbase/terms/prev.rb +3 -6
  65. data/lib/factbase/terms/sorted.rb +2 -2
  66. data/lib/factbase/terms/sprintf.rb +11 -2
  67. data/lib/factbase/terms/starts_with.rb +27 -0
  68. data/lib/factbase/terms/sum.rb +2 -2
  69. data/lib/factbase/terms/to_float.rb +2 -2
  70. data/lib/factbase/terms/to_integer.rb +2 -2
  71. data/lib/factbase/terms/to_string.rb +1 -1
  72. data/lib/factbase/terms/to_time.rb +10 -2
  73. data/lib/factbase/terms/traced.rb +2 -2
  74. data/lib/factbase/terms/undef.rb +2 -2
  75. data/lib/factbase/terms/unique.rb +3 -7
  76. data/lib/factbase/terms/when.rb +2 -3
  77. data/lib/factbase/to_json.rb +2 -2
  78. data/lib/factbase/to_xml.rb +6 -10
  79. data/lib/factbase/to_yaml.rb +1 -1
  80. data/lib/factbase/version.rb +1 -2
  81. data/lib/factbase.rb +27 -13
  82. data/lib/fuzz.rb +3 -3
  83. metadata +6 -1
@@ -23,6 +23,6 @@ class Factbase::Boolean
23
23
  val = val[0] if val.respond_to?(:each)
24
24
  return false if val.nil?
25
25
  return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)
26
- raise "Boolean is expected, while #{val.class} received from #{@from}"
26
+ raise(ArgumentError, "Boolean is expected, while #{val.class} received from #{@from}")
27
27
  end
28
28
  end
@@ -31,8 +31,24 @@ class Factbase::Compare < Factbase::TermBase
31
31
  l = l.floor if l.is_a?(Time) && @op == :==
32
32
  rights.any? do |r|
33
33
  r = r.floor if r.is_a?(Time) && @op == :==
34
- l.send(@op, r)
34
+ _compare(l, r)
35
35
  end
36
36
  end
37
37
  end
38
+
39
+ private
40
+
41
+ # Compare values with a contextual error if Ruby rejects the operands.
42
+ # @param [Object] left Left value
43
+ # @param [Object] right Right value
44
+ # @return [Boolean] The result of the comparison
45
+ def _compare(left, right)
46
+ left.__send__(@op, right)
47
+ rescue ArgumentError => e
48
+ raise(
49
+ RuntimeError,
50
+ "Cannot compare #{left.inspect} (#{left.class}) " \
51
+ "with #{right.inspect} (#{right.class}) using (compare #{@op}): #{e.message}"
52
+ )
53
+ end
38
54
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative 'base'
7
+ require_relative 'compare'
8
+
9
+ # Represents a 'contains' term in the Factbase.
10
+ # Returns true if any value of the left operand contains any value of the right
11
+ # as a substring. Operates on string values via `String#include?`.
12
+ class Factbase::Contains < Factbase::TermBase
13
+ # Constructor.
14
+ # @param [Array] operands Operands
15
+ def initialize(operands)
16
+ super()
17
+ @op = Factbase::Compare.new(:include?, operands)
18
+ end
19
+
20
+ # Evaluate term on a fact.
21
+ # @param [Factbase::Fact] fact The fact
22
+ # @param [Array<Factbase::Fact>] maps All maps available
23
+ # @param [Factbase] fb Factbase to use for sub-queries
24
+ # @return [Boolean] True if any left value includes any right value
25
+ def evaluate(fact, maps, fb)
26
+ @op.evaluate(fact, maps, fb)
27
+ end
28
+ end
@@ -25,13 +25,15 @@ class Factbase::Defn < Factbase::TermBase
25
25
  def evaluate(_fact, _maps, _fb)
26
26
  assert_args(2)
27
27
  fn = @operands[0]
28
- raise "A symbol expected as first argument of 'defn'" unless fn.is_a?(Symbol)
29
- raise "Can't use '#{fn}' name as a term" if Factbase::Term.method_defined?(fn)
30
- raise "Term '#{fn}' is already defined" if Factbase::Term.private_method_defined?(fn, false)
31
- raise "The '#{fn}' is a bad name for a term" unless fn.match?(/^[a-z_]+$/)
32
- e = "class Factbase::Term\nprivate\ndef #{fn}(fact, maps, fb)\n#{@operands[1]}\nend\nend"
28
+ raise(ArgumentError, "A symbol expected as first argument of 'defn'") unless fn.is_a?(Symbol)
29
+ raise(ArgumentError, "Can't use '#{fn}' name as a term") if Factbase::Term.method_defined?(fn)
30
+ raise(ArgumentError, "Term '#{fn}' is already defined") if Factbase::Term.private_method_defined?(fn, false)
31
+ raise(ArgumentError, "The '#{fn}' is a bad name for a term") unless fn.match?(/^[a-z_]+$/)
33
32
  # rubocop:disable Security/Eval
34
- eval(e)
33
+ eval(
34
+ "class Factbase::Term\nprivate\ndef #{fn}(fact, maps, fb)\n#{@operands[1]}\nend\nend",
35
+ binding, __FILE__, __LINE__ - 1
36
+ )
35
37
  # rubocop:enable Security/Eval
36
38
  true
37
39
  end
@@ -24,7 +24,7 @@ class Factbase::Empty < Factbase::TermBase
24
24
  assert_args(1)
25
25
  term = @operands[0]
26
26
  unless term.is_a?(Factbase::Term) || term.is_a?(Factbase::TermBase)
27
- raise "A term is expected, but '#{term}' provided"
27
+ raise(ArgumentError, "A term is expected, but '#{term}' provided")
28
28
  end
29
29
  # rubocop:disable Lint/UnreachableLoop
30
30
  fb.query(term, maps).each(fb, fact) do
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative 'base'
7
+ require_relative 'compare'
8
+
9
+ # Represents an 'ends_with' term in the Factbase.
10
+ # Returns true if any value of the left operand ends with any value of the right.
11
+ class Factbase::EndsWith < Factbase::TermBase
12
+ # Constructor.
13
+ # @param [Array] operands Operands
14
+ def initialize(operands)
15
+ super()
16
+ @op = Factbase::Compare.new(:end_with?, operands)
17
+ end
18
+
19
+ # Evaluate term on a fact.
20
+ # @param [Factbase::Fact] fact The fact
21
+ # @param [Array<Factbase::Fact>] maps All maps available
22
+ # @param [Factbase] fb Factbase to use for sub-queries
23
+ # @return [Boolean] True if any left value ends with any right value
24
+ def evaluate(fact, maps, fb)
25
+ @op.evaluate(fact, maps, fb)
26
+ end
27
+ end
@@ -22,9 +22,9 @@ class Factbase::First < Factbase::TermBase
22
22
  def evaluate(_fact, maps, _fb)
23
23
  assert_args(1)
24
24
  k = @operands[0]
25
- raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
25
+ raise(ArgumentError, "A symbol is expected, but #{k} provided") unless k.is_a?(Symbol)
26
26
  first = maps[0]
27
- return nil if first.nil?
27
+ return if first.nil?
28
28
  first[k.to_s]
29
29
  end
30
30
  end
@@ -27,11 +27,11 @@ class Factbase::Head < Factbase::TermBase
27
27
  def predict(maps, fb, params)
28
28
  assert_args(2)
29
29
  max = @operands[0]
30
- raise "An integer is expected as first argument of '#{@op}'" unless max.is_a?(Integer)
30
+ raise(ArgumentError, "An integer is expected as first argument of '#{@op}'") unless max.is_a?(Integer)
31
31
  term = @operands[1]
32
- raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
32
+ raise(ArgumentError, "A term is expected, but '#{term}' provided") unless term.is_a?(Factbase::Term)
33
33
  fb.query(term, maps).each(fb, params).to_a
34
34
  .take(max)
35
- .map { |m| m.all_properties.to_h { |k| [k, m[k]] } }
35
+ .map! { |m| m.all_properties.to_h { |k| [k, m[k]] } }
36
36
  end
37
37
  end
@@ -26,9 +26,9 @@ class Factbase::Inverted < Factbase::TermBase
26
26
  def predict(maps, fb, params)
27
27
  assert_args(1)
28
28
  term = @operands[0]
29
- raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
29
+ raise(ArgumentError, "A term is expected, but '#{term}' provided") unless term.is_a?(Factbase::Term)
30
30
  fb.query(term, maps).each(fb, params).to_a
31
31
  .reverse
32
- .map { |m| m.all_properties.to_h { |k| [k, m[k]] } }
32
+ .map! { |m| m.all_properties.to_h { |k| [k, m[k]] } }
33
33
  end
34
34
  end
@@ -23,18 +23,19 @@ class Factbase::Join < Factbase::TermBase
23
23
  def evaluate(fact, maps, fb)
24
24
  assert_args(2)
25
25
  jumps = @operands[0]
26
- raise "A string is expected as first argument of 'join'" unless jumps.is_a?(String)
27
- jumps = jumps.split(',')
28
- .map(&:strip)
29
- .map { |j| j.split('<=').map(&:strip) }
30
- .map { |j| j.size == 1 ? [j[0], j[0]] : j }
26
+ raise(ArgumentError, "A string is expected as first argument of 'join'") unless jumps.is_a?(String)
27
+ jumps =
28
+ jumps.split(',')
29
+ .map(&:strip)
30
+ .map! { |j| j.split('<=').map(&:strip) }
31
+ .map! { |j| j.size == 1 ? [j[0], j[0]] : j }
31
32
  term = @operands[1]
32
- raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
33
+ raise(ArgumentError, "A term is expected, but '#{term}' provided") unless term.is_a?(Factbase::Term)
33
34
  subset = fb.query(term, maps).each(fb, fact).to_a
34
35
  subset.each do |s|
35
36
  jumps.each do |to, from|
36
37
  s[from]&.each do |v|
37
- fact.send(:"#{to}=", v)
38
+ fact.__send__(:"#{to}=", v)
38
39
  end
39
40
  end
40
41
  end
@@ -14,6 +14,7 @@ class Factbase::Matches < Factbase::TermBase
14
14
  def initialize(operands)
15
15
  super()
16
16
  @operands = operands
17
+ @regexps = {}
17
18
  end
18
19
 
19
20
  # Evaluate term on a fact.
@@ -25,10 +26,19 @@ class Factbase::Matches < Factbase::TermBase
25
26
  assert_args(2)
26
27
  str = _values(0, fact, maps, fb)
27
28
  return false if str.nil?
28
- raise 'Exactly one string is expected' unless str.size == 1
29
+ raise(RuntimeError, 'Exactly one string is expected') unless str.size == 1
29
30
  re = _values(1, fact, maps, fb)
30
- raise 'Regexp is nil' if re.nil?
31
- raise 'Exactly one regexp is expected' unless re.size == 1
32
- str[0].to_s.match?(re[0])
31
+ raise(RuntimeError, 'Regexp is nil') if re.nil?
32
+ raise(RuntimeError, 'Exactly one regexp is expected') unless re.size == 1
33
+ str[0].to_s.match?(regexp(re[0]))
34
+ end
35
+
36
+ private
37
+
38
+ def regexp(pattern)
39
+ key = pattern.to_s
40
+ @regexps[key] ||= Regexp.new(key)
41
+ rescue RegexpError => e
42
+ raise(RuntimeError, "Invalid regexp '#{key}': #{e.message}")
33
43
  end
34
44
  end
@@ -4,8 +4,8 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative '../../factbase'
7
- require_relative 'best'
8
7
  require_relative 'base'
8
+ require_relative 'best'
9
9
 
10
10
  # The 'max' term.
11
11
  # This term calculates the max value among the evaluated operands.
@@ -4,8 +4,8 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative '../../factbase'
7
- require_relative 'best'
8
7
  require_relative 'base'
8
+ require_relative 'best'
9
9
 
10
10
  # The 'min' term.
11
11
  # This term calculates the minimum value among the evaluated operands.
@@ -23,11 +23,11 @@ class Factbase::Nth < Factbase::TermBase
23
23
  def evaluate(_fact, maps, _fb)
24
24
  assert_args(2)
25
25
  pos = @operands[0]
26
- raise "An integer is expected, but #{pos} provided" unless pos.is_a?(Integer)
26
+ raise(ArgumentError, "An integer is expected, but #{pos} provided") unless pos.is_a?(Integer)
27
27
  k = @operands[1]
28
- raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
28
+ raise(ArgumentError, "A symbol is expected, but #{k} provided") unless k.is_a?(Symbol)
29
29
  m = maps[pos]
30
- return nil if m.nil?
30
+ return if m.nil?
31
31
  m[k.to_s]
32
32
  end
33
33
  end
@@ -3,8 +3,8 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require_relative 'base'
7
6
  require_relative 'arithmetic'
7
+ require_relative 'base'
8
8
 
9
9
  # Represents a Plus term in the Factbase system.
10
10
  # This class is used to perform addition operations on operands.
@@ -4,8 +4,8 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative 'base'
7
- # The Factbase::Unique class provides functionality for evaluating the uniqueness
8
- # of terms based on provided operands and facts.
7
+ # The Factbase::Prev class returns the previous value of a property
8
+ # during iteration, enabling comparisons between consecutive facts.
9
9
  class Factbase::Prev < Factbase::TermBase
10
10
  # Constructor.
11
11
  # @param [Array] operands Operands
@@ -21,9 +21,6 @@ class Factbase::Prev < Factbase::TermBase
21
21
  # @return [Object] The previous value
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
- before = @prev
25
- v = _values(0, fact, maps, fb)
26
- @prev = v
27
- before
24
+ @prev.tap { @prev = _values(0, fact, maps, fb) }
28
25
  end
29
26
  end
@@ -27,9 +27,9 @@ class Factbase::Sorted < Factbase::TermBase
27
27
  def predict(maps, fb, params)
28
28
  assert_args(2)
29
29
  prop = @operands[0]
30
- raise "A symbol is expected as first argument of 'sorted'" unless prop.is_a?(Symbol)
30
+ raise(ArgumentError, "A symbol is expected as first argument of 'sorted'") unless prop.is_a?(Symbol)
31
31
  term = @operands[1]
32
- raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
32
+ raise(ArgumentError, "A term is expected, but '#{term}' provided") unless term.is_a?(Factbase::Term)
33
33
  fb.query(term, maps).each(fb, params).to_a
34
34
  .reject { |m| m[prop].nil? }
35
35
  .sort_by { |m| m[prop].first }
@@ -22,8 +22,17 @@ class Factbase::Sprintf < Factbase::TermBase
22
22
  # @param [Factbase] fb Factbase to use for sub-queries
23
23
  # @return [String] The formatted string
24
24
  def evaluate(fact, maps, fb)
25
- fmt = _values(0, fact, maps, fb)[0]
26
- ops = (1..(@operands.length - 1)).map { |i| _values(i, fact, maps, fb)&.first }
25
+ formatted(
26
+ _values(0, fact, maps, fb)[0],
27
+ (1..(@operands.length - 1)).map { |i| _values(i, fact, maps, fb)&.first }
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def formatted(fmt, ops)
27
34
  format(*([fmt] + ops))
35
+ rescue ArgumentError => e
36
+ raise(RuntimeError, "Cannot format #{ops.inspect} with '#{fmt}' in (sprintf ...): #{e.message}")
28
37
  end
29
38
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative 'base'
7
+ require_relative 'compare'
8
+
9
+ # Represents a 'starts_with' term in the Factbase.
10
+ # Returns true if any value of the left operand starts with any value of the right.
11
+ class Factbase::StartsWith < Factbase::TermBase
12
+ # Constructor.
13
+ # @param [Array] operands Operands
14
+ def initialize(operands)
15
+ super()
16
+ @op = Factbase::Compare.new(:start_with?, operands)
17
+ end
18
+
19
+ # Evaluate term on a fact.
20
+ # @param [Factbase::Fact] fact The fact
21
+ # @param [Array<Factbase::Fact>] maps All maps available
22
+ # @param [Factbase] fb Factbase to use for sub-queries
23
+ # @return [Boolean] True if any left value starts with any right value
24
+ def evaluate(fact, maps, fb)
25
+ @op.evaluate(fact, maps, fb)
26
+ end
27
+ end
@@ -6,7 +6,7 @@
6
6
  require_relative 'base'
7
7
 
8
8
  # This class represents a specialized 'sum' term.
9
- # This term calculates the sum of values for a specified key.
9
+ # This term calculates the sum of values for a specified key.
10
10
  class Factbase::Sum < Factbase::TermBase
11
11
  # Constructor.
12
12
  # @param [Array] operands Operands
@@ -23,7 +23,7 @@ class Factbase::Sum < Factbase::TermBase
23
23
  # @return [Integer] The sum of values for the specified key across all maps
24
24
  def evaluate(_fact, maps, _fb)
25
25
  k = @operands[0]
26
- raise "A symbol is expected, but '#{k}' provided" unless k.is_a?(Symbol)
26
+ raise(ArgumentError, "A symbol is expected, but '#{k}' provided") unless k.is_a?(Symbol)
27
27
  sum = 0
28
28
  maps.each do |m|
29
29
  vv = m[k.to_s]
@@ -22,7 +22,7 @@ class Factbase::ToFloat < Factbase::TermBase
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
- return nil if vv.nil?
26
- vv[0].to_f
25
+ return if vv.nil?
26
+ Float(vv[0])
27
27
  end
28
28
  end
@@ -22,7 +22,7 @@ class Factbase::ToInteger < Factbase::TermBase
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
- return nil if vv.nil?
26
- vv[0].to_i
25
+ return if vv.nil?
26
+ Integer(vv[0])
27
27
  end
28
28
  end
@@ -22,7 +22,7 @@ class Factbase::ToString < Factbase::TermBase
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
- return nil if vv.nil?
25
+ return if vv.nil?
26
26
  vv[0].to_s
27
27
  end
28
28
  end
@@ -22,7 +22,15 @@ class Factbase::ToTime < Factbase::TermBase
22
22
  def evaluate(fact, maps, fb)
23
23
  assert_args(1)
24
24
  vv = _values(0, fact, maps, fb)
25
- return nil if vv.nil?
26
- Time.parse(vv[0].to_s)
25
+ return if vv.nil?
26
+ parse(vv[0])
27
+ end
28
+
29
+ private
30
+
31
+ def parse(value)
32
+ Time.parse(value.to_s)
33
+ rescue ArgumentError => e
34
+ raise(RuntimeError, "Cannot parse '#{value}' as Time in (to_time ...): #{e.message}")
27
35
  end
28
36
  end
@@ -25,9 +25,9 @@ class Factbase::Traced < Factbase::TermBase
25
25
  def evaluate(fact, maps, fb)
26
26
  assert_args(1)
27
27
  t = @operands[0]
28
- raise "A term is expected, but '#{t}' provided" unless t.is_a?(Factbase::Term)
28
+ raise(ArgumentError, "A term is expected, but '#{t}' provided") unless t.is_a?(Factbase::Term)
29
29
  r = t.evaluate(fact, maps, fb)
30
- puts "#{self} -> #{r}" # rubocop:disable Lint/Debugger
30
+ puts("#{self} -> #{r}") # rubocop:disable Lint/Debugger
31
31
  r
32
32
  end
33
33
  end
@@ -24,9 +24,9 @@ class Factbase::Undef < Factbase::TermBase
24
24
  def evaluate(_fact, _maps, _fb)
25
25
  assert_args(1)
26
26
  fn = @operands[0]
27
- raise "A symbol expected as first argument of 'undef'" unless fn.is_a?(Symbol)
27
+ raise(ArgumentError, "A symbol expected as first argument of 'undef'") unless fn.is_a?(Symbol)
28
28
  if Factbase::Term.private_method_defined?(fn, false)
29
- Factbase::Term.class_eval("undef :#{fn}", __FILE__, __LINE__ - 1) # undef :foo
29
+ Factbase::Term.class_eval("undef :#{fn}", __FILE__, __LINE__ - 1)
30
30
  end
31
31
  true
32
32
  end
@@ -21,14 +21,10 @@ class Factbase::Unique < Factbase::TermBase
21
21
  # @return [Boolean] True if the value is unique, false otherwise
22
22
  def evaluate(fact, maps, fb)
23
23
  @seen = Set.new if @seen.nil?
24
- raise "Too few operands for 'unique' (at least 1 expected)" if @operands.empty?
24
+ raise(ArgumentError, "Too few operands for 'unique' (at least 1 expected)") if @operands.empty?
25
25
  vv = (0..(@operands.size - 1)).map { |i| _values(i, fact, maps, fb) }
26
26
  return false if vv.any?(nil)
27
- pass = true
28
- Enumerator.product(*vv).to_a.each do |t|
29
- pass = false if @seen.include?(t)
30
- @seen << t
31
- end
32
- pass
27
+ tuples = Enumerator.product(*vv).to_a
28
+ tuples.none? { |t| @seen.include?(t) }.tap { tuples.each { |t| @seen << t } }
33
29
  end
34
30
  end
@@ -22,8 +22,7 @@ class Factbase::When < Factbase::TermBase
22
22
  # @return [Boolean] True if first operand is false OR both are true
23
23
  def evaluate(fact, maps, fb)
24
24
  assert_args(2)
25
- a = @operands[0]
26
- b = @operands[1]
27
- !a.evaluate(fact, maps, fb) || (a.evaluate(fact, maps, fb) && b.evaluate(fact, maps, fb))
25
+ return true unless @operands[0].evaluate(fact, maps, fb)
26
+ @operands[1].evaluate(fact, maps, fb)
28
27
  end
29
28
  end
@@ -9,7 +9,7 @@ require_relative '../factbase/flatten'
9
9
 
10
10
  # Factbase to JSON converter.
11
11
  #
12
- # This class helps converting an entire Factbase to YAML format, for example:
12
+ # This class helps converting an entire Factbase to JSON format, for example:
13
13
  #
14
14
  # require 'factbase/to_json'
15
15
  # fb = Factbase.new
@@ -28,6 +28,6 @@ class Factbase::ToJSON
28
28
  # Convert the entire factbase into JSON.
29
29
  # @return [String] The factbase in JSON format
30
30
  def json
31
- Factbase::Flatten.new(Marshal.load(@fb.export), @sorter).it.to_json
31
+ Factbase::Flatten.new(@fb.each.to_a, @sorter).it.to_json
32
32
  end
33
33
  end
@@ -10,7 +10,7 @@ require_relative '../factbase/flatten'
10
10
 
11
11
  # Factbase to XML converter.
12
12
  #
13
- # This class helps converting an entire Factbase to YAML format, for example:
13
+ # This class helps converting an entire Factbase to XML format, for example:
14
14
  #
15
15
  # require 'factbase/to_xml'
16
16
  # fb = Factbase.new
@@ -29,24 +29,20 @@ class Factbase::ToXML
29
29
  # Convert the entire factbase into XML.
30
30
  # @return [String] The factbase in XML format
31
31
  def xml
32
- bytes = @fb.export
33
- meta = {
34
- version: Factbase::VERSION,
35
- size: bytes.size
36
- }
32
+ meta = { version: Factbase::VERSION, size: @fb.export.size }
37
33
  Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
38
34
  xml.fb(meta) do
39
- Factbase::Flatten.new(Marshal.load(bytes), @sorter).it.each do |m|
35
+ Factbase::Flatten.new(@fb.each.to_a, @sorter).it.each do |m|
40
36
  xml.f_ do
41
37
  m.sort.to_h.each do |k, vv|
42
38
  if vv.is_a?(Array)
43
- xml.send(:"#{k}_") do
39
+ xml.__send__(:"#{k}_") do
44
40
  vv.each do |v|
45
- xml.send(:v, to_str(v), t: type_of(v))
41
+ xml.__send__(:v, to_str(v), t: type_of(v))
46
42
  end
47
43
  end
48
44
  else
49
- xml.send(:"#{k}_", to_str(vv), t: type_of(vv))
45
+ xml.__send__(:"#{k}_", to_str(vv), t: type_of(vv))
50
46
  end
51
47
  end
52
48
  end
@@ -28,6 +28,6 @@ class Factbase::ToYAML
28
28
  # Convert the entire factbase into YAML.
29
29
  # @return [String] The factbase in YAML format
30
30
  def yaml
31
- YAML.dump(Factbase::Flatten.new(Marshal.load(@fb.export), @sorter).it)
31
+ YAML.dump(Factbase::Flatten.new(@fb.each.to_a, @sorter).it)
32
32
  end
33
33
  end
@@ -8,6 +8,5 @@
8
8
  # Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
9
9
  # License:: MIT
10
10
  class Factbase
11
- # Current version of the gem (changed by .rultor.yml on every release)
12
- VERSION = '0.19.10' unless const_defined?(:VERSION)
11
+ VERSION = '0.19.12' unless const_defined?(:VERSION)
13
12
  end
data/lib/factbase.rb CHANGED
@@ -98,18 +98,22 @@ class Factbase
98
98
  @maps.size
99
99
  end
100
100
 
101
+ # Iterate over all facts yielding plain hashes.
102
+ # @yieldparam [Hash] fact Each fact as a plain Hash
103
+ # @return [Integer, Enumerator] Total number of facts or Enumerator
104
+ def each(&)
105
+ @maps.each(&)
106
+ end
107
+
101
108
  # Insert a new fact and return it.
102
109
  #
103
110
  # A fact, when inserted, is empty. It doesn't contain any properties.
104
111
  #
105
- # The operation is thread-safe, meaning that different threads may
106
- # insert facts in parallel without breaking the consistency of the factbase.
107
- #
108
112
  # @return [Factbase::Fact] The fact just inserted
109
113
  def insert
110
114
  map = {}
111
115
  @maps << map
112
- require_relative 'factbase/fact'
116
+ require_relative('factbase/fact')
113
117
  Factbase::Fact.new(map)
114
118
  end
115
119
 
@@ -135,7 +139,7 @@ class Factbase
135
139
  def query(term, maps = nil)
136
140
  maps ||= @maps
137
141
  term = to_term(term) if term.is_a?(String)
138
- require_relative 'factbase/query'
142
+ require_relative('factbase/query')
139
143
  Factbase::Query.new(maps, term, self)
140
144
  end
141
145
 
@@ -143,7 +147,7 @@ class Factbase
143
147
  # @param [String] query The query to convert
144
148
  # @return [Factbase::Term] The term
145
149
  def to_term(query)
146
- require_relative 'factbase/syntax'
150
+ require_relative('factbase/syntax')
147
151
  Factbase::Syntax.new(query).to_term
148
152
  end
149
153
 
@@ -164,15 +168,15 @@ class Factbase
164
168
  #
165
169
  # @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
166
170
  def txn
167
- require_relative 'factbase/lazy_taped'
171
+ require_relative('factbase/lazy_taped')
168
172
  taped = Factbase::LazyTaped.new(@maps)
169
- require_relative 'factbase/churn'
173
+ require_relative('factbase/churn')
170
174
  churn = Factbase::Churn.new
171
- catch :commit do
172
- require_relative 'factbase/light'
175
+ catch(:commit) do
176
+ require_relative('factbase/light')
173
177
  commit = false
174
- catch :rollback do
175
- yield Factbase::Light.new(Factbase.new(taped))
178
+ catch(:rollback) do
179
+ yield(Factbase::Light.new(Factbase.new(taped)))
176
180
  commit = true
177
181
  end
178
182
  return churn unless commit
@@ -236,9 +240,19 @@ class Factbase
236
240
  # This method supports both the original format (Array of maps) and
237
241
  # the IndexedFactbase format (Hash with :maps and :idx keys).
238
242
  #
243
+ # SECURITY: the input must come from a source you trust, because it is
244
+ # deserialized with +Marshal.load+. Loading a +Marshal+ stream crafted
245
+ # by an attacker can execute arbitrary code in the calling process,
246
+ # so never call +import+ on bytes received over the network, read from
247
+ # a user-supplied path, or pulled from any other untrusted channel
248
+ # without an out-of-band integrity check. See the official Ruby
249
+ # security notes for +Marshal.load+ at
250
+ # https://docs.ruby-lang.org/en/3.3/security_rdoc.html#label-Marshal.load
251
+ # for details.
252
+ #
239
253
  # @param [String] bytes Binary string to import
240
254
  def import(bytes)
241
- raise 'Empty input, cannot load a factbase' if bytes.empty?
255
+ raise(StandardError, 'Empty input, cannot load a factbase') if bytes.empty?
242
256
  data = Marshal.load(bytes)
243
257
  @maps +=
244
258
  if data.is_a?(Hash) && data.key?(:maps)