hayadentaku 3.5.7
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 +7 -0
- data/.github/workflows/rspec.yml +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/.gitignore +14 -0
- data/.pryrc +2 -0
- data/.rubocop.yml +114 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +328 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +352 -0
- data/Rakefile +31 -0
- data/hayadentaku.gemspec +35 -0
- data/lib/dentaku/ast/access.rb +44 -0
- data/lib/dentaku/ast/arithmetic.rb +292 -0
- data/lib/dentaku/ast/array.rb +38 -0
- data/lib/dentaku/ast/bitwise.rb +42 -0
- data/lib/dentaku/ast/case/case_conditional.rb +38 -0
- data/lib/dentaku/ast/case/case_else.rb +35 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
- data/lib/dentaku/ast/case/case_then.rb +35 -0
- data/lib/dentaku/ast/case/case_when.rb +39 -0
- data/lib/dentaku/ast/case.rb +93 -0
- data/lib/dentaku/ast/combinators.rb +50 -0
- data/lib/dentaku/ast/comparators.rb +88 -0
- data/lib/dentaku/ast/datetime.rb +8 -0
- data/lib/dentaku/ast/function.rb +56 -0
- data/lib/dentaku/ast/function_registry.rb +107 -0
- data/lib/dentaku/ast/functions/abs.rb +5 -0
- data/lib/dentaku/ast/functions/all.rb +19 -0
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/any.rb +19 -0
- data/lib/dentaku/ast/functions/avg.rb +13 -0
- data/lib/dentaku/ast/functions/count.rb +26 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/enum.rb +54 -0
- data/lib/dentaku/ast/functions/filter.rb +21 -0
- data/lib/dentaku/ast/functions/if.rb +47 -0
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/map.rb +19 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/mul.rb +12 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/or.rb +25 -0
- data/lib/dentaku/ast/functions/pluck.rb +34 -0
- data/lib/dentaku/ast/functions/reduce.rb +60 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +8 -0
- data/lib/dentaku/ast/functions/roundup.rb +8 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
- data/lib/dentaku/ast/functions/string_functions.rb +212 -0
- data/lib/dentaku/ast/functions/sum.rb +12 -0
- data/lib/dentaku/ast/functions/switch.rb +8 -0
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/grouping.rb +23 -0
- data/lib/dentaku/ast/identifier.rb +52 -0
- data/lib/dentaku/ast/literal.rb +30 -0
- data/lib/dentaku/ast/logical.rb +8 -0
- data/lib/dentaku/ast/negation.rb +54 -0
- data/lib/dentaku/ast/nil.rb +13 -0
- data/lib/dentaku/ast/node.rb +29 -0
- data/lib/dentaku/ast/numeric.rb +8 -0
- data/lib/dentaku/ast/operation.rb +44 -0
- data/lib/dentaku/ast/string.rb +15 -0
- data/lib/dentaku/ast.rb +42 -0
- data/lib/dentaku/bulk_expression_solver.rb +158 -0
- data/lib/dentaku/calculator.rb +192 -0
- data/lib/dentaku/date_arithmetic.rb +60 -0
- data/lib/dentaku/dependency_resolver.rb +29 -0
- data/lib/dentaku/exceptions.rb +116 -0
- data/lib/dentaku/flat_hash.rb +161 -0
- data/lib/dentaku/parser.rb +318 -0
- data/lib/dentaku/print_visitor.rb +112 -0
- data/lib/dentaku/string_casing.rb +7 -0
- data/lib/dentaku/token.rb +48 -0
- data/lib/dentaku/token_matcher.rb +138 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +240 -0
- data/lib/dentaku/tokenizer.rb +127 -0
- data/lib/dentaku/version.rb +3 -0
- data/lib/dentaku/visitor/infix.rb +86 -0
- data/lib/dentaku.rb +69 -0
- data/spec/ast/abs_spec.rb +26 -0
- data/spec/ast/addition_spec.rb +67 -0
- data/spec/ast/all_spec.rb +38 -0
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +32 -0
- data/spec/ast/any_spec.rb +36 -0
- data/spec/ast/arithmetic_spec.rb +147 -0
- data/spec/ast/avg_spec.rb +42 -0
- data/spec/ast/case_spec.rb +84 -0
- data/spec/ast/comparator_spec.rb +87 -0
- data/spec/ast/count_spec.rb +40 -0
- data/spec/ast/division_spec.rb +64 -0
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +69 -0
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +40 -0
- data/spec/ast/max_spec.rb +33 -0
- data/spec/ast/min_spec.rb +33 -0
- data/spec/ast/mul_spec.rb +43 -0
- data/spec/ast/negation_spec.rb +48 -0
- data/spec/ast/node_spec.rb +43 -0
- data/spec/ast/numeric_spec.rb +16 -0
- data/spec/ast/or_spec.rb +35 -0
- data/spec/ast/pluck_spec.rb +49 -0
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/ast/round_spec.rb +35 -0
- data/spec/ast/rounddown_spec.rb +35 -0
- data/spec/ast/roundup_spec.rb +35 -0
- data/spec/ast/string_functions_spec.rb +217 -0
- data/spec/ast/sum_spec.rb +43 -0
- data/spec/ast/switch_spec.rb +30 -0
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/benchmark.rb +70 -0
- data/spec/bulk_expression_solver_spec.rb +241 -0
- data/spec/calculator_spec.rb +1003 -0
- data/spec/dentaku_spec.rb +52 -0
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/exceptions_spec.rb +9 -0
- data/spec/external_function_spec.rb +177 -0
- data/spec/parser_spec.rb +183 -0
- data/spec/print_visitor_spec.rb +77 -0
- data/spec/spec_helper.rb +69 -0
- data/spec/token_matcher_spec.rb +134 -0
- data/spec/token_scanner_spec.rb +49 -0
- data/spec/token_spec.rb +16 -0
- data/spec/tokenizer_spec.rb +375 -0
- data/spec/visitor/infix_spec.rb +52 -0
- data/spec/visitor_spec.rb +139 -0
- metadata +353 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require_relative './node'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class Operation < Node
|
|
6
|
+
attr_reader :left, :right
|
|
7
|
+
|
|
8
|
+
def self.min_param_count
|
|
9
|
+
arity
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.max_param_count
|
|
13
|
+
arity
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(left, right)
|
|
17
|
+
@left = left
|
|
18
|
+
@right = right
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def dependencies(context = {})
|
|
22
|
+
(left.dependencies(context) + right.dependencies(context)).uniq
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.right_associative?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def accept(visitor)
|
|
30
|
+
visitor.visit_operation(self)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def display_operator
|
|
34
|
+
operator.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
alias_method :to_s, :display_operator
|
|
38
|
+
|
|
39
|
+
def operator_spacing
|
|
40
|
+
" "
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/dentaku/ast.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require_relative './ast/node'
|
|
2
|
+
require_relative './ast/nil'
|
|
3
|
+
require_relative './ast/datetime'
|
|
4
|
+
require_relative './ast/numeric'
|
|
5
|
+
require_relative './ast/logical'
|
|
6
|
+
require_relative './ast/string'
|
|
7
|
+
require_relative './ast/identifier'
|
|
8
|
+
require_relative './ast/arithmetic'
|
|
9
|
+
require_relative './ast/bitwise'
|
|
10
|
+
require_relative './ast/negation'
|
|
11
|
+
require_relative './ast/comparators'
|
|
12
|
+
require_relative './ast/combinators'
|
|
13
|
+
require_relative './ast/access'
|
|
14
|
+
require_relative './ast/array'
|
|
15
|
+
require_relative './ast/grouping'
|
|
16
|
+
require_relative './ast/case'
|
|
17
|
+
require_relative './ast/function_registry'
|
|
18
|
+
require_relative './ast/functions/abs'
|
|
19
|
+
require_relative './ast/functions/all'
|
|
20
|
+
require_relative './ast/functions/and'
|
|
21
|
+
require_relative './ast/functions/any'
|
|
22
|
+
require_relative './ast/functions/avg'
|
|
23
|
+
require_relative './ast/functions/count'
|
|
24
|
+
require_relative './ast/functions/duration'
|
|
25
|
+
require_relative './ast/functions/filter'
|
|
26
|
+
require_relative './ast/functions/if'
|
|
27
|
+
require_relative './ast/functions/intercept'
|
|
28
|
+
require_relative './ast/functions/map'
|
|
29
|
+
require_relative './ast/functions/max'
|
|
30
|
+
require_relative './ast/functions/min'
|
|
31
|
+
require_relative './ast/functions/not'
|
|
32
|
+
require_relative './ast/functions/or'
|
|
33
|
+
require_relative './ast/functions/pluck'
|
|
34
|
+
require_relative './ast/functions/reduce'
|
|
35
|
+
require_relative './ast/functions/round'
|
|
36
|
+
require_relative './ast/functions/rounddown'
|
|
37
|
+
require_relative './ast/functions/roundup'
|
|
38
|
+
require_relative './ast/functions/ruby_math'
|
|
39
|
+
require_relative './ast/functions/string_functions'
|
|
40
|
+
require_relative './ast/functions/sum'
|
|
41
|
+
require_relative './ast/functions/switch'
|
|
42
|
+
require_relative './ast/functions/xor'
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require 'dentaku/dependency_resolver'
|
|
2
|
+
require 'dentaku/exceptions'
|
|
3
|
+
require 'dentaku/flat_hash'
|
|
4
|
+
require 'dentaku/parser'
|
|
5
|
+
require 'dentaku/tokenizer'
|
|
6
|
+
|
|
7
|
+
module Dentaku
|
|
8
|
+
class BulkExpressionSolver
|
|
9
|
+
class StrictEvaluator
|
|
10
|
+
def initialize(calculator)
|
|
11
|
+
@calculator = calculator
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def evaluate(*args)
|
|
15
|
+
@calculator.evaluate!(*args)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class PermissiveEvaluator
|
|
20
|
+
def initialize(calculator, block)
|
|
21
|
+
@calculator = calculator
|
|
22
|
+
@block = block || ->(*) { :undefined }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def evaluate(*args)
|
|
26
|
+
@calculator.evaluate(*args) { |expr, ex|
|
|
27
|
+
@block.call(ex)
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(expressions, calculator)
|
|
33
|
+
@expression_hash = FlatHash.from_hash(expressions)
|
|
34
|
+
@calculator = calculator
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def solve!
|
|
38
|
+
@evaluator = StrictEvaluator.new(calculator)
|
|
39
|
+
solve(&raise_exception_handler)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def solve(&block)
|
|
43
|
+
@evaluator ||= PermissiveEvaluator.new(calculator, block)
|
|
44
|
+
error_handler = block || return_undefined_handler
|
|
45
|
+
results = load_results(&error_handler)
|
|
46
|
+
|
|
47
|
+
FlatHash.expand(
|
|
48
|
+
expression_hash.each_with_object({}) do |(k, v), r|
|
|
49
|
+
default = v.nil? ? v : :undefined
|
|
50
|
+
r[k] = results.fetch(k.to_s, default)
|
|
51
|
+
end
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def dependencies
|
|
56
|
+
Hash[expression_deps].tap do |d|
|
|
57
|
+
d.values.each do |deps|
|
|
58
|
+
unresolved = deps.reject { |ud| d.has_key?(ud) }
|
|
59
|
+
unresolved.each { |u| add_dependencies(d, u) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def self.dependency_cache
|
|
67
|
+
@dep_cache ||= {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
attr_reader :expression_hash, :calculator, :evaluator
|
|
71
|
+
|
|
72
|
+
def return_undefined_handler
|
|
73
|
+
->(*) { :undefined }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def raise_exception_handler
|
|
77
|
+
->(ex) { raise ex }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def expression_with_exception_handler(var_name, &block)
|
|
81
|
+
->(_expr, ex) {
|
|
82
|
+
ex.recipient_variable = var_name
|
|
83
|
+
block.call(ex)
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def load_results(&block)
|
|
88
|
+
facts, _formulas = expressions.transform_keys(&:downcase)
|
|
89
|
+
.transform_values { |v| calculator.ast(v) }
|
|
90
|
+
.partition { |_, v| calculator.dependencies(v, nil).empty? }
|
|
91
|
+
|
|
92
|
+
evaluated_facts = facts.to_h.each_with_object({}) do |(var_name, ast), h|
|
|
93
|
+
with_rescues(var_name, h, block) do
|
|
94
|
+
h[var_name] = ast.is_a?(Array) ? ast.map(&:value) : ast.value
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
context = calculator.memory.merge(evaluated_facts)
|
|
99
|
+
|
|
100
|
+
variables_in_resolve_order.each_with_object({}) do |var_name, results|
|
|
101
|
+
next if expressions[var_name].nil?
|
|
102
|
+
|
|
103
|
+
with_rescues(var_name, results, block) do
|
|
104
|
+
results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
|
|
105
|
+
expressions[var_name],
|
|
106
|
+
context.merge(results),
|
|
107
|
+
&expression_with_exception_handler(var_name, &block)
|
|
108
|
+
).tap { |res|
|
|
109
|
+
res.recipient_variable = var_name if res.respond_to?(:recipient_variable=)
|
|
110
|
+
res
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
rescue TSort::Cyclic => ex
|
|
116
|
+
block.call(ex)
|
|
117
|
+
{}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def with_rescues(var_name, results, block)
|
|
121
|
+
yield
|
|
122
|
+
rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
|
|
123
|
+
ex.recipient_variable = var_name
|
|
124
|
+
results[var_name] = block.call(ex)
|
|
125
|
+
ensure
|
|
126
|
+
if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
|
|
127
|
+
results[var_name] = calculator.memory[var_name.downcase]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def expressions
|
|
132
|
+
@expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def expression_deps
|
|
136
|
+
expressions.map do |var, expr|
|
|
137
|
+
[var, calculator.dependencies(expr)]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def add_dependencies(current_dependencies, variable)
|
|
142
|
+
node = calculator.memory[variable]
|
|
143
|
+
if node.respond_to?(:dependencies)
|
|
144
|
+
current_dependencies[variable] = node.dependencies
|
|
145
|
+
node.dependencies.each { |d| add_dependencies(current_dependencies, d) }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def variables_in_resolve_order
|
|
150
|
+
cache_key = expressions.keys.map(&:to_s).sort.join("|")
|
|
151
|
+
@ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
|
|
152
|
+
DependencyResolver.find_resolve_order(dependencies).tap do |d|
|
|
153
|
+
self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
|
|
154
|
+
end
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
require 'dentaku/bulk_expression_solver'
|
|
2
|
+
require 'dentaku/dependency_resolver'
|
|
3
|
+
require 'dentaku/exceptions'
|
|
4
|
+
require 'dentaku/flat_hash'
|
|
5
|
+
require 'dentaku/parser'
|
|
6
|
+
require 'dentaku/string_casing'
|
|
7
|
+
require 'dentaku/token'
|
|
8
|
+
|
|
9
|
+
module Dentaku
|
|
10
|
+
class Calculator
|
|
11
|
+
include StringCasing
|
|
12
|
+
attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
|
|
13
|
+
:nested_data_support, :ast_cache, :raw_date_literals
|
|
14
|
+
|
|
15
|
+
def initialize(options = {})
|
|
16
|
+
clear
|
|
17
|
+
@tokenizer = Tokenizer.new
|
|
18
|
+
@case_sensitive = options.delete(:case_sensitive)
|
|
19
|
+
@aliases = options.delete(:aliases) || Dentaku.aliases
|
|
20
|
+
@nested_data_support = options.fetch(:nested_data_support, true)
|
|
21
|
+
options.delete(:nested_data_support)
|
|
22
|
+
@raw_date_literals = options.fetch(:raw_date_literals, true)
|
|
23
|
+
options.delete(:raw_date_literals)
|
|
24
|
+
@ast_cache = options
|
|
25
|
+
@disable_ast_cache = false
|
|
26
|
+
@function_registry = Dentaku::AST::FunctionRegistry.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.add_function(name, type, body, callback = nil)
|
|
30
|
+
Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.add_functions(functions)
|
|
34
|
+
functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_function(name, type, body, callback = nil)
|
|
38
|
+
@function_registry.register(name, type, body, callback)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add_functions(functions)
|
|
43
|
+
functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def disable_cache
|
|
48
|
+
@disable_ast_cache = true
|
|
49
|
+
yield(self) if block_given?
|
|
50
|
+
ensure
|
|
51
|
+
@disable_ast_cache = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def evaluate(expression, data = {}, &block)
|
|
55
|
+
context = evaluation_context(data, :permissive)
|
|
56
|
+
return evaluate_array(expression, context, &block) if expression.is_a?(Array)
|
|
57
|
+
|
|
58
|
+
evaluate!(expression, context)
|
|
59
|
+
rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
|
|
60
|
+
block.call(expression, ex) if block_given?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private def evaluate_array(expression, data = {}, &block)
|
|
64
|
+
expression.map { |e| evaluate(e, data, &block) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def evaluate!(expression, data = {}, &block)
|
|
68
|
+
context = evaluation_context(data, :strict)
|
|
69
|
+
return evaluate_array!(expression, context, &block) if expression.is_a? Array
|
|
70
|
+
|
|
71
|
+
store(context) do
|
|
72
|
+
node = ast(expression)
|
|
73
|
+
unbound = node.dependencies(memory)
|
|
74
|
+
|
|
75
|
+
unless unbound.empty?
|
|
76
|
+
raise UnboundVariableError.new(unbound),
|
|
77
|
+
"no value provided for variables: #{unbound.uniq.join(', ')}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
node.value(memory)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private def evaluate_array!(expression, data = {}, &block)
|
|
85
|
+
expression.map { |e| evaluate!(e, data, &block) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def solve!(expression_hash)
|
|
89
|
+
BulkExpressionSolver.new(expression_hash, self).solve!
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def solve(expression_hash, &block)
|
|
93
|
+
BulkExpressionSolver.new(expression_hash, self).solve(&block)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def dependencies(expression, context = {})
|
|
97
|
+
test_context = context.nil? ? {} : store(context) { memory }
|
|
98
|
+
|
|
99
|
+
case expression
|
|
100
|
+
when Dentaku::AST::Node
|
|
101
|
+
expression.dependencies(test_context)
|
|
102
|
+
when Array
|
|
103
|
+
expression.flat_map { |e| dependencies(e, context) }
|
|
104
|
+
else
|
|
105
|
+
ast(expression).dependencies(test_context)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ast(expression)
|
|
110
|
+
return expression if expression.is_a?(AST::Node)
|
|
111
|
+
return expression.map { |e| ast(e) } if expression.is_a? Array
|
|
112
|
+
|
|
113
|
+
@ast_cache.fetch(expression) {
|
|
114
|
+
options = {
|
|
115
|
+
aliases: aliases,
|
|
116
|
+
case_sensitive: case_sensitive,
|
|
117
|
+
function_registry: @function_registry,
|
|
118
|
+
raw_date_literals: raw_date_literals
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
tokens = tokenizer.tokenize(expression, options)
|
|
122
|
+
Parser.new(tokens, options).parse.tap do |node|
|
|
123
|
+
@ast_cache[expression] = node if cache_ast?
|
|
124
|
+
end
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def load_cache(ast_cache)
|
|
129
|
+
@ast_cache = ast_cache
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def clear_cache(pattern = :all)
|
|
133
|
+
case pattern
|
|
134
|
+
when :all
|
|
135
|
+
@ast_cache = {}
|
|
136
|
+
when String
|
|
137
|
+
@ast_cache.delete(pattern)
|
|
138
|
+
when Regexp
|
|
139
|
+
@ast_cache.delete_if { |k, _| k =~ pattern }
|
|
140
|
+
else
|
|
141
|
+
raise ::ArgumentError
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def evaluation_context(data, evaluation_mode)
|
|
146
|
+
data.key?(:__evaluation_mode) ? data : data.merge(__evaluation_mode: evaluation_mode)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def store(key_or_hash, value = nil)
|
|
150
|
+
restore = Hash[memory]
|
|
151
|
+
|
|
152
|
+
if value.nil?
|
|
153
|
+
key_or_hash = FlatHash.from_hash_with_intermediates(key_or_hash) if nested_data_support
|
|
154
|
+
key_or_hash.each do |key, val|
|
|
155
|
+
memory[standardize_case(key.to_s)] = val
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
memory[standardize_case(key_or_hash.to_s)] = value
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if block_given?
|
|
162
|
+
begin
|
|
163
|
+
result = yield
|
|
164
|
+
@memory = restore
|
|
165
|
+
return result
|
|
166
|
+
rescue => e
|
|
167
|
+
@memory = restore
|
|
168
|
+
raise e
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
self
|
|
173
|
+
end
|
|
174
|
+
alias_method :bind, :store
|
|
175
|
+
|
|
176
|
+
def store_formula(key, formula)
|
|
177
|
+
store(key, ast(formula))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def clear
|
|
181
|
+
@memory = {}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def empty?
|
|
185
|
+
memory.empty?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def cache_ast?
|
|
189
|
+
Dentaku.cache_ast? && !@disable_ast_cache
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Dentaku
|
|
2
|
+
class DateArithmetic
|
|
3
|
+
def initialize(date)
|
|
4
|
+
if date.respond_to?(:strftime)
|
|
5
|
+
@base = date
|
|
6
|
+
else
|
|
7
|
+
@base = Time.parse(date).to_datetime
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(duration)
|
|
12
|
+
case duration
|
|
13
|
+
when Numeric
|
|
14
|
+
@base + duration
|
|
15
|
+
when Dentaku::AST::Duration::Value
|
|
16
|
+
case @base
|
|
17
|
+
when Time
|
|
18
|
+
change_datetime(@base.to_datetime, duration.unit, duration.value).to_time
|
|
19
|
+
else
|
|
20
|
+
change_datetime(@base, duration.unit, duration.value)
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
|
24
|
+
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sub(duration)
|
|
29
|
+
case duration
|
|
30
|
+
when Date, DateTime, Numeric, Time
|
|
31
|
+
@base - duration
|
|
32
|
+
when Dentaku::AST::Duration::Value
|
|
33
|
+
case @base
|
|
34
|
+
when Time
|
|
35
|
+
change_datetime(@base.to_datetime, duration.unit, -duration.value).to_time
|
|
36
|
+
else
|
|
37
|
+
change_datetime(@base, duration.unit, -duration.value)
|
|
38
|
+
end
|
|
39
|
+
when Dentaku::TokenScanner::DATE_TIME_REGEXP
|
|
40
|
+
@base - Time.parse(duration).to_datetime
|
|
41
|
+
else
|
|
42
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
|
43
|
+
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def change_datetime(base, unit, value)
|
|
50
|
+
case unit
|
|
51
|
+
when :year
|
|
52
|
+
base >> (value * 12)
|
|
53
|
+
when :month
|
|
54
|
+
base >> value
|
|
55
|
+
when :day
|
|
56
|
+
base + value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'tsort'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
class DependencyResolver
|
|
5
|
+
include TSort
|
|
6
|
+
|
|
7
|
+
def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
|
|
8
|
+
self.new(vars_to_dependencies_hash).sort
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(vars_to_dependencies_hash)
|
|
12
|
+
@key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [k.downcase, k] }]
|
|
13
|
+
# ensure variables are normalized strings
|
|
14
|
+
@vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.downcase.to_s, v] }]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sort
|
|
18
|
+
tsort.map { |k| @key_mapping.fetch(k, k) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def tsort_each_node(&block)
|
|
22
|
+
@vars_to_deps.each_key(&block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def tsort_each_child(node, &block)
|
|
26
|
+
@vars_to_deps.fetch(node.to_s, []).each(&block)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Dentaku
|
|
2
|
+
class Error < StandardError
|
|
3
|
+
attr_accessor :recipient_variable
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class UnboundVariableError < Error
|
|
7
|
+
attr_reader :unbound_variables
|
|
8
|
+
|
|
9
|
+
def initialize(unbound_variables)
|
|
10
|
+
@unbound_variables = unbound_variables
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class MathDomainError < Error
|
|
15
|
+
attr_reader :function_name, :args
|
|
16
|
+
|
|
17
|
+
def initialize(function_name, args)
|
|
18
|
+
@function_name = function_name
|
|
19
|
+
@args = args
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class NodeError < Error
|
|
24
|
+
attr_reader :child, :expect, :actual
|
|
25
|
+
|
|
26
|
+
def initialize(expect, actual, child)
|
|
27
|
+
@expect = Array(expect)
|
|
28
|
+
@actual = actual
|
|
29
|
+
@child = child
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class ParseError < Error
|
|
34
|
+
attr_reader :reason, :meta
|
|
35
|
+
|
|
36
|
+
def initialize(reason, **meta)
|
|
37
|
+
@reason = reason
|
|
38
|
+
@meta = meta
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private_class_method :new
|
|
42
|
+
|
|
43
|
+
VALID_REASONS = %i[
|
|
44
|
+
node_invalid too_few_operands too_many_operands undefined_function
|
|
45
|
+
unprocessed_token unknown_case_token unbalanced_bracket
|
|
46
|
+
unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
|
|
47
|
+
invalid_statement
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
def self.for(reason, **meta)
|
|
51
|
+
unless VALID_REASONS.include?(reason)
|
|
52
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
new(reason, **meta)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class TokenizerError < Error
|
|
60
|
+
attr_reader :reason, :meta
|
|
61
|
+
|
|
62
|
+
def initialize(reason, **meta)
|
|
63
|
+
@reason = reason
|
|
64
|
+
@meta = meta
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private_class_method :new
|
|
68
|
+
|
|
69
|
+
VALID_REASONS = %i[
|
|
70
|
+
parse_error
|
|
71
|
+
too_many_closing_parentheses
|
|
72
|
+
too_many_opening_parentheses
|
|
73
|
+
unexpected_zero_width_match
|
|
74
|
+
].freeze
|
|
75
|
+
|
|
76
|
+
def self.for(reason, **meta)
|
|
77
|
+
unless VALID_REASONS.include?(reason)
|
|
78
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
new(reason, **meta)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class ArgumentError < ::ArgumentError
|
|
86
|
+
attr_reader :reason, :meta
|
|
87
|
+
attr_accessor :recipient_variable
|
|
88
|
+
|
|
89
|
+
def initialize(reason, **meta)
|
|
90
|
+
@reason = reason
|
|
91
|
+
@meta = meta
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private_class_method :new
|
|
95
|
+
|
|
96
|
+
VALID_REASONS = %i[
|
|
97
|
+
incompatible_type
|
|
98
|
+
invalid_operator
|
|
99
|
+
invalid_value
|
|
100
|
+
too_few_arguments
|
|
101
|
+
wrong_number_of_arguments
|
|
102
|
+
].freeze
|
|
103
|
+
|
|
104
|
+
def self.for(reason, **meta)
|
|
105
|
+
unless VALID_REASONS.include?(reason)
|
|
106
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
new(reason, **meta)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class ZeroDivisionError < ::ZeroDivisionError
|
|
114
|
+
attr_accessor :recipient_variable
|
|
115
|
+
end
|
|
116
|
+
end
|