dentaku 3.0.0 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|