dentaku 3.2.0 → 3.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +5 -10
- data/.travis.yml +4 -6
- data/CHANGELOG.md +86 -2
- data/README.md +7 -6
- data/dentaku.gemspec +1 -1
- data/lib/dentaku/ast/access.rb +21 -1
- data/lib/dentaku/ast/arithmetic.rb +51 -15
- data/lib/dentaku/ast/array.rb +41 -0
- data/lib/dentaku/ast/bitwise.rb +30 -5
- data/lib/dentaku/ast/case/case_conditional.rb +17 -2
- data/lib/dentaku/ast/case/case_else.rb +17 -3
- data/lib/dentaku/ast/case/case_switch_variable.rb +14 -0
- data/lib/dentaku/ast/case/case_then.rb +17 -3
- data/lib/dentaku/ast/case/case_when.rb +21 -3
- data/lib/dentaku/ast/case.rb +19 -3
- data/lib/dentaku/ast/comparators.rb +38 -28
- data/lib/dentaku/ast/function.rb +11 -3
- data/lib/dentaku/ast/function_registry.rb +21 -0
- data/lib/dentaku/ast/functions/all.rb +23 -0
- data/lib/dentaku/ast/functions/and.rb +2 -2
- data/lib/dentaku/ast/functions/any.rb +23 -0
- data/lib/dentaku/ast/functions/avg.rb +2 -2
- data/lib/dentaku/ast/functions/count.rb +8 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/enum.rb +37 -0
- data/lib/dentaku/ast/functions/filter.rb +23 -0
- data/lib/dentaku/ast/functions/if.rb +19 -2
- data/lib/dentaku/ast/functions/map.rb +23 -0
- data/lib/dentaku/ast/functions/or.rb +4 -4
- data/lib/dentaku/ast/functions/pluck.rb +30 -0
- data/lib/dentaku/ast/functions/round.rb +1 -1
- data/lib/dentaku/ast/functions/rounddown.rb +1 -1
- data/lib/dentaku/ast/functions/roundup.rb +1 -1
- data/lib/dentaku/ast/functions/ruby_math.rb +50 -3
- data/lib/dentaku/ast/functions/string_functions.rb +105 -12
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/grouping.rb +3 -1
- data/lib/dentaku/ast/identifier.rb +16 -4
- data/lib/dentaku/ast/literal.rb +10 -0
- data/lib/dentaku/ast/negation.rb +7 -1
- data/lib/dentaku/ast/nil.rb +4 -0
- data/lib/dentaku/ast/node.rb +8 -0
- data/lib/dentaku/ast/operation.rb +17 -0
- data/lib/dentaku/ast/string.rb +7 -0
- data/lib/dentaku/ast.rb +8 -0
- data/lib/dentaku/bulk_expression_solver.rb +38 -27
- data/lib/dentaku/calculator.rb +21 -8
- data/lib/dentaku/date_arithmetic.rb +45 -0
- data/lib/dentaku/exceptions.rb +11 -8
- data/lib/dentaku/flat_hash.rb +9 -2
- data/lib/dentaku/parser.rb +57 -16
- data/lib/dentaku/print_visitor.rb +101 -0
- data/lib/dentaku/token_matcher.rb +1 -1
- data/lib/dentaku/token_scanner.rb +9 -3
- data/lib/dentaku/tokenizer.rb +7 -2
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +82 -0
- data/lib/dentaku.rb +20 -7
- data/spec/ast/addition_spec.rb +7 -1
- data/spec/ast/all_spec.rb +25 -0
- data/spec/ast/and_function_spec.rb +6 -6
- data/spec/ast/and_spec.rb +1 -1
- data/spec/ast/any_spec.rb +23 -0
- data/spec/ast/arithmetic_spec.rb +64 -29
- data/spec/ast/avg_spec.rb +9 -5
- data/spec/ast/comparator_spec.rb +31 -1
- data/spec/ast/count_spec.rb +7 -7
- data/spec/ast/division_spec.rb +7 -1
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +20 -15
- data/spec/ast/map_spec.rb +27 -0
- data/spec/ast/max_spec.rb +16 -3
- data/spec/ast/min_spec.rb +16 -3
- data/spec/ast/mul_spec.rb +11 -6
- data/spec/ast/negation_spec.rb +48 -0
- data/spec/ast/node_spec.rb +11 -8
- data/spec/ast/numeric_spec.rb +1 -1
- data/spec/ast/or_spec.rb +7 -7
- data/spec/ast/pluck_spec.rb +32 -0
- data/spec/ast/round_spec.rb +14 -4
- data/spec/ast/rounddown_spec.rb +14 -4
- data/spec/ast/roundup_spec.rb +14 -4
- data/spec/ast/string_functions_spec.rb +73 -0
- data/spec/ast/sum_spec.rb +11 -6
- data/spec/ast/switch_spec.rb +5 -5
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/bulk_expression_solver_spec.rb +37 -1
- data/spec/calculator_spec.rb +341 -32
- data/spec/dentaku_spec.rb +19 -6
- data/spec/external_function_spec.rb +32 -6
- data/spec/parser_spec.rb +100 -123
- data/spec/print_visitor_spec.rb +66 -0
- data/spec/spec_helper.rb +6 -4
- data/spec/token_matcher_spec.rb +8 -8
- data/spec/token_scanner_spec.rb +4 -4
- data/spec/tokenizer_spec.rb +56 -13
- data/spec/visitor/infix_spec.rb +31 -0
- data/spec/visitor_spec.rb +138 -0
- metadata +52 -7
data/lib/dentaku/exceptions.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
module Dentaku
|
2
|
-
class
|
2
|
+
class Error < StandardError
|
3
3
|
attr_accessor :recipient_variable
|
4
|
+
end
|
4
5
|
|
6
|
+
class UnboundVariableError < Error
|
5
7
|
attr_reader :unbound_variables
|
6
8
|
|
7
9
|
def initialize(unbound_variables)
|
@@ -9,7 +11,7 @@ module Dentaku
|
|
9
11
|
end
|
10
12
|
end
|
11
13
|
|
12
|
-
class NodeError <
|
14
|
+
class NodeError < Error
|
13
15
|
attr_reader :child, :expect, :actual
|
14
16
|
|
15
17
|
def initialize(expect, actual, child)
|
@@ -19,7 +21,7 @@ module Dentaku
|
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
22
|
-
class ParseError <
|
24
|
+
class ParseError < Error
|
23
25
|
attr_reader :reason, :meta
|
24
26
|
|
25
27
|
def initialize(reason, **meta)
|
@@ -30,7 +32,7 @@ module Dentaku
|
|
30
32
|
private_class_method :new
|
31
33
|
|
32
34
|
VALID_REASONS = %i[
|
33
|
-
node_invalid too_few_operands undefined_function
|
35
|
+
node_invalid too_few_operands too_many_operands undefined_function
|
34
36
|
unprocessed_token unknown_case_token unbalanced_bracket
|
35
37
|
unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
|
36
38
|
invalid_statement
|
@@ -41,11 +43,11 @@ module Dentaku
|
|
41
43
|
raise ::ArgumentError, "Unhandled #{reason}"
|
42
44
|
end
|
43
45
|
|
44
|
-
new
|
46
|
+
new(reason, **meta)
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
48
|
-
class TokenizerError <
|
50
|
+
class TokenizerError < Error
|
49
51
|
attr_reader :reason, :meta
|
50
52
|
|
51
53
|
def initialize(reason, **meta)
|
@@ -65,12 +67,13 @@ module Dentaku
|
|
65
67
|
raise ::ArgumentError, "Unhandled #{reason}"
|
66
68
|
end
|
67
69
|
|
68
|
-
new
|
70
|
+
new(reason, **meta)
|
69
71
|
end
|
70
72
|
end
|
71
73
|
|
72
74
|
class ArgumentError < ::ArgumentError
|
73
75
|
attr_reader :reason, :meta
|
76
|
+
attr_accessor :recipient_variable
|
74
77
|
|
75
78
|
def initialize(reason, **meta)
|
76
79
|
@reason = reason
|
@@ -89,7 +92,7 @@ module Dentaku
|
|
89
92
|
raise ::ArgumentError, "Unhandled #{reason}"
|
90
93
|
end
|
91
94
|
|
92
|
-
new
|
95
|
+
new(reason, **meta)
|
93
96
|
end
|
94
97
|
end
|
95
98
|
|
data/lib/dentaku/flat_hash.rb
CHANGED
@@ -6,6 +6,13 @@ module Dentaku
|
|
6
6
|
flatten_keys(acc)
|
7
7
|
end
|
8
8
|
|
9
|
+
def self.from_hash_with_intermediates(h, key = [], acc = {})
|
10
|
+
acc.update(key => h) unless key.empty?
|
11
|
+
return unless h.is_a? Hash
|
12
|
+
h.each { |k, v| from_hash_with_intermediates(v, key + [k], acc) }
|
13
|
+
flatten_keys(acc)
|
14
|
+
end
|
15
|
+
|
9
16
|
def self.flatten_keys(hash)
|
10
17
|
hash.each_with_object({}) do |(k, v), h|
|
11
18
|
h[flatten_key(k)] = v
|
@@ -19,8 +26,8 @@ module Dentaku
|
|
19
26
|
key
|
20
27
|
end
|
21
28
|
|
22
|
-
def self.expand(
|
23
|
-
|
29
|
+
def self.expand(hash)
|
30
|
+
hash.each_with_object({}) do |(k, v), r|
|
24
31
|
hash_levels = k.to_s.split('.')
|
25
32
|
hash_levels = hash_levels.map(&:to_sym) if k.is_a?(Symbol)
|
26
33
|
child_hash = hash_levels[0...-1].reduce(r) { |h, n| h[n] ||= {} }
|
data/lib/dentaku/parser.rb
CHANGED
@@ -10,8 +10,11 @@ module Dentaku
|
|
10
10
|
pow: AST::Exponentiation,
|
11
11
|
negate: AST::Negation,
|
12
12
|
mod: AST::Modulo,
|
13
|
+
|
13
14
|
bitor: AST::BitwiseOr,
|
14
15
|
bitand: AST::BitwiseAnd,
|
16
|
+
bitshiftleft: AST::BitwiseShiftLeft,
|
17
|
+
bitshiftright: AST::BitwiseShiftRight,
|
15
18
|
|
16
19
|
lt: AST::LessThan,
|
17
20
|
gt: AST::GreaterThan,
|
@@ -22,6 +25,7 @@ module Dentaku
|
|
22
25
|
|
23
26
|
and: AST::And,
|
24
27
|
or: AST::Or,
|
28
|
+
xor: AST::Xor,
|
25
29
|
}.freeze
|
26
30
|
|
27
31
|
attr_reader :input, :output, :operations, :arities, :case_sensitive
|
@@ -37,15 +41,28 @@ module Dentaku
|
|
37
41
|
|
38
42
|
def consume(count = 2)
|
39
43
|
operator = operations.pop
|
44
|
+
fail! :invalid_statement if operator.nil?
|
45
|
+
|
40
46
|
operator.peek(output)
|
41
47
|
|
42
48
|
args_size = operator.arity || count
|
43
|
-
|
44
|
-
|
49
|
+
min_size = operator.arity || operator.min_param_count || count
|
50
|
+
max_size = operator.arity || operator.max_param_count || count
|
51
|
+
|
52
|
+
if output.length < min_size || args_size < min_size
|
53
|
+
fail! :too_few_operands, operator: operator, expect: min_size, actual: output.length
|
54
|
+
end
|
55
|
+
|
56
|
+
if output.length > max_size && operations.empty? || args_size > max_size
|
57
|
+
fail! :too_many_operands, operator: operator, expect: max_size, actual: output.length
|
45
58
|
end
|
59
|
+
|
60
|
+
fail! :invalid_statement if output.size < args_size
|
46
61
|
args = Array.new(args_size) { output.pop }.reverse
|
47
62
|
|
48
63
|
output.push operator.new(*args)
|
64
|
+
rescue ::ArgumentError => e
|
65
|
+
raise Dentaku::ArgumentError, e.message
|
49
66
|
rescue NodeError => e
|
50
67
|
fail! :node_invalid, operator: operator, child: e.child, expect: e.expect, actual: e.actual
|
51
68
|
end
|
@@ -111,17 +128,19 @@ module Dentaku
|
|
111
128
|
open_cases = 0
|
112
129
|
case_end_index = nil
|
113
130
|
|
114
|
-
input.each_with_index do |
|
115
|
-
if
|
116
|
-
|
117
|
-
|
131
|
+
input.each_with_index do |input_token, index|
|
132
|
+
if input_token.category == :case
|
133
|
+
if input_token.value == :open
|
134
|
+
open_cases += 1
|
135
|
+
end
|
118
136
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
137
|
+
if input_token.value == :close
|
138
|
+
if open_cases > 0
|
139
|
+
open_cases -= 1
|
140
|
+
else
|
141
|
+
case_end_index = index
|
142
|
+
break
|
143
|
+
end
|
125
144
|
end
|
126
145
|
end
|
127
146
|
end
|
@@ -129,7 +148,9 @@ module Dentaku
|
|
129
148
|
subparser = Parser.new(
|
130
149
|
inner_case_inputs,
|
131
150
|
operations: [AST::Case],
|
132
|
-
arities: [0]
|
151
|
+
arities: [0],
|
152
|
+
function_registry: @function_registry,
|
153
|
+
case_sensitive: case_sensitive
|
133
154
|
)
|
134
155
|
subparser.parse
|
135
156
|
output.concat(subparser.output)
|
@@ -205,11 +226,28 @@ module Dentaku
|
|
205
226
|
end
|
206
227
|
|
207
228
|
unless operations.last == AST::Access
|
208
|
-
fail! :unbalanced_bracket, token
|
229
|
+
fail! :unbalanced_bracket, token: token
|
209
230
|
end
|
210
231
|
consume
|
211
232
|
end
|
212
233
|
|
234
|
+
when :array
|
235
|
+
case token.value
|
236
|
+
when :array_start
|
237
|
+
operations.push AST::Array
|
238
|
+
arities.push 0
|
239
|
+
when :array_end
|
240
|
+
while operations.any? && operations.last != AST::Array
|
241
|
+
consume
|
242
|
+
end
|
243
|
+
|
244
|
+
unless operations.last == AST::Array
|
245
|
+
fail! :unbalanced_bracket, token: token
|
246
|
+
end
|
247
|
+
|
248
|
+
consume(arities.pop.succ)
|
249
|
+
end
|
250
|
+
|
213
251
|
when :grouping
|
214
252
|
case token.value
|
215
253
|
when :open
|
@@ -236,8 +274,9 @@ module Dentaku
|
|
236
274
|
end
|
237
275
|
|
238
276
|
when :comma
|
277
|
+
fail! :invalid_statement if arities.empty?
|
239
278
|
arities[-1] += 1
|
240
|
-
while operations.any? && operations.last != AST::Grouping
|
279
|
+
while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
|
241
280
|
consume
|
242
281
|
end
|
243
282
|
|
@@ -282,6 +321,8 @@ module Dentaku
|
|
282
321
|
"#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
|
283
322
|
when :too_few_operands
|
284
323
|
"#{meta.fetch(:operator)} has too few operands"
|
324
|
+
when :too_many_operands
|
325
|
+
"#{meta.fetch(:operator)} has too many operands"
|
285
326
|
when :undefined_function
|
286
327
|
"Undefined function #{meta.fetch(:function_name)}"
|
287
328
|
when :unprocessed_token
|
@@ -302,7 +343,7 @@ module Dentaku
|
|
302
343
|
raise ::ArgumentError, "Unhandled #{reason}"
|
303
344
|
end
|
304
345
|
|
305
|
-
raise ParseError.for(reason, meta), message
|
346
|
+
raise ParseError.for(reason, **meta), message
|
306
347
|
end
|
307
348
|
end
|
308
349
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Dentaku
|
2
|
+
class PrintVisitor
|
3
|
+
def initialize(node)
|
4
|
+
@output = ''
|
5
|
+
node.accept(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def visit_operation(node)
|
9
|
+
if node.left
|
10
|
+
visit_operand(node.left, node.class.precedence, suffix: " ")
|
11
|
+
end
|
12
|
+
|
13
|
+
@output << node.display_operator
|
14
|
+
|
15
|
+
if node.right
|
16
|
+
visit_operand(node.right, node.class.precedence, prefix: " ")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit_operand(node, precedence, prefix: "", suffix: "")
|
21
|
+
@output << prefix
|
22
|
+
@output << "(" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
|
23
|
+
node.accept(self)
|
24
|
+
@output << ")" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
|
25
|
+
@output << suffix
|
26
|
+
end
|
27
|
+
|
28
|
+
def visit_function(node)
|
29
|
+
@output << node.name
|
30
|
+
@output << "("
|
31
|
+
arg_count = node.args.length
|
32
|
+
node.args.each_with_index do |a, index|
|
33
|
+
a.accept(self)
|
34
|
+
@output << ", " unless index >= arg_count - 1
|
35
|
+
end
|
36
|
+
@output << ")"
|
37
|
+
end
|
38
|
+
|
39
|
+
def visit_case(node)
|
40
|
+
@output << "CASE "
|
41
|
+
node.switch.accept(self)
|
42
|
+
node.conditions.each { |c| c.accept(self) }
|
43
|
+
node.else && node.else.accept(self)
|
44
|
+
@output << " END"
|
45
|
+
end
|
46
|
+
|
47
|
+
def visit_switch(node)
|
48
|
+
node.node.accept(self)
|
49
|
+
end
|
50
|
+
|
51
|
+
def visit_case_conditional(node)
|
52
|
+
node.when.accept(self)
|
53
|
+
node.then.accept(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def visit_when(node)
|
57
|
+
@output << " WHEN "
|
58
|
+
node.node.accept(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_then(node)
|
62
|
+
@output << " THEN "
|
63
|
+
node.node.accept(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def visit_else(node)
|
67
|
+
@output << " ELSE "
|
68
|
+
node.node.accept(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
def visit_negation(node)
|
72
|
+
@output << "-"
|
73
|
+
@output << "(" unless node.node.is_a? Dentaku::AST::Literal
|
74
|
+
node.node.accept(self)
|
75
|
+
@output << ")" unless node.node.is_a? Dentaku::AST::Literal
|
76
|
+
end
|
77
|
+
|
78
|
+
def visit_access(node)
|
79
|
+
node.structure.accept(self)
|
80
|
+
@output << "["
|
81
|
+
node.index.accept(self)
|
82
|
+
@output << "]"
|
83
|
+
end
|
84
|
+
|
85
|
+
def visit_literal(node)
|
86
|
+
@output << node.quoted
|
87
|
+
end
|
88
|
+
|
89
|
+
def visit_identifier(node)
|
90
|
+
@output << node.identifier
|
91
|
+
end
|
92
|
+
|
93
|
+
def visit_nil(node)
|
94
|
+
@output << "NULL"
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_s
|
98
|
+
@output
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -43,6 +43,7 @@ module Dentaku
|
|
43
43
|
:combinator,
|
44
44
|
:operator,
|
45
45
|
:grouping,
|
46
|
+
:array,
|
46
47
|
:access,
|
47
48
|
:case_statement,
|
48
49
|
:comparator,
|
@@ -90,7 +91,7 @@ module Dentaku
|
|
90
91
|
|
91
92
|
def numeric
|
92
93
|
new(:numeric, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
|
93
|
-
raw =~
|
94
|
+
raw =~ /(\.|e|E)/ ? BigDecimal(raw) : raw.to_i
|
94
95
|
})
|
95
96
|
end
|
96
97
|
|
@@ -119,9 +120,9 @@ module Dentaku
|
|
119
120
|
|
120
121
|
def operator
|
121
122
|
names = {
|
122
|
-
pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&'
|
123
|
+
pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&', bitshiftleft: '<<', bitshiftright: '>>'
|
123
124
|
}.invert
|
124
|
-
new(:operator, '
|
125
|
+
new(:operator, '\^|\+|-|\*|\/|%|\||&|<<|>>', lambda { |raw| names[raw] })
|
125
126
|
end
|
126
127
|
|
127
128
|
def grouping
|
@@ -129,6 +130,11 @@ module Dentaku
|
|
129
130
|
new(:grouping, '\(|\)|,', lambda { |raw| names[raw] })
|
130
131
|
end
|
131
132
|
|
133
|
+
def array
|
134
|
+
names = { array_start: '{', array_end: '}', }.invert
|
135
|
+
new(:array, '\{|\}|,', lambda { |raw| names[raw] })
|
136
|
+
end
|
137
|
+
|
132
138
|
def access
|
133
139
|
names = { lbracket: '[', rbracket: ']' }.invert
|
134
140
|
new(:access, '\[|\]', lambda { |raw| names[raw] })
|
data/lib/dentaku/tokenizer.rb
CHANGED
@@ -12,7 +12,7 @@ module Dentaku
|
|
12
12
|
def tokenize(string, options = {})
|
13
13
|
@nesting = 0
|
14
14
|
@tokens = []
|
15
|
-
@aliases = options.fetch(:aliases,
|
15
|
+
@aliases = options.fetch(:aliases, global_aliases)
|
16
16
|
input = strip_comments(string.to_s.dup)
|
17
17
|
input = replace_aliases(input)
|
18
18
|
@case_sensitive = options.fetch(:case_sensitive, false)
|
@@ -84,6 +84,11 @@ module Dentaku
|
|
84
84
|
|
85
85
|
private
|
86
86
|
|
87
|
+
def global_aliases
|
88
|
+
return {} unless Dentaku.respond_to?(:aliases)
|
89
|
+
Dentaku.aliases
|
90
|
+
end
|
91
|
+
|
87
92
|
def fail!(reason, **meta)
|
88
93
|
message =
|
89
94
|
case reason
|
@@ -99,7 +104,7 @@ module Dentaku
|
|
99
104
|
raise ::ArgumentError, "Unhandled #{reason}"
|
100
105
|
end
|
101
106
|
|
102
|
-
raise TokenizerError.for(reason, meta), message
|
107
|
+
raise TokenizerError.for(reason, **meta), message
|
103
108
|
end
|
104
109
|
end
|
105
110
|
end
|
data/lib/dentaku/version.rb
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
# infix visitor
|
2
|
+
#
|
3
|
+
# use this visitor in a processor to get infix visiting order
|
4
|
+
#
|
5
|
+
# visitor node deps
|
6
|
+
# accept -> visit left ->
|
7
|
+
# process
|
8
|
+
# visit right ->
|
9
|
+
module Dentaku
|
10
|
+
module Visitor
|
11
|
+
module Infix
|
12
|
+
def visit(ast)
|
13
|
+
ast.accept(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def process(_ast)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit_function(node)
|
21
|
+
node.args.each do |arg|
|
22
|
+
visit(arg)
|
23
|
+
end
|
24
|
+
process(node)
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_identifier(node)
|
28
|
+
process(node)
|
29
|
+
end
|
30
|
+
|
31
|
+
def visit_operation(node)
|
32
|
+
visit(node.left) if node.left
|
33
|
+
process(node)
|
34
|
+
visit(node.right) if node.right
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_operand(node)
|
38
|
+
process(node)
|
39
|
+
end
|
40
|
+
|
41
|
+
def visit_case(node)
|
42
|
+
process(node)
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_switch(node)
|
46
|
+
process(node)
|
47
|
+
end
|
48
|
+
|
49
|
+
def visit_case_conditional(node)
|
50
|
+
process(node)
|
51
|
+
end
|
52
|
+
|
53
|
+
def visit_when(node)
|
54
|
+
process(node)
|
55
|
+
end
|
56
|
+
|
57
|
+
def visit_then(node)
|
58
|
+
process(node)
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_else(node)
|
62
|
+
process(node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def visit_negation(node)
|
66
|
+
process(node)
|
67
|
+
end
|
68
|
+
|
69
|
+
def visit_access(node)
|
70
|
+
process(node)
|
71
|
+
end
|
72
|
+
|
73
|
+
def visit_literal(node)
|
74
|
+
process(node)
|
75
|
+
end
|
76
|
+
|
77
|
+
def visit_nil(node)
|
78
|
+
process(node)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/dentaku.rb
CHANGED
@@ -1,23 +1,26 @@
|
|
1
1
|
require "bigdecimal"
|
2
|
+
require "concurrent"
|
2
3
|
require "dentaku/calculator"
|
3
4
|
require "dentaku/version"
|
4
5
|
|
5
6
|
module Dentaku
|
6
7
|
@enable_ast_caching = false
|
7
8
|
@enable_dependency_order_caching = false
|
9
|
+
@enable_identifier_caching = false
|
8
10
|
@aliases = {}
|
9
11
|
|
10
|
-
def self.evaluate(expression, data = {})
|
11
|
-
calculator.evaluate(expression, data)
|
12
|
+
def self.evaluate(expression, data = {}, &block)
|
13
|
+
calculator.value.evaluate(expression, data, &block)
|
12
14
|
end
|
13
15
|
|
14
|
-
def self.evaluate!(expression, data = {})
|
15
|
-
calculator.evaluate!(expression, data)
|
16
|
+
def self.evaluate!(expression, data = {}, &block)
|
17
|
+
calculator.value.evaluate!(expression, data, &block)
|
16
18
|
end
|
17
19
|
|
18
20
|
def self.enable_caching!
|
19
21
|
enable_ast_cache!
|
20
22
|
enable_dependency_order_cache!
|
23
|
+
enable_identifier_cache!
|
21
24
|
end
|
22
25
|
|
23
26
|
def self.enable_ast_cache!
|
@@ -36,6 +39,14 @@ module Dentaku
|
|
36
39
|
@enable_dependency_order_caching
|
37
40
|
end
|
38
41
|
|
42
|
+
def self.enable_identifier_cache!
|
43
|
+
@enable_identifier_caching = true
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.cache_identifier?
|
47
|
+
@enable_identifier_caching
|
48
|
+
end
|
49
|
+
|
39
50
|
def self.aliases
|
40
51
|
@aliases
|
41
52
|
end
|
@@ -44,13 +55,15 @@ module Dentaku
|
|
44
55
|
@aliases = hash
|
45
56
|
end
|
46
57
|
|
47
|
-
private
|
48
|
-
|
49
58
|
def self.calculator
|
50
|
-
@calculator ||= Dentaku::Calculator.new
|
59
|
+
@calculator ||= Concurrent::ThreadLocalVar.new { Dentaku::Calculator.new }
|
51
60
|
end
|
52
61
|
end
|
53
62
|
|
54
63
|
def Dentaku(expression, data = {})
|
55
64
|
Dentaku.evaluate(expression, data)
|
56
65
|
end
|
66
|
+
|
67
|
+
def Dentaku!(expression, data = {})
|
68
|
+
Dentaku.evaluate!(expression, data)
|
69
|
+
end
|
data/spec/ast/addition_spec.rb
CHANGED
@@ -9,9 +9,15 @@ describe Dentaku::AST::Addition do
|
|
9
9
|
|
10
10
|
let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
|
11
11
|
|
12
|
+
it 'allows access to its sub-trees' do
|
13
|
+
node = described_class.new(five, six)
|
14
|
+
expect(node.left).to eq(five)
|
15
|
+
expect(node.right).to eq(six)
|
16
|
+
end
|
17
|
+
|
12
18
|
it 'performs addition' do
|
13
19
|
node = described_class.new(five, six)
|
14
|
-
expect(node.value).to eq
|
20
|
+
expect(node.value).to eq(11)
|
15
21
|
end
|
16
22
|
|
17
23
|
it 'requires numeric operands' do
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/all'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe Dentaku::AST::All do
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
it 'performs ALL operation' do
|
8
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
|
9
|
+
expect(result).to eq(false)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'works with a single value if needed for some reason' do
|
13
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: 1)
|
14
|
+
expect(result).to eq(false)
|
15
|
+
|
16
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: 2)
|
17
|
+
expect(result).to eq(true)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'raises argument error if a string is passed as identifier' do
|
21
|
+
expect { calculator.evaluate!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
|
22
|
+
Dentaku::ArgumentError, 'ALL() requires second argument to be an identifier'
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
@@ -7,29 +7,29 @@ describe 'Dentaku::AST::And' do
|
|
7
7
|
|
8
8
|
it 'returns false if any of the arguments is false' do
|
9
9
|
result = Dentaku('AND(1 = 1, 0 = 1)')
|
10
|
-
expect(result).to eq
|
10
|
+
expect(result).to eq(false)
|
11
11
|
end
|
12
12
|
|
13
13
|
it 'supports nested expressions' do
|
14
14
|
result = Dentaku('AND(y = 1, x = 1)', x: 1, y: 2)
|
15
|
-
expect(result).to eq
|
15
|
+
expect(result).to eq(false)
|
16
16
|
end
|
17
17
|
|
18
18
|
it 'returns true if all of the arguments are true' do
|
19
19
|
result = Dentaku('AND(1 = 1, "2" = "2", true = true, true)')
|
20
|
-
expect(result).to eq
|
20
|
+
expect(result).to eq(true)
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'returns true if all nested AND functions return true' do
|
24
24
|
result = Dentaku('AND(AND(1 = 1), AND(true != false, AND(true)))')
|
25
|
-
expect(result).to eq
|
25
|
+
expect(result).to eq(true)
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'raises an error if no arguments are passed' do
|
29
|
-
expect { calculator.evaluate!('AND()') }.to raise_error(ArgumentError)
|
29
|
+
expect { calculator.evaluate!('AND()') }.to raise_error(Dentaku::ArgumentError)
|
30
30
|
end
|
31
31
|
|
32
32
|
it 'raises an error if a non logical argument is passed' do
|
33
|
-
expect { calculator.evaluate!('AND("r")') }.to raise_error(ArgumentError)
|
33
|
+
expect { calculator.evaluate!('AND("r")') }.to raise_error(Dentaku::ArgumentError)
|
34
34
|
end
|
35
35
|
end
|