dentaku 3.2.0 → 3.5.1
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 +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
@@ -3,7 +3,28 @@ require_relative '../function'
|
|
3
3
|
module Dentaku
|
4
4
|
module AST
|
5
5
|
module StringFunctions
|
6
|
-
class
|
6
|
+
class Base < Function
|
7
|
+
def type
|
8
|
+
:string
|
9
|
+
end
|
10
|
+
|
11
|
+
def negative_argument_failure(fun, arg = 'length')
|
12
|
+
raise Dentaku::ArgumentError.for(
|
13
|
+
:invalid_value,
|
14
|
+
function_name: "#{fun}()"
|
15
|
+
), "#{fun}() requires #{arg} to be positive"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Left < Base
|
20
|
+
def self.min_param_count
|
21
|
+
2
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.max_param_count
|
25
|
+
2
|
26
|
+
end
|
27
|
+
|
7
28
|
def initialize(*args)
|
8
29
|
super
|
9
30
|
@string, @length = *@args
|
@@ -11,12 +32,21 @@ module Dentaku
|
|
11
32
|
|
12
33
|
def value(context = {})
|
13
34
|
string = @string.value(context).to_s
|
14
|
-
length = @length.value(context)
|
35
|
+
length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
|
36
|
+
negative_argument_failure('LEFT') if length < 0
|
15
37
|
string[0, length]
|
16
38
|
end
|
17
39
|
end
|
18
40
|
|
19
|
-
class Right <
|
41
|
+
class Right < Base
|
42
|
+
def self.min_param_count
|
43
|
+
2
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.max_param_count
|
47
|
+
2
|
48
|
+
end
|
49
|
+
|
20
50
|
def initialize(*args)
|
21
51
|
super
|
22
52
|
@string, @length = *@args
|
@@ -24,12 +54,21 @@ module Dentaku
|
|
24
54
|
|
25
55
|
def value(context = {})
|
26
56
|
string = @string.value(context).to_s
|
27
|
-
length = @length.value(context)
|
57
|
+
length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
|
58
|
+
negative_argument_failure('RIGHT') if length < 0
|
28
59
|
string[length * -1, length] || string
|
29
60
|
end
|
30
61
|
end
|
31
62
|
|
32
|
-
class Mid <
|
63
|
+
class Mid < Base
|
64
|
+
def self.min_param_count
|
65
|
+
3
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.max_param_count
|
69
|
+
3
|
70
|
+
end
|
71
|
+
|
33
72
|
def initialize(*args)
|
34
73
|
super
|
35
74
|
@string, @offset, @length = *@args
|
@@ -37,13 +76,23 @@ module Dentaku
|
|
37
76
|
|
38
77
|
def value(context = {})
|
39
78
|
string = @string.value(context).to_s
|
40
|
-
offset = @offset.value(context)
|
41
|
-
|
79
|
+
offset = Dentaku::AST::Function.numeric(@offset.value(context)).to_i
|
80
|
+
negative_argument_failure('MID', 'offset') if offset < 0
|
81
|
+
length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
|
82
|
+
negative_argument_failure('MID') if length < 0
|
42
83
|
string[offset - 1, length].to_s
|
43
84
|
end
|
44
85
|
end
|
45
86
|
|
46
|
-
class Len <
|
87
|
+
class Len < Base
|
88
|
+
def self.min_param_count
|
89
|
+
1
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.max_param_count
|
93
|
+
1
|
94
|
+
end
|
95
|
+
|
47
96
|
def initialize(*args)
|
48
97
|
super
|
49
98
|
@string = @args[0]
|
@@ -53,9 +102,21 @@ module Dentaku
|
|
53
102
|
string = @string.value(context).to_s
|
54
103
|
string.length
|
55
104
|
end
|
105
|
+
|
106
|
+
def type
|
107
|
+
:numeric
|
108
|
+
end
|
56
109
|
end
|
57
110
|
|
58
|
-
class Find <
|
111
|
+
class Find < Base
|
112
|
+
def self.min_param_count
|
113
|
+
2
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.max_param_count
|
117
|
+
2
|
118
|
+
end
|
119
|
+
|
59
120
|
def initialize(*args)
|
60
121
|
super
|
61
122
|
@needle, @haystack = *@args
|
@@ -68,9 +129,21 @@ module Dentaku
|
|
68
129
|
pos = haystack.index(needle)
|
69
130
|
pos && pos + 1
|
70
131
|
end
|
132
|
+
|
133
|
+
def type
|
134
|
+
:numeric
|
135
|
+
end
|
71
136
|
end
|
72
137
|
|
73
|
-
class Substitute <
|
138
|
+
class Substitute < Base
|
139
|
+
def self.min_param_count
|
140
|
+
3
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.max_param_count
|
144
|
+
3
|
145
|
+
end
|
146
|
+
|
74
147
|
def initialize(*args)
|
75
148
|
super
|
76
149
|
@original, @search, @replacement = *@args
|
@@ -85,7 +158,15 @@ module Dentaku
|
|
85
158
|
end
|
86
159
|
end
|
87
160
|
|
88
|
-
class Concat <
|
161
|
+
class Concat < Base
|
162
|
+
def self.min_param_count
|
163
|
+
1
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.max_param_count
|
167
|
+
Float::INFINITY
|
168
|
+
end
|
169
|
+
|
89
170
|
def initialize(*args)
|
90
171
|
super
|
91
172
|
end
|
@@ -95,7 +176,15 @@ module Dentaku
|
|
95
176
|
end
|
96
177
|
end
|
97
178
|
|
98
|
-
class Contains <
|
179
|
+
class Contains < Base
|
180
|
+
def self.min_param_count
|
181
|
+
2
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.max_param_count
|
185
|
+
2
|
186
|
+
end
|
187
|
+
|
99
188
|
def initialize(*args)
|
100
189
|
super
|
101
190
|
@needle, @haystack = *args
|
@@ -104,6 +193,10 @@ module Dentaku
|
|
104
193
|
def value(context = {})
|
105
194
|
@haystack.value(context).to_s.include? @needle.value(context).to_s
|
106
195
|
end
|
196
|
+
|
197
|
+
def type
|
198
|
+
:logical
|
199
|
+
end
|
107
200
|
end
|
108
201
|
end
|
109
202
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative '../function'
|
2
|
+
require_relative '../../exceptions'
|
3
|
+
|
4
|
+
module Dentaku
|
5
|
+
module AST
|
6
|
+
class Xor < Function
|
7
|
+
def self.min_param_count
|
8
|
+
1
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.max_param_count
|
12
|
+
Float::INFINITY
|
13
|
+
end
|
14
|
+
|
15
|
+
def value(context = {})
|
16
|
+
if @args.empty?
|
17
|
+
raise Dentaku::ArgumentError.for(
|
18
|
+
:too_few_arguments,
|
19
|
+
function_name: 'XOR()', at_least: 1, given: 0
|
20
|
+
), 'XOR() requires at least one argument'
|
21
|
+
end
|
22
|
+
|
23
|
+
true_arg_count = 0
|
24
|
+
@args.each do |arg|
|
25
|
+
case arg.value(context)
|
26
|
+
when TrueClass
|
27
|
+
true_arg_count += 1
|
28
|
+
break if true_arg_count > 1
|
29
|
+
when FalseClass, nil
|
30
|
+
next
|
31
|
+
else
|
32
|
+
raise Dentaku::ArgumentError.for(
|
33
|
+
:incompatible_type,
|
34
|
+
function_name: 'XOR()', expect: :logical, actual: arg.class
|
35
|
+
), 'XOR() requires arguments to be logical expressions'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
true_arg_count == 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Dentaku::AST::Function.register_class(:xor, Dentaku::AST::Xor)
|
data/lib/dentaku/ast/grouping.rb
CHANGED
@@ -20,20 +20,32 @@ module Dentaku
|
|
20
20
|
|
21
21
|
case v
|
22
22
|
when Node
|
23
|
-
v.value(context)
|
23
|
+
value = v.value(context)
|
24
|
+
context[identifier] = value if Dentaku.cache_identifier?
|
25
|
+
value
|
26
|
+
when Proc
|
27
|
+
v.call
|
24
28
|
else
|
25
29
|
v
|
26
30
|
end
|
27
31
|
end
|
28
32
|
|
29
33
|
def dependencies(context = {})
|
30
|
-
context.key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
|
34
|
+
context.key?(identifier) ? dependencies_of(context[identifier], context) : [identifier]
|
35
|
+
end
|
36
|
+
|
37
|
+
def accept(visitor)
|
38
|
+
visitor.visit_identifier(self)
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
identifier.to_s
|
31
43
|
end
|
32
44
|
|
33
45
|
private
|
34
46
|
|
35
|
-
def dependencies_of(node)
|
36
|
-
node.respond_to?(:dependencies) ? node.dependencies : []
|
47
|
+
def dependencies_of(node, context)
|
48
|
+
node.respond_to?(:dependencies) ? node.dependencies(context) : []
|
37
49
|
end
|
38
50
|
end
|
39
51
|
end
|
data/lib/dentaku/ast/literal.rb
CHANGED
@@ -4,6 +4,7 @@ module Dentaku
|
|
4
4
|
attr_reader :type
|
5
5
|
|
6
6
|
def initialize(token)
|
7
|
+
@token = token
|
7
8
|
@value = token.value
|
8
9
|
@type = token.category
|
9
10
|
end
|
@@ -15,6 +16,15 @@ module Dentaku
|
|
15
16
|
def dependencies(*)
|
16
17
|
[]
|
17
18
|
end
|
19
|
+
|
20
|
+
def accept(visitor)
|
21
|
+
visitor.visit_literal(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def quoted
|
25
|
+
@token.raw_value || value.to_s
|
26
|
+
end
|
27
|
+
alias_method :to_s, :quoted
|
18
28
|
end
|
19
29
|
end
|
20
30
|
end
|
data/lib/dentaku/ast/negation.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
module Dentaku
|
2
2
|
module AST
|
3
3
|
class Negation < Arithmetic
|
4
|
+
attr_reader :node
|
5
|
+
|
4
6
|
def initialize(node)
|
5
7
|
@node = node
|
6
8
|
|
7
9
|
unless valid_node?(node)
|
8
|
-
raise NodeError.new(:numeric,
|
10
|
+
raise NodeError.new(:numeric, node.type, :node),
|
9
11
|
"#{self.class} requires numeric operands"
|
10
12
|
end
|
11
13
|
end
|
@@ -38,6 +40,10 @@ module Dentaku
|
|
38
40
|
@node.dependencies(context)
|
39
41
|
end
|
40
42
|
|
43
|
+
def accept(visitor)
|
44
|
+
visitor.visit_negation(self)
|
45
|
+
end
|
46
|
+
|
41
47
|
private
|
42
48
|
|
43
49
|
def valid_node?(node)
|
data/lib/dentaku/ast/nil.rb
CHANGED
data/lib/dentaku/ast/node.rb
CHANGED
@@ -5,6 +5,14 @@ module Dentaku
|
|
5
5
|
class Operation < Node
|
6
6
|
attr_reader :left, :right
|
7
7
|
|
8
|
+
def self.min_param_count
|
9
|
+
arity
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.max_param_count
|
13
|
+
arity
|
14
|
+
end
|
15
|
+
|
8
16
|
def initialize(left, right)
|
9
17
|
@left = left
|
10
18
|
@right = right
|
@@ -17,6 +25,15 @@ module Dentaku
|
|
17
25
|
def self.right_associative?
|
18
26
|
false
|
19
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
|
+
alias_method :to_s, :display_operator
|
20
37
|
end
|
21
38
|
end
|
22
39
|
end
|
data/lib/dentaku/ast/string.rb
CHANGED
data/lib/dentaku/ast.rb
CHANGED
@@ -11,17 +11,24 @@ require_relative './ast/negation'
|
|
11
11
|
require_relative './ast/comparators'
|
12
12
|
require_relative './ast/combinators'
|
13
13
|
require_relative './ast/access'
|
14
|
+
require_relative './ast/array'
|
14
15
|
require_relative './ast/grouping'
|
15
16
|
require_relative './ast/case'
|
16
17
|
require_relative './ast/function_registry'
|
18
|
+
require_relative './ast/functions/all'
|
17
19
|
require_relative './ast/functions/and'
|
20
|
+
require_relative './ast/functions/any'
|
18
21
|
require_relative './ast/functions/avg'
|
19
22
|
require_relative './ast/functions/count'
|
23
|
+
require_relative './ast/functions/duration'
|
24
|
+
require_relative './ast/functions/filter'
|
20
25
|
require_relative './ast/functions/if'
|
26
|
+
require_relative './ast/functions/map'
|
21
27
|
require_relative './ast/functions/max'
|
22
28
|
require_relative './ast/functions/min'
|
23
29
|
require_relative './ast/functions/not'
|
24
30
|
require_relative './ast/functions/or'
|
31
|
+
require_relative './ast/functions/pluck'
|
25
32
|
require_relative './ast/functions/round'
|
26
33
|
require_relative './ast/functions/rounddown'
|
27
34
|
require_relative './ast/functions/roundup'
|
@@ -29,3 +36,4 @@ require_relative './ast/functions/ruby_math'
|
|
29
36
|
require_relative './ast/functions/string_functions'
|
30
37
|
require_relative './ast/functions/sum'
|
31
38
|
require_relative './ast/functions/switch'
|
39
|
+
require_relative './ast/functions/xor'
|
@@ -20,8 +20,9 @@ module Dentaku
|
|
20
20
|
results = load_results(&error_handler)
|
21
21
|
|
22
22
|
FlatHash.expand(
|
23
|
-
expression_hash.each_with_object({}) do |(k,
|
24
|
-
|
23
|
+
expression_hash.each_with_object({}) do |(k, v), r|
|
24
|
+
default = v.nil? ? v : :undefined
|
25
|
+
r[k] = results.fetch(k.to_s, default)
|
25
26
|
end
|
26
27
|
)
|
27
28
|
end
|
@@ -52,35 +53,49 @@ module Dentaku
|
|
52
53
|
end
|
53
54
|
|
54
55
|
def expression_with_exception_handler(&block)
|
55
|
-
->(
|
56
|
+
->(_expr, ex) { block.call(ex) }
|
56
57
|
end
|
57
58
|
|
58
59
|
def load_results(&block)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
60
|
+
facts, _formulas = expressions.transform_keys(&:downcase)
|
61
|
+
.transform_values { |v| calculator.ast(v) }
|
62
|
+
.partition { |_, v| calculator.dependencies(v, nil).empty? }
|
63
|
+
|
64
|
+
evaluated_facts = facts.to_h.each_with_object({}) do |(var_name, ast), h|
|
65
|
+
with_rescues(var_name, h, block) do
|
66
|
+
h[var_name] = ast.is_a?(Array) ? ast.map(&:value) : ast.value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context = calculator.memory.merge(evaluated_facts)
|
71
|
+
|
72
|
+
variables_in_resolve_order.each_with_object({}) do |var_name, results|
|
73
|
+
next if expressions[var_name].nil?
|
74
|
+
|
75
|
+
with_rescues(var_name, results, block) do
|
76
|
+
results[var_name] = evaluated_facts[var_name] || calculator.evaluate!(
|
71
77
|
expressions[var_name],
|
72
|
-
|
78
|
+
context.merge(results),
|
73
79
|
&expression_with_exception_handler(&block)
|
74
80
|
)
|
75
|
-
|
76
|
-
r[var_name] = value
|
77
|
-
rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
|
78
|
-
ex.recipient_variable = var_name
|
79
|
-
r[var_name] = block.call(ex)
|
80
|
-
rescue Dentaku::ArgumentError => ex
|
81
|
-
r[var_name] = block.call(ex)
|
82
81
|
end
|
83
82
|
end
|
83
|
+
|
84
|
+
rescue TSort::Cyclic => ex
|
85
|
+
block.call(ex)
|
86
|
+
{}
|
87
|
+
end
|
88
|
+
|
89
|
+
def with_rescues(var_name, results, block)
|
90
|
+
yield
|
91
|
+
|
92
|
+
rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
|
93
|
+
ex.recipient_variable = var_name
|
94
|
+
results[var_name] = block.call(ex)
|
95
|
+
ensure
|
96
|
+
if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
|
97
|
+
results[var_name] = calculator.memory[var_name.downcase]
|
98
|
+
end
|
84
99
|
end
|
85
100
|
|
86
101
|
def expressions
|
@@ -109,9 +124,5 @@ module Dentaku
|
|
109
124
|
end
|
110
125
|
}
|
111
126
|
end
|
112
|
-
|
113
|
-
def evaluate!(expression, results, &block)
|
114
|
-
calculator.evaluate!(expression, results, &block)
|
115
|
-
end
|
116
127
|
end
|
117
128
|
end
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -9,7 +9,8 @@ require 'dentaku/token'
|
|
9
9
|
module Dentaku
|
10
10
|
class Calculator
|
11
11
|
include StringCasing
|
12
|
-
attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
|
12
|
+
attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
|
13
|
+
:nested_data_support, :ast_cache
|
13
14
|
|
14
15
|
def initialize(options = {})
|
15
16
|
clear
|
@@ -46,7 +47,7 @@ module Dentaku
|
|
46
47
|
|
47
48
|
def evaluate(expression, data = {}, &block)
|
48
49
|
evaluate!(expression, data)
|
49
|
-
rescue
|
50
|
+
rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
|
50
51
|
block.call(expression, ex) if block_given?
|
51
52
|
end
|
52
53
|
|
@@ -58,10 +59,10 @@ module Dentaku
|
|
58
59
|
store(data) do
|
59
60
|
node = expression
|
60
61
|
node = ast(node) unless node.is_a?(AST::Node)
|
61
|
-
unbound = node.dependencies
|
62
|
+
unbound = node.dependencies(memory)
|
62
63
|
unless unbound.empty?
|
63
64
|
raise UnboundVariableError.new(unbound),
|
64
|
-
"no value provided for variables: #{unbound.join(', ')}"
|
65
|
+
"no value provided for variables: #{unbound.uniq.join(', ')}"
|
65
66
|
end
|
66
67
|
node.value(memory)
|
67
68
|
end
|
@@ -76,13 +77,21 @@ module Dentaku
|
|
76
77
|
end
|
77
78
|
|
78
79
|
def dependencies(expression, context = {})
|
79
|
-
|
80
|
-
|
80
|
+
test_context = context.nil? ? {} : store(context) { memory }
|
81
|
+
|
82
|
+
case expression
|
83
|
+
when Dentaku::AST::Node
|
84
|
+
expression.dependencies(test_context)
|
85
|
+
when Array
|
86
|
+
expression.flat_map { |e| dependencies(e, context) }
|
87
|
+
else
|
88
|
+
ast(expression).dependencies(test_context)
|
81
89
|
end
|
82
|
-
store(context) { ast(expression).dependencies(memory) }
|
83
90
|
end
|
84
91
|
|
85
92
|
def ast(expression)
|
93
|
+
return expression.map { |e| ast(e) } if expression.is_a? Array
|
94
|
+
|
86
95
|
@ast_cache.fetch(expression) {
|
87
96
|
options = {
|
88
97
|
case_sensitive: case_sensitive,
|
@@ -97,6 +106,10 @@ module Dentaku
|
|
97
106
|
}
|
98
107
|
end
|
99
108
|
|
109
|
+
def load_cache(ast_cache)
|
110
|
+
@ast_cache = ast_cache
|
111
|
+
end
|
112
|
+
|
100
113
|
def clear_cache(pattern = :all)
|
101
114
|
case pattern
|
102
115
|
when :all
|
@@ -114,7 +127,7 @@ module Dentaku
|
|
114
127
|
restore = Hash[memory]
|
115
128
|
|
116
129
|
if value.nil?
|
117
|
-
key_or_hash = FlatHash.
|
130
|
+
key_or_hash = FlatHash.from_hash_with_intermediates(key_or_hash) if nested_data_support
|
118
131
|
key_or_hash.each do |key, val|
|
119
132
|
memory[standardize_case(key.to_s)] = val
|
120
133
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Dentaku
|
2
|
+
class DateArithmetic
|
3
|
+
def initialize(date)
|
4
|
+
@base = date
|
5
|
+
end
|
6
|
+
|
7
|
+
def add(duration)
|
8
|
+
case duration
|
9
|
+
when Numeric
|
10
|
+
@base + duration
|
11
|
+
when Dentaku::AST::Duration::Value
|
12
|
+
case duration.unit
|
13
|
+
when :year
|
14
|
+
Time.local(@base.year + duration.value, @base.month, @base.day).to_datetime
|
15
|
+
when :month
|
16
|
+
@base >> duration.value
|
17
|
+
when :day
|
18
|
+
@base + duration.value
|
19
|
+
end
|
20
|
+
else
|
21
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
22
|
+
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def sub(duration)
|
27
|
+
case duration
|
28
|
+
when DateTime, Numeric
|
29
|
+
@base - duration
|
30
|
+
when Dentaku::AST::Duration::Value
|
31
|
+
case duration.unit
|
32
|
+
when :year
|
33
|
+
Time.local(@base.year - duration.value, @base.month, @base.day).to_datetime
|
34
|
+
when :month
|
35
|
+
@base << duration.value
|
36
|
+
when :day
|
37
|
+
@base - duration.value
|
38
|
+
end
|
39
|
+
else
|
40
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
41
|
+
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|