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.
- checksums.yaml +4 -4
- data/Gemfile +5 -4
- data/Gemfile.lock +18 -14
- data/README.md +127 -20
- data/Rakefile +2 -7
- data/factbase.gemspec +11 -11
- data/lib/factbase/accum.rb +1 -1
- data/lib/factbase/cached/cached_fact.rb +1 -2
- data/lib/factbase/cached/cached_factbase.rb +3 -3
- data/lib/factbase/cached/cached_query.rb +4 -6
- data/lib/factbase/cached/cached_term.rb +1 -2
- data/lib/factbase/churn.rb +4 -8
- data/lib/factbase/fact.rb +12 -9
- data/lib/factbase/flatten.rb +2 -2
- data/lib/factbase/impatient.rb +14 -13
- data/lib/factbase/indexed/indexed_and.rb +14 -20
- data/lib/factbase/indexed/indexed_eq.rb +5 -1
- data/lib/factbase/indexed/indexed_fact.rb +1 -4
- data/lib/factbase/indexed/indexed_factbase.rb +4 -4
- data/lib/factbase/indexed/indexed_gt.rb +3 -1
- data/lib/factbase/indexed/indexed_gte.rb +51 -0
- data/lib/factbase/indexed/indexed_lt.rb +3 -1
- data/lib/factbase/indexed/indexed_lte.rb +51 -0
- data/lib/factbase/indexed/indexed_not.rb +1 -1
- data/lib/factbase/indexed/indexed_or.rb +2 -2
- data/lib/factbase/indexed/indexed_query.rb +6 -7
- data/lib/factbase/indexed/indexed_term.rb +10 -6
- data/lib/factbase/indexed/indexed_unique.rb +4 -2
- data/lib/factbase/inv.rb +3 -3
- data/lib/factbase/lazy_taped.rb +10 -13
- data/lib/factbase/lazy_taped_hash.rb +2 -1
- data/lib/factbase/light.rb +1 -1
- data/lib/factbase/logged.rb +37 -34
- data/lib/factbase/pre.rb +3 -3
- data/lib/factbase/query.rb +4 -5
- data/lib/factbase/rules.rb +8 -8
- data/lib/factbase/sync/sync_factbase.rb +2 -2
- data/lib/factbase/syntax.rb +19 -20
- data/lib/factbase/tallied.rb +7 -8
- data/lib/factbase/taped.rb +5 -11
- data/lib/factbase/tee.rb +2 -2
- data/lib/factbase/term.rb +58 -59
- data/lib/factbase/terms/agg.rb +3 -4
- data/lib/factbase/terms/arithmetic.rb +7 -7
- data/lib/factbase/terms/as.rb +2 -2
- data/lib/factbase/terms/assert.rb +5 -13
- data/lib/factbase/terms/base.rb +7 -10
- data/lib/factbase/terms/best.rb +1 -1
- data/lib/factbase/terms/boolean.rb +1 -1
- data/lib/factbase/terms/compare.rb +17 -1
- data/lib/factbase/terms/contains.rb +28 -0
- data/lib/factbase/terms/defn.rb +8 -6
- data/lib/factbase/terms/empty.rb +1 -1
- data/lib/factbase/terms/ends_with.rb +27 -0
- data/lib/factbase/terms/first.rb +2 -2
- data/lib/factbase/terms/head.rb +3 -3
- data/lib/factbase/terms/inverted.rb +2 -2
- data/lib/factbase/terms/join.rb +8 -7
- data/lib/factbase/terms/matches.rb +14 -4
- data/lib/factbase/terms/max.rb +1 -1
- data/lib/factbase/terms/min.rb +1 -1
- data/lib/factbase/terms/nth.rb +3 -3
- data/lib/factbase/terms/plus.rb +1 -1
- data/lib/factbase/terms/prev.rb +3 -6
- data/lib/factbase/terms/sorted.rb +2 -2
- data/lib/factbase/terms/sprintf.rb +11 -2
- data/lib/factbase/terms/starts_with.rb +27 -0
- data/lib/factbase/terms/sum.rb +2 -2
- data/lib/factbase/terms/to_float.rb +2 -2
- data/lib/factbase/terms/to_integer.rb +2 -2
- data/lib/factbase/terms/to_string.rb +1 -1
- data/lib/factbase/terms/to_time.rb +10 -2
- data/lib/factbase/terms/traced.rb +2 -2
- data/lib/factbase/terms/undef.rb +2 -2
- data/lib/factbase/terms/unique.rb +3 -7
- data/lib/factbase/terms/when.rb +2 -3
- data/lib/factbase/to_json.rb +2 -2
- data/lib/factbase/to_xml.rb +6 -10
- data/lib/factbase/to_yaml.rb +1 -1
- data/lib/factbase/version.rb +1 -2
- data/lib/factbase.rb +27 -13
- data/lib/fuzz.rb +3 -3
- 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
|
|
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
|
data/lib/factbase/terms/defn.rb
CHANGED
|
@@ -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(
|
|
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
|
data/lib/factbase/terms/empty.rb
CHANGED
|
@@ -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
|
data/lib/factbase/terms/first.rb
CHANGED
|
@@ -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
|
|
27
|
+
return if first.nil?
|
|
28
28
|
first[k.to_s]
|
|
29
29
|
end
|
|
30
30
|
end
|
data/lib/factbase/terms/head.rb
CHANGED
|
@@ -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
|
data/lib/factbase/terms/join.rb
CHANGED
|
@@ -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 =
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
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
|
data/lib/factbase/terms/max.rb
CHANGED
data/lib/factbase/terms/min.rb
CHANGED
data/lib/factbase/terms/nth.rb
CHANGED
|
@@ -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
|
|
30
|
+
return if m.nil?
|
|
31
31
|
m[k.to_s]
|
|
32
32
|
end
|
|
33
33
|
end
|
data/lib/factbase/terms/plus.rb
CHANGED
|
@@ -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.
|
data/lib/factbase/terms/prev.rb
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
6
|
require_relative 'base'
|
|
7
|
-
# The Factbase::
|
|
8
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
data/lib/factbase/terms/sum.rb
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
require_relative 'base'
|
|
7
7
|
|
|
8
8
|
# This class represents a specialized 'sum' term.
|
|
9
|
-
# This term
|
|
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,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
|
|
26
|
-
|
|
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
|
|
30
|
+
puts("#{self} -> #{r}") # rubocop:disable Lint/Debugger
|
|
31
31
|
r
|
|
32
32
|
end
|
|
33
33
|
end
|
data/lib/factbase/terms/undef.rb
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
28
|
-
|
|
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
|
data/lib/factbase/terms/when.rb
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
data/lib/factbase/to_json.rb
CHANGED
|
@@ -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
|
|
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(
|
|
31
|
+
Factbase::Flatten.new(@fb.each.to_a, @sorter).it.to_json
|
|
32
32
|
end
|
|
33
33
|
end
|
data/lib/factbase/to_xml.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
39
|
+
xml.__send__(:"#{k}_") do
|
|
44
40
|
vv.each do |v|
|
|
45
|
-
xml.
|
|
41
|
+
xml.__send__(:v, to_str(v), t: type_of(v))
|
|
46
42
|
end
|
|
47
43
|
end
|
|
48
44
|
else
|
|
49
|
-
xml.
|
|
45
|
+
xml.__send__(:"#{k}_", to_str(vv), t: type_of(vv))
|
|
50
46
|
end
|
|
51
47
|
end
|
|
52
48
|
end
|
data/lib/factbase/to_yaml.rb
CHANGED
|
@@ -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(
|
|
31
|
+
YAML.dump(Factbase::Flatten.new(@fb.each.to_a, @sorter).it)
|
|
32
32
|
end
|
|
33
33
|
end
|
data/lib/factbase/version.rb
CHANGED
|
@@ -8,6 +8,5 @@
|
|
|
8
8
|
# Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
9
9
|
# License:: MIT
|
|
10
10
|
class Factbase
|
|
11
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
171
|
+
require_relative('factbase/lazy_taped')
|
|
168
172
|
taped = Factbase::LazyTaped.new(@maps)
|
|
169
|
-
require_relative
|
|
173
|
+
require_relative('factbase/churn')
|
|
170
174
|
churn = Factbase::Churn.new
|
|
171
|
-
catch
|
|
172
|
-
require_relative
|
|
175
|
+
catch(:commit) do
|
|
176
|
+
require_relative('factbase/light')
|
|
173
177
|
commit = false
|
|
174
|
-
catch
|
|
175
|
-
yield
|
|
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)
|