dentaku 3.0.0 → 3.1.0
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/.rubocop.yml +119 -0
- data/.travis.yml +8 -9
- data/CHANGELOG.md +9 -0
- data/Gemfile +0 -5
- data/LICENSE +21 -0
- data/README.md +44 -3
- data/Rakefile +4 -1
- data/dentaku.gemspec +8 -4
- data/lib/dentaku.rb +15 -2
- data/lib/dentaku/ast.rb +1 -0
- data/lib/dentaku/ast/access.rb +2 -2
- data/lib/dentaku/ast/arithmetic.rb +7 -7
- data/lib/dentaku/ast/bitwise.rb +2 -2
- data/lib/dentaku/ast/case.rb +5 -5
- data/lib/dentaku/ast/case/case_conditional.rb +1 -1
- data/lib/dentaku/ast/case/case_else.rb +2 -2
- data/lib/dentaku/ast/case/case_switch_variable.rb +2 -2
- data/lib/dentaku/ast/case/case_then.rb +2 -2
- data/lib/dentaku/ast/case/case_when.rb +2 -2
- data/lib/dentaku/ast/combinators.rb +10 -2
- data/lib/dentaku/ast/comparators.rb +34 -6
- data/lib/dentaku/ast/function.rb +1 -1
- data/lib/dentaku/ast/function_registry.rb +1 -1
- data/lib/dentaku/ast/functions/if.rb +6 -2
- data/lib/dentaku/ast/functions/max.rb +1 -1
- data/lib/dentaku/ast/functions/min.rb +1 -1
- data/lib/dentaku/ast/functions/ruby_math.rb +1 -1
- data/lib/dentaku/ast/functions/string_functions.rb +8 -8
- data/lib/dentaku/ast/functions/sum.rb +12 -0
- data/lib/dentaku/ast/grouping.rb +2 -2
- data/lib/dentaku/ast/identifier.rb +8 -5
- data/lib/dentaku/ast/negation.rb +2 -2
- data/lib/dentaku/ast/node.rb +1 -1
- data/lib/dentaku/ast/operation.rb +1 -1
- data/lib/dentaku/bulk_expression_solver.rb +39 -20
- data/lib/dentaku/calculator.rb +38 -28
- data/lib/dentaku/dependency_resolver.rb +1 -1
- data/lib/dentaku/flat_hash.rb +31 -0
- data/lib/dentaku/parser.rb +7 -6
- data/lib/dentaku/string_casing.rb +7 -0
- data/lib/dentaku/token.rb +1 -1
- data/lib/dentaku/token_matcher.rb +4 -4
- data/lib/dentaku/token_scanner.rb +18 -7
- data/lib/dentaku/tokenizer.rb +26 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +2 -2
- data/spec/ast/comparator_spec.rb +57 -0
- data/spec/ast/function_spec.rb +1 -1
- data/spec/ast/max_spec.rb +5 -0
- data/spec/ast/min_spec.rb +5 -0
- data/spec/ast/sum_spec.rb +38 -0
- data/spec/benchmark.rb +2 -2
- data/spec/bulk_expression_solver_spec.rb +89 -1
- data/spec/calculator_spec.rb +40 -7
- data/spec/dentaku_spec.rb +11 -0
- data/spec/external_function_spec.rb +7 -7
- data/spec/parser_spec.rb +11 -11
- data/spec/spec_helper.rb +21 -3
- data/spec/token_matcher_spec.rb +0 -1
- data/spec/token_spec.rb +6 -0
- data/spec/tokenizer_spec.rb +37 -0
- metadata +70 -5
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'dentaku/dependency_resolver'
|
2
2
|
require 'dentaku/exceptions'
|
3
|
+
require 'dentaku/flat_hash'
|
3
4
|
require 'dentaku/parser'
|
4
5
|
require 'dentaku/tokenizer'
|
5
6
|
|
6
7
|
module Dentaku
|
7
8
|
class BulkExpressionSolver
|
8
|
-
def initialize(
|
9
|
-
|
10
|
-
|
9
|
+
def initialize(expressions, calculator)
|
10
|
+
@expression_hash = FlatHash.from_hash(expressions)
|
11
|
+
@calculator = calculator
|
11
12
|
end
|
12
13
|
|
13
14
|
def solve!
|
@@ -18,8 +19,19 @@ module Dentaku
|
|
18
19
|
error_handler = block || return_undefined_handler
|
19
20
|
results = load_results(&error_handler)
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
FlatHash.expand(
|
23
|
+
expression_hash.each_with_object({}) do |(k, _), r|
|
24
|
+
r[k] = results[k.to_s]
|
25
|
+
end
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def dependencies
|
30
|
+
Hash[expression_deps].tap do |d|
|
31
|
+
d.values.each do |deps|
|
32
|
+
unresolved = deps.reject { |ud| d.has_key?(ud) }
|
33
|
+
unresolved.each { |u| add_dependencies(d, u) }
|
34
|
+
end
|
23
35
|
end
|
24
36
|
end
|
25
37
|
|
@@ -29,7 +41,7 @@ module Dentaku
|
|
29
41
|
@dep_cache ||= {}
|
30
42
|
end
|
31
43
|
|
32
|
-
|
44
|
+
attr_reader :expression_hash, :calculator
|
33
45
|
|
34
46
|
def return_undefined_handler
|
35
47
|
->(*) { :undefined }
|
@@ -39,38 +51,45 @@ module Dentaku
|
|
39
51
|
->(ex) { raise ex }
|
40
52
|
end
|
41
53
|
|
54
|
+
def expression_with_exception_handler(&block)
|
55
|
+
->(expr, ex) { block.call(ex) }
|
56
|
+
end
|
57
|
+
|
42
58
|
def load_results(&block)
|
43
59
|
variables_in_resolve_order.each_with_object({}) do |var_name, r|
|
44
60
|
begin
|
45
|
-
|
61
|
+
solved = calculator.memory
|
62
|
+
value_from_memory = solved[var_name.downcase]
|
46
63
|
|
47
64
|
if value_from_memory.nil? &&
|
48
65
|
expressions[var_name].nil? &&
|
49
|
-
!
|
66
|
+
!solved.has_key?(var_name)
|
50
67
|
next
|
51
68
|
end
|
52
69
|
|
53
|
-
value = value_from_memory ||
|
54
|
-
|
70
|
+
value = value_from_memory || evaluate!(
|
71
|
+
expressions[var_name],
|
72
|
+
expressions.merge(r).merge(solved),
|
73
|
+
&expression_with_exception_handler(&block)
|
74
|
+
)
|
55
75
|
|
56
76
|
r[var_name] = value
|
57
77
|
rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
|
58
78
|
ex.recipient_variable = var_name
|
59
79
|
r[var_name] = block.call(ex)
|
80
|
+
rescue Dentaku::ArgumentError => ex
|
81
|
+
r[var_name] = block.call(ex)
|
60
82
|
end
|
61
83
|
end
|
62
84
|
end
|
63
85
|
|
64
86
|
def expressions
|
65
|
-
@expressions ||= Hash[expression_hash.map { |k,v| [k.to_s, v] }]
|
87
|
+
@expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
|
66
88
|
end
|
67
89
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
unresolved = deps.reject { |ud| d.has_key?(ud) }
|
72
|
-
unresolved.each { |u| add_dependencies(d, u) }
|
73
|
-
end
|
90
|
+
def expression_deps
|
91
|
+
expressions.map do |var, expr|
|
92
|
+
[var, calculator.dependencies(expr)]
|
74
93
|
end
|
75
94
|
end
|
76
95
|
|
@@ -85,14 +104,14 @@ module Dentaku
|
|
85
104
|
def variables_in_resolve_order
|
86
105
|
cache_key = expressions.keys.map(&:to_s).sort.join("|")
|
87
106
|
@ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
|
88
|
-
DependencyResolver.find_resolve_order(
|
107
|
+
DependencyResolver.find_resolve_order(dependencies).tap do |d|
|
89
108
|
self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
|
90
109
|
end
|
91
110
|
}
|
92
111
|
end
|
93
112
|
|
94
|
-
def evaluate!(expression, results)
|
95
|
-
calculator.evaluate!(expression, results)
|
113
|
+
def evaluate!(expression, results, &block)
|
114
|
+
calculator.evaluate!(expression, results, &block)
|
96
115
|
end
|
97
116
|
end
|
98
117
|
end
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -1,18 +1,24 @@
|
|
1
1
|
require 'dentaku/bulk_expression_solver'
|
2
|
-
require 'dentaku/exceptions'
|
3
|
-
require 'dentaku/token'
|
4
2
|
require 'dentaku/dependency_resolver'
|
3
|
+
require 'dentaku/exceptions'
|
4
|
+
require 'dentaku/flat_hash'
|
5
5
|
require 'dentaku/parser'
|
6
|
-
|
6
|
+
require 'dentaku/string_casing'
|
7
|
+
require 'dentaku/token'
|
7
8
|
|
8
9
|
module Dentaku
|
9
10
|
class Calculator
|
10
|
-
|
11
|
+
include StringCasing
|
12
|
+
attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases, :nested_data_support
|
11
13
|
|
12
|
-
def initialize(
|
14
|
+
def initialize(options = {})
|
13
15
|
clear
|
14
16
|
@tokenizer = Tokenizer.new
|
15
|
-
@
|
17
|
+
@case_sensitive = options.delete(:case_sensitive)
|
18
|
+
@aliases = options.delete(:aliases) || Dentaku.aliases
|
19
|
+
@nested_data_support = options.fetch(:nested_data_support, true)
|
20
|
+
options.delete(:nested_data_support)
|
21
|
+
@ast_cache = options
|
16
22
|
@disable_ast_cache = false
|
17
23
|
@function_registry = Dentaku::AST::FunctionRegistry.new
|
18
24
|
end
|
@@ -38,13 +44,17 @@ module Dentaku
|
|
38
44
|
@disable_ast_cache = false
|
39
45
|
end
|
40
46
|
|
41
|
-
def evaluate(expression, data={})
|
47
|
+
def evaluate(expression, data = {}, &block)
|
42
48
|
evaluate!(expression, data)
|
43
|
-
rescue UnboundVariableError, Dentaku::ArgumentError
|
44
|
-
|
49
|
+
rescue UnboundVariableError, Dentaku::ArgumentError => ex
|
50
|
+
block.call(expression, ex) if block_given?
|
45
51
|
end
|
46
52
|
|
47
|
-
def evaluate!(expression, data={})
|
53
|
+
def evaluate!(expression, data = {}, &block)
|
54
|
+
return expression.map { |e|
|
55
|
+
evaluate(e, data, &block)
|
56
|
+
} if expression.is_a? Array
|
57
|
+
|
48
58
|
store(data) do
|
49
59
|
node = expression
|
50
60
|
node = ast(node) unless node.is_a?(AST::Node)
|
@@ -66,39 +76,50 @@ module Dentaku
|
|
66
76
|
end
|
67
77
|
|
68
78
|
def dependencies(expression)
|
79
|
+
if expression.is_a? Array
|
80
|
+
return expression.flat_map { |e| dependencies(e) }
|
81
|
+
end
|
69
82
|
ast(expression).dependencies(memory)
|
70
83
|
end
|
71
84
|
|
72
85
|
def ast(expression)
|
73
86
|
@ast_cache.fetch(expression) {
|
74
|
-
|
87
|
+
options = {
|
88
|
+
case_sensitive: case_sensitive,
|
89
|
+
function_registry: @function_registry,
|
90
|
+
aliases: aliases
|
91
|
+
}
|
92
|
+
|
93
|
+
tokens = tokenizer.tokenize(expression, options)
|
94
|
+
Parser.new(tokens, options).parse.tap do |node|
|
75
95
|
@ast_cache[expression] = node if cache_ast?
|
76
96
|
end
|
77
97
|
}
|
78
98
|
end
|
79
99
|
|
80
|
-
def clear_cache(pattern
|
100
|
+
def clear_cache(pattern = :all)
|
81
101
|
case pattern
|
82
102
|
when :all
|
83
103
|
@ast_cache = {}
|
84
104
|
when String
|
85
105
|
@ast_cache.delete(pattern)
|
86
106
|
when Regexp
|
87
|
-
@ast_cache.delete_if { |k,_| k =~ pattern }
|
107
|
+
@ast_cache.delete_if { |k, _| k =~ pattern }
|
88
108
|
else
|
89
109
|
raise ::ArgumentError
|
90
110
|
end
|
91
111
|
end
|
92
112
|
|
93
|
-
def store(key_or_hash, value=nil)
|
113
|
+
def store(key_or_hash, value = nil)
|
94
114
|
restore = Hash[memory]
|
95
115
|
|
96
116
|
if value.nil?
|
97
|
-
|
98
|
-
|
117
|
+
key_or_hash = FlatHash.from_hash(key_or_hash) if nested_data_support
|
118
|
+
key_or_hash.each do |key, val|
|
119
|
+
memory[standardize_case(key.to_s)] = val
|
99
120
|
end
|
100
121
|
else
|
101
|
-
memory[key_or_hash.to_s
|
122
|
+
memory[standardize_case(key_or_hash.to_s)] = value
|
102
123
|
end
|
103
124
|
|
104
125
|
if block_given?
|
@@ -131,16 +152,5 @@ module Dentaku
|
|
131
152
|
def cache_ast?
|
132
153
|
Dentaku.cache_ast? && !@disable_ast_cache
|
133
154
|
end
|
134
|
-
|
135
|
-
private
|
136
|
-
|
137
|
-
def _flat_hash(hash, k = [])
|
138
|
-
if hash.is_a?(Hash)
|
139
|
-
hash.inject({}) { |h, v| h.merge! _flat_hash(v[-1], k + [v[0]]) }
|
140
|
-
else
|
141
|
-
return { k.join('.') => hash } if k.is_a?(Array)
|
142
|
-
{ k => hash }
|
143
|
-
end
|
144
|
-
end
|
145
155
|
end
|
146
156
|
end
|
@@ -10,7 +10,7 @@ module Dentaku
|
|
10
10
|
|
11
11
|
def initialize(vars_to_dependencies_hash)
|
12
12
|
# ensure variables are strings
|
13
|
-
@vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_s, v]}]
|
13
|
+
@vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_s, v] }]
|
14
14
|
end
|
15
15
|
|
16
16
|
def tsort_each_node(&block)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dentaku
|
2
|
+
class FlatHash
|
3
|
+
def self.from_hash(h, key = [], acc = {})
|
4
|
+
return acc.update(key => h) unless h.is_a? Hash
|
5
|
+
h.each { |k, v| from_hash(v, key + [k], acc) }
|
6
|
+
flatten_keys(acc)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.flatten_keys(hash)
|
10
|
+
hash.each_with_object({}) do |(k, v), h|
|
11
|
+
h[flatten_key(k)] = v
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.flatten_key(segments)
|
16
|
+
return segments.first if segments.length == 1
|
17
|
+
key = segments.join('.')
|
18
|
+
key = key.to_sym if segments.first.is_a?(Symbol)
|
19
|
+
key
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.expand(h)
|
23
|
+
h.each_with_object({}) do |(k, v), r|
|
24
|
+
hash_levels = k.to_s.split('.')
|
25
|
+
hash_levels = hash_levels.map(&:to_sym) if k.is_a?(Symbol)
|
26
|
+
child_hash = hash_levels[0...-1].reduce(r) { |h, n| h[n] ||= {} }
|
27
|
+
child_hash[hash_levels.last] = v
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/dentaku/parser.rb
CHANGED
@@ -2,17 +2,18 @@ require_relative './ast'
|
|
2
2
|
|
3
3
|
module Dentaku
|
4
4
|
class Parser
|
5
|
-
attr_reader :input, :output, :operations, :arities
|
5
|
+
attr_reader :input, :output, :operations, :arities, :case_sensitive
|
6
6
|
|
7
|
-
def initialize(tokens, options={})
|
8
|
-
@input
|
9
|
-
@output
|
7
|
+
def initialize(tokens, options = {})
|
8
|
+
@input = tokens.dup
|
9
|
+
@output = []
|
10
10
|
@operations = options.fetch(:operations, [])
|
11
11
|
@arities = options.fetch(:arities, [])
|
12
12
|
@function_registry = options.fetch(:function_registry, nil)
|
13
|
+
@case_sensitive = options.fetch(:case_sensitive, false)
|
13
14
|
end
|
14
15
|
|
15
|
-
def consume(count=2)
|
16
|
+
def consume(count = 2)
|
16
17
|
operator = operations.pop
|
17
18
|
operator.peek(output)
|
18
19
|
|
@@ -45,7 +46,7 @@ module Dentaku
|
|
45
46
|
output.push AST::String.new(token)
|
46
47
|
|
47
48
|
when :identifier
|
48
|
-
output.push AST::Identifier.new(token)
|
49
|
+
output.push AST::Identifier.new(token, case_sensitive: case_sensitive)
|
49
50
|
|
50
51
|
when :operator, :comparator, :combinator
|
51
52
|
op_class = operation(token)
|
data/lib/dentaku/token.rb
CHANGED
@@ -4,10 +4,10 @@ module Dentaku
|
|
4
4
|
class TokenMatcher
|
5
5
|
attr_reader :children, :categories, :values
|
6
6
|
|
7
|
-
def initialize(categories=nil, values=nil, children=[])
|
7
|
+
def initialize(categories = nil, values = nil, children = [])
|
8
8
|
# store categories and values as hash to optimize key lookup, h/t @jan-mangs
|
9
|
-
@categories = [categories].compact.flatten.each_with_object({}) { |c,h| h[c] = 1 }
|
10
|
-
@values = [values].compact.flatten.each_with_object({}) { |v,h| h[v] = 1 }
|
9
|
+
@categories = [categories].compact.flatten.each_with_object({}) { |c, h| h[c] = 1 }
|
10
|
+
@values = [values].compact.flatten.each_with_object({}) { |v, h| h[v] = 1 }
|
11
11
|
@children = children.compact
|
12
12
|
@invert = false
|
13
13
|
|
@@ -29,7 +29,7 @@ module Dentaku
|
|
29
29
|
leaf_matcher? ? matches_token?(token) : any_child_matches_token?(token)
|
30
30
|
end
|
31
31
|
|
32
|
-
def match(token_stream, offset=0)
|
32
|
+
def match(token_stream, offset = 0)
|
33
33
|
matched_tokens = []
|
34
34
|
matched = false
|
35
35
|
|
@@ -1,17 +1,20 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
require 'time'
|
3
|
+
require 'dentaku/string_casing'
|
3
4
|
require 'dentaku/token'
|
4
5
|
|
5
6
|
module Dentaku
|
6
7
|
class TokenScanner
|
7
|
-
|
8
|
+
extend StringCasing
|
9
|
+
|
10
|
+
def initialize(category, regexp, converter = nil, condition = nil)
|
8
11
|
@category = category
|
9
12
|
@regexp = %r{\A(#{ regexp })}i
|
10
13
|
@converter = converter
|
11
14
|
@condition = condition || ->(*) { true }
|
12
15
|
end
|
13
16
|
|
14
|
-
def scan(string, last_token=nil)
|
17
|
+
def scan(string, last_token = nil)
|
15
18
|
if (m = @regexp.match(string)) && @condition.call(last_token)
|
16
19
|
value = raw = m.to_s
|
17
20
|
value = @converter.call(raw) if @converter
|
@@ -25,6 +28,8 @@ module Dentaku
|
|
25
28
|
end
|
26
29
|
|
27
30
|
class << self
|
31
|
+
attr_reader :case_sensitive
|
32
|
+
|
28
33
|
def available_scanners
|
29
34
|
[
|
30
35
|
:null,
|
@@ -62,10 +67,11 @@ module Dentaku
|
|
62
67
|
end
|
63
68
|
|
64
69
|
def scanners=(scanner_ids)
|
65
|
-
@scanners.select! { |k,v| scanner_ids.include?(k) }
|
70
|
+
@scanners.select! { |k, v| scanner_ids.include?(k) }
|
66
71
|
end
|
67
72
|
|
68
|
-
def scanners
|
73
|
+
def scanners(options = {})
|
74
|
+
@case_sensitive = options.fetch(:case_sensitive, false)
|
69
75
|
@scanners.values
|
70
76
|
end
|
71
77
|
|
@@ -112,7 +118,9 @@ module Dentaku
|
|
112
118
|
end
|
113
119
|
|
114
120
|
def operator
|
115
|
-
names = {
|
121
|
+
names = {
|
122
|
+
pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&'
|
123
|
+
}.invert
|
116
124
|
new(:operator, '\^|\+|-|\*|\/|%|\||&', lambda { |raw| names[raw] })
|
117
125
|
end
|
118
126
|
|
@@ -152,12 +160,15 @@ module Dentaku
|
|
152
160
|
def function
|
153
161
|
new(:function, '\w+!?\s*\(', lambda do |raw|
|
154
162
|
function_name = raw.gsub('(', '')
|
155
|
-
[
|
163
|
+
[
|
164
|
+
Token.new(:function, function_name.strip.downcase.to_sym, function_name),
|
165
|
+
Token.new(:grouping, :open, '(')
|
166
|
+
]
|
156
167
|
end)
|
157
168
|
end
|
158
169
|
|
159
170
|
def identifier
|
160
|
-
new(:identifier, '[\w\.]+\b', lambda { |raw| raw.strip
|
171
|
+
new(:identifier, '[\w\.]+\b', lambda { |raw| standardize_case(raw.strip) })
|
161
172
|
end
|
162
173
|
end
|
163
174
|
|