dentaku 3.5.2 → 3.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +1 -1
- data/dentaku.gemspec +1 -0
- data/lib/dentaku/ast/access.rb +0 -3
- data/lib/dentaku/ast/arithmetic.rb +25 -35
- data/lib/dentaku/ast/array.rb +1 -4
- data/lib/dentaku/ast/functions/all.rb +1 -5
- data/lib/dentaku/ast/functions/any.rb +1 -5
- data/lib/dentaku/ast/functions/enum.rb +13 -0
- data/lib/dentaku/ast/functions/if.rb +3 -7
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/map.rb +1 -5
- data/lib/dentaku/ast/functions/pluck.rb +6 -2
- data/lib/dentaku/ast/functions/reduce.rb +61 -0
- data/lib/dentaku/ast/node.rb +2 -1
- data/lib/dentaku/ast.rb +3 -1
- data/lib/dentaku/bulk_expression_solver.rb +37 -7
- data/lib/dentaku/calculator.rb +23 -7
- data/lib/dentaku/date_arithmetic.rb +26 -15
- data/lib/dentaku/dependency_resolver.rb +9 -4
- data/lib/dentaku/exceptions.rb +8 -3
- data/lib/dentaku/parser.rb +8 -5
- data/lib/dentaku/token.rb +12 -0
- data/lib/dentaku/token_scanner.rb +7 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/addition_spec.rb +4 -4
- data/spec/ast/all_spec.rb +13 -0
- data/spec/ast/any_spec.rb +13 -0
- data/spec/ast/arithmetic_spec.rb +43 -2
- data/spec/ast/division_spec.rb +25 -0
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +13 -0
- data/spec/ast/pluck_spec.rb +17 -0
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/bulk_expression_solver_spec.rb +33 -0
- data/spec/calculator_spec.rb +66 -7
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/parser_spec.rb +3 -0
- data/spec/tokenizer_spec.rb +6 -4
- data/spec/visitor_spec.rb +2 -1
- metadata +25 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef9e8cb8d852db9a8e4c81c28815c6a7e7008c6e9bebcc8a11222768756fd7a8
|
4
|
+
data.tar.gz: f7d1c005ef1ba8bcfc77fcac8b885378a0e3e36fdba8f97a2c602b6b0ba6f34c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d616fd597440c13e8432ac6f9d5480592cdcbb7e9aec30c1f2c21e94e0a4bcf3ffced31cf486dc7390bd8668bb5e377745a7eebf467edfab1cd5fa0ae4f70e9
|
7
|
+
data.tar.gz: fca27bf921a54469f065f634a44d4a6481c6420ccbcaa39400ffe4a5cdd172ff6ae209a04aee96208a64979bbff814635719732b557df33f7d406f87123b1c3a
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [v3.5.4]
|
4
|
+
- add support for default value for PLUCK function
|
5
|
+
- improve error handling for MAP/ANY/ALL functions
|
6
|
+
- fix modulo / percentage operator determination
|
7
|
+
- fix string casing bug with bulk expressions
|
8
|
+
- add explicit gem dependency for BigDecimal
|
9
|
+
|
10
|
+
## [v3.5.3]
|
11
|
+
- add support for empty array literals
|
12
|
+
- add support for quoted identifiers
|
13
|
+
- add REDUCE function
|
14
|
+
- add INTERCEPT function
|
15
|
+
- improve date/time parsing an arithmetic
|
16
|
+
- improve custom class arithmetic
|
17
|
+
- fix IF dependency
|
18
|
+
|
3
19
|
## [v3.5.2]
|
4
20
|
- add ABS function
|
5
21
|
- add array support for AST visitors
|
@@ -244,6 +260,8 @@
|
|
244
260
|
## [v0.1.0] 2012-01-20
|
245
261
|
- initial release
|
246
262
|
|
263
|
+
[v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
|
264
|
+
[v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
|
247
265
|
[v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
|
248
266
|
[v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
|
249
267
|
[v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
|
data/README.md
CHANGED
@@ -145,7 +145,7 @@ Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
|
|
145
145
|
|
146
146
|
Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`
|
147
147
|
|
148
|
-
Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`
|
148
|
+
Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`
|
149
149
|
|
150
150
|
Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L593))
|
151
151
|
|
data/dentaku.gemspec
CHANGED
data/lib/dentaku/ast/access.rb
CHANGED
@@ -62,6 +62,8 @@ module Dentaku
|
|
62
62
|
|
63
63
|
def decimal(val)
|
64
64
|
BigDecimal(val.to_s, Float::DIG + 1)
|
65
|
+
rescue # return as is, in case value can't be coerced to big decimal
|
66
|
+
val
|
65
67
|
end
|
66
68
|
|
67
69
|
def datetime?(val)
|
@@ -177,50 +179,42 @@ module Dentaku
|
|
177
179
|
|
178
180
|
class Modulo < Arithmetic
|
179
181
|
def self.arity
|
180
|
-
|
182
|
+
2
|
181
183
|
end
|
182
184
|
|
183
|
-
def self.
|
184
|
-
|
185
|
-
@arity = 2 if input.length > 1
|
185
|
+
def self.precedence
|
186
|
+
20
|
186
187
|
end
|
187
188
|
|
188
|
-
def
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
189
|
+
def self.resolve_class(next_token)
|
190
|
+
next_token.nil? || next_token.operator? || next_token.close? ? Percentage : self
|
191
|
+
end
|
192
|
+
|
193
|
+
def operator
|
194
|
+
:%
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class Percentage < Arithmetic
|
199
|
+
def self.arity
|
200
|
+
1
|
201
|
+
end
|
202
|
+
|
203
|
+
def initialize(child)
|
204
|
+
@right = child
|
195
205
|
|
196
|
-
unless valid_left?
|
197
|
-
raise NodeError.new(%i[numeric nil], left.type, :left),
|
198
|
-
"#{self.class} requires numeric operands or nil"
|
199
|
-
end
|
200
206
|
unless valid_right?
|
201
207
|
raise NodeError.new(:numeric, right.type, :right),
|
202
|
-
"#{self.class} requires numeric
|
208
|
+
"#{self.class} requires a numeric operand"
|
203
209
|
end
|
204
210
|
end
|
205
211
|
|
206
212
|
def dependencies(context = {})
|
207
|
-
|
208
|
-
@right.dependencies(context)
|
209
|
-
else
|
210
|
-
super
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
def percent?
|
215
|
-
left.nil?
|
213
|
+
@right.dependencies(context)
|
216
214
|
end
|
217
215
|
|
218
216
|
def value(context = {})
|
219
|
-
|
220
|
-
cast(right.value(context)) * 0.01
|
221
|
-
else
|
222
|
-
super
|
223
|
-
end
|
217
|
+
cast(right.value(context)) * 0.01
|
224
218
|
end
|
225
219
|
|
226
220
|
def operator
|
@@ -228,11 +222,7 @@ module Dentaku
|
|
228
222
|
end
|
229
223
|
|
230
224
|
def self.precedence
|
231
|
-
|
232
|
-
end
|
233
|
-
|
234
|
-
def valid_left?
|
235
|
-
valid_node?(left) || left.nil?
|
225
|
+
30
|
236
226
|
end
|
237
227
|
end
|
238
228
|
|
data/lib/dentaku/ast/array.rb
CHANGED
@@ -14,9 +14,6 @@ module Dentaku
|
|
14
14
|
Float::INFINITY
|
15
15
|
end
|
16
16
|
|
17
|
-
def self.peek(*)
|
18
|
-
end
|
19
|
-
|
20
17
|
def initialize(*elements)
|
21
18
|
@elements = *elements
|
22
19
|
end
|
@@ -32,7 +29,7 @@ module Dentaku
|
|
32
29
|
def type
|
33
30
|
nil
|
34
31
|
end
|
35
|
-
|
32
|
+
|
36
33
|
def accept(visitor)
|
37
34
|
visitor.visit_array(self)
|
38
35
|
end
|
@@ -9,11 +9,7 @@ module Dentaku
|
|
9
9
|
expression = @args[2]
|
10
10
|
|
11
11
|
collection.all? do |item_value|
|
12
|
-
expression
|
13
|
-
context.merge(
|
14
|
-
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
15
|
-
)
|
16
|
-
)
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -9,11 +9,7 @@ module Dentaku
|
|
9
9
|
expression = @args[2]
|
10
10
|
|
11
11
|
collection.any? do |item_value|
|
12
|
-
expression
|
13
|
-
context.merge(
|
14
|
-
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
15
|
-
)
|
16
|
-
)
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -33,6 +33,19 @@ module Dentaku
|
|
33
33
|
def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
|
34
34
|
raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
|
35
35
|
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def mapped_value(expression, context, item_context)
|
40
|
+
expression.value(
|
41
|
+
context.merge(
|
42
|
+
FlatHash.from_hash_with_intermediates(item_context)
|
43
|
+
)
|
44
|
+
)
|
45
|
+
rescue => e
|
46
|
+
raise e if context["__evaluation_mode"] == :strict
|
47
|
+
nil
|
48
|
+
end
|
36
49
|
end
|
37
50
|
end
|
38
51
|
end
|
@@ -36,13 +36,9 @@ module Dentaku
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def dependencies(context = {})
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
|
43
|
-
else
|
44
|
-
(deps + left.dependencies(context) + right.dependencies(context)).uniq
|
45
|
-
end
|
39
|
+
predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
|
40
|
+
rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError
|
41
|
+
args.flat_map { |arg| arg.dependencies(context) }.uniq
|
46
42
|
end
|
47
43
|
end
|
48
44
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../function'
|
2
|
+
|
3
|
+
Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
|
4
|
+
if args.length != 2
|
5
|
+
raise Dentaku::ArgumentError.for(
|
6
|
+
:wrong_number_of_arguments,
|
7
|
+
function_name: 'INTERCEPT()', exact: 2, given: args.length
|
8
|
+
), 'INTERCEPT() requires exactly two arrays of numbers'
|
9
|
+
end
|
10
|
+
|
11
|
+
x_values, y_values = args
|
12
|
+
if !x_values.is_a?(Array) || !y_values.is_a?(Array) || x_values.length != y_values.length
|
13
|
+
raise Dentaku::ArgumentError.for(
|
14
|
+
:invalid_value,
|
15
|
+
function_name: 'INTERCEPT()'
|
16
|
+
), 'INTERCEPT() requires arrays of equal length'
|
17
|
+
end
|
18
|
+
|
19
|
+
n = x_values.length.to_f
|
20
|
+
x_values = x_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
|
21
|
+
y_values = y_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
|
22
|
+
|
23
|
+
x_avg = x_values.sum / n
|
24
|
+
y_avg = y_values.sum / n
|
25
|
+
|
26
|
+
xy_sum = x_values.zip(y_values).map { |x, y| (x_avg - x) * (y_avg - y) }.sum
|
27
|
+
x_square_sum = x_values.map { |x| (x_avg - x)**2 }.sum
|
28
|
+
|
29
|
+
slope = xy_sum / x_square_sum
|
30
|
+
intercept = x_values.zip(y_values).map { |x, y| y - slope * x }.sum / n
|
31
|
+
|
32
|
+
BigDecimal(intercept, Float::DIG + 1)
|
33
|
+
})
|
@@ -9,11 +9,7 @@ module Dentaku
|
|
9
9
|
expression = @args[2]
|
10
10
|
|
11
11
|
collection.map do |item_value|
|
12
|
-
expression
|
13
|
-
context.merge(
|
14
|
-
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
15
|
-
)
|
16
|
-
)
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -9,19 +9,23 @@ module Dentaku
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.max_param_count
|
12
|
-
|
12
|
+
3
|
13
13
|
end
|
14
14
|
|
15
15
|
def value(context = {})
|
16
16
|
collection = Array(@args[0].value(context))
|
17
|
+
|
17
18
|
unless collection.all? { |elem| elem.is_a?(Hash) }
|
18
19
|
raise ArgumentError.for(:incompatible_type, value: collection),
|
19
20
|
'PLUCK() requires first argument to be an array of hashes'
|
20
21
|
end
|
21
22
|
|
22
23
|
pluck_path = @args[1].identifier
|
24
|
+
default = @args[2]
|
23
25
|
|
24
|
-
collection.map { |h|
|
26
|
+
collection.map { |h|
|
27
|
+
h.transform_keys(&:to_s).fetch(pluck_path, default&.value(context))
|
28
|
+
}
|
25
29
|
end
|
26
30
|
end
|
27
31
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative '../function'
|
2
|
+
require_relative '../../exceptions'
|
3
|
+
|
4
|
+
module Dentaku
|
5
|
+
module AST
|
6
|
+
class Reduce < Function
|
7
|
+
def self.min_param_count
|
8
|
+
4
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.max_param_count
|
12
|
+
5
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
super
|
17
|
+
|
18
|
+
validate_identifier(@args[1], 'second')
|
19
|
+
validate_identifier(@args[2], 'third')
|
20
|
+
end
|
21
|
+
|
22
|
+
def dependencies(context = {})
|
23
|
+
collection = @args[0]
|
24
|
+
memo_identifier = @args[1].identifier
|
25
|
+
item_identifier = @args[2].identifier
|
26
|
+
expression = @args[3]
|
27
|
+
|
28
|
+
collection_deps = collection.dependencies(context)
|
29
|
+
expression_deps = expression.dependencies(context).reject do |i|
|
30
|
+
i == memo_identifier || i.start_with?("#{memo_identifier}.") ||
|
31
|
+
i == item_identifier || i.start_with?("#{item_identifier}.")
|
32
|
+
end
|
33
|
+
inital_value_deps = @args[4] ? @args[4].dependencies(context) : []
|
34
|
+
|
35
|
+
collection_deps + expression_deps + inital_value_deps
|
36
|
+
end
|
37
|
+
|
38
|
+
def value(context = {})
|
39
|
+
collection = Array(@args[0].value(context))
|
40
|
+
memo_identifier = @args[1].identifier
|
41
|
+
item_identifier = @args[2].identifier
|
42
|
+
expression = @args[3]
|
43
|
+
initial_value = @args[4] && @args[4].value(context)
|
44
|
+
|
45
|
+
collection.reduce(initial_value) do |memo, item|
|
46
|
+
expression.value(
|
47
|
+
context.merge(
|
48
|
+
FlatHash.from_hash_with_intermediates(memo_identifier => memo, item_identifier => item)
|
49
|
+
)
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_identifier(arg, position, message = "#{name}() requires #{position} argument to be an identifier")
|
55
|
+
raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Dentaku::AST::Function.register_class(:reduce, Dentaku::AST::Reduce)
|
data/lib/dentaku/ast/node.rb
CHANGED
data/lib/dentaku/ast.rb
CHANGED
@@ -24,12 +24,14 @@ require_relative './ast/functions/count'
|
|
24
24
|
require_relative './ast/functions/duration'
|
25
25
|
require_relative './ast/functions/filter'
|
26
26
|
require_relative './ast/functions/if'
|
27
|
+
require_relative './ast/functions/intercept'
|
27
28
|
require_relative './ast/functions/map'
|
28
29
|
require_relative './ast/functions/max'
|
29
30
|
require_relative './ast/functions/min'
|
30
31
|
require_relative './ast/functions/not'
|
31
32
|
require_relative './ast/functions/or'
|
32
33
|
require_relative './ast/functions/pluck'
|
34
|
+
require_relative './ast/functions/reduce'
|
33
35
|
require_relative './ast/functions/round'
|
34
36
|
require_relative './ast/functions/rounddown'
|
35
37
|
require_relative './ast/functions/roundup'
|
@@ -37,4 +39,4 @@ require_relative './ast/functions/ruby_math'
|
|
37
39
|
require_relative './ast/functions/string_functions'
|
38
40
|
require_relative './ast/functions/sum'
|
39
41
|
require_relative './ast/functions/switch'
|
40
|
-
require_relative './ast/functions/xor'
|
42
|
+
require_relative './ast/functions/xor'
|
@@ -6,16 +6,41 @@ require 'dentaku/tokenizer'
|
|
6
6
|
|
7
7
|
module Dentaku
|
8
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
|
+
|
9
32
|
def initialize(expressions, calculator)
|
10
33
|
@expression_hash = FlatHash.from_hash(expressions)
|
11
34
|
@calculator = calculator
|
12
35
|
end
|
13
36
|
|
14
37
|
def solve!
|
38
|
+
@evaluator = StrictEvaluator.new(calculator)
|
15
39
|
solve(&raise_exception_handler)
|
16
40
|
end
|
17
41
|
|
18
42
|
def solve(&block)
|
43
|
+
@evaluator ||= PermissiveEvaluator.new(calculator, block)
|
19
44
|
error_handler = block || return_undefined_handler
|
20
45
|
results = load_results(&error_handler)
|
21
46
|
|
@@ -42,7 +67,7 @@ module Dentaku
|
|
42
67
|
@dep_cache ||= {}
|
43
68
|
end
|
44
69
|
|
45
|
-
attr_reader :expression_hash, :calculator
|
70
|
+
attr_reader :expression_hash, :calculator, :evaluator
|
46
71
|
|
47
72
|
def return_undefined_handler
|
48
73
|
->(*) { :undefined }
|
@@ -52,8 +77,11 @@ module Dentaku
|
|
52
77
|
->(ex) { raise ex }
|
53
78
|
end
|
54
79
|
|
55
|
-
def expression_with_exception_handler(&block)
|
56
|
-
->(_expr, ex) {
|
80
|
+
def expression_with_exception_handler(var_name, &block)
|
81
|
+
->(_expr, ex) {
|
82
|
+
ex.recipient_variable = var_name
|
83
|
+
block.call(ex)
|
84
|
+
}
|
57
85
|
end
|
58
86
|
|
59
87
|
def load_results(&block)
|
@@ -73,11 +101,14 @@ module Dentaku
|
|
73
101
|
next if expressions[var_name].nil?
|
74
102
|
|
75
103
|
with_rescues(var_name, results, block) do
|
76
|
-
results[var_name] = evaluated_facts[var_name] ||
|
104
|
+
results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
|
77
105
|
expressions[var_name],
|
78
106
|
context.merge(results),
|
79
|
-
&expression_with_exception_handler(&block)
|
80
|
-
)
|
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
|
+
}
|
81
112
|
end
|
82
113
|
end
|
83
114
|
|
@@ -88,7 +119,6 @@ module Dentaku
|
|
88
119
|
|
89
120
|
def with_rescues(var_name, results, block)
|
90
121
|
yield
|
91
|
-
|
92
122
|
rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
|
93
123
|
ex.recipient_variable = var_name
|
94
124
|
results[var_name] = block.call(ex)
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -52,28 +52,39 @@ module Dentaku
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def evaluate(expression, data = {}, &block)
|
55
|
-
|
55
|
+
context = evaluation_context(data, :permissive)
|
56
|
+
return evaluate_array(expression, context, &block) if expression.is_a?(Array)
|
57
|
+
|
58
|
+
evaluate!(expression, context)
|
56
59
|
rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
|
57
60
|
block.call(expression, ex) if block_given?
|
58
61
|
end
|
59
62
|
|
63
|
+
private def evaluate_array(expression, data = {}, &block)
|
64
|
+
expression.map { |e| evaluate(e, data, &block) }
|
65
|
+
end
|
66
|
+
|
60
67
|
def evaluate!(expression, data = {}, &block)
|
61
|
-
|
62
|
-
|
63
|
-
} if expression.is_a? Array
|
68
|
+
context = evaluation_context(data, :strict)
|
69
|
+
return evaluate_array!(expression, context, &block) if expression.is_a? Array
|
64
70
|
|
65
|
-
store(
|
66
|
-
node = expression
|
67
|
-
node = ast(node) unless node.is_a?(AST::Node)
|
71
|
+
store(context) do
|
72
|
+
node = ast(expression)
|
68
73
|
unbound = node.dependencies(memory)
|
74
|
+
|
69
75
|
unless unbound.empty?
|
70
76
|
raise UnboundVariableError.new(unbound),
|
71
77
|
"no value provided for variables: #{unbound.uniq.join(', ')}"
|
72
78
|
end
|
79
|
+
|
73
80
|
node.value(memory)
|
74
81
|
end
|
75
82
|
end
|
76
83
|
|
84
|
+
private def evaluate_array!(expression, data = {}, &block)
|
85
|
+
expression.map { |e| evaluate!(e, data, &block) }
|
86
|
+
end
|
87
|
+
|
77
88
|
def solve!(expression_hash)
|
78
89
|
BulkExpressionSolver.new(expression_hash, self).solve!
|
79
90
|
end
|
@@ -96,6 +107,7 @@ module Dentaku
|
|
96
107
|
end
|
97
108
|
|
98
109
|
def ast(expression)
|
110
|
+
return expression if expression.is_a?(AST::Node)
|
99
111
|
return expression.map { |e| ast(e) } if expression.is_a? Array
|
100
112
|
|
101
113
|
@ast_cache.fetch(expression) {
|
@@ -130,6 +142,10 @@ module Dentaku
|
|
130
142
|
end
|
131
143
|
end
|
132
144
|
|
145
|
+
def evaluation_context(data, evaluation_mode)
|
146
|
+
data.key?(:__evaluation_mode) ? data : data.merge(__evaluation_mode: evaluation_mode)
|
147
|
+
end
|
148
|
+
|
133
149
|
def store(key_or_hash, value = nil)
|
134
150
|
restore = Hash[memory]
|
135
151
|
|
@@ -13,13 +13,11 @@ module Dentaku
|
|
13
13
|
when Numeric
|
14
14
|
@base + duration
|
15
15
|
when Dentaku::AST::Duration::Value
|
16
|
-
case
|
17
|
-
when
|
18
|
-
|
19
|
-
|
20
|
-
@base
|
21
|
-
when :day
|
22
|
-
@base + 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)
|
23
21
|
end
|
24
22
|
else
|
25
23
|
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
@@ -29,21 +27,34 @@ module Dentaku
|
|
29
27
|
|
30
28
|
def sub(duration)
|
31
29
|
case duration
|
32
|
-
when DateTime, Numeric
|
30
|
+
when Date, DateTime, Numeric, Time
|
33
31
|
@base - duration
|
34
32
|
when Dentaku::AST::Duration::Value
|
35
|
-
case
|
36
|
-
when
|
37
|
-
|
38
|
-
|
39
|
-
@base
|
40
|
-
when :day
|
41
|
-
@base - 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)
|
42
38
|
end
|
39
|
+
when Dentaku::TokenScanner::DATE_TIME_REGEXP
|
40
|
+
@base - Time.parse(duration).to_datetime
|
43
41
|
else
|
44
42
|
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
45
43
|
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
46
44
|
end
|
47
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
|
48
59
|
end
|
49
60
|
end
|
@@ -4,13 +4,18 @@ module Dentaku
|
|
4
4
|
class DependencyResolver
|
5
5
|
include TSort
|
6
6
|
|
7
|
-
def self.find_resolve_order(vars_to_dependencies_hash)
|
8
|
-
self.new(vars_to_dependencies_hash).
|
7
|
+
def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
|
8
|
+
self.new(vars_to_dependencies_hash).sort
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize(vars_to_dependencies_hash)
|
12
|
-
|
13
|
-
|
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) }
|
14
19
|
end
|
15
20
|
|
16
21
|
def tsort_each_node(&block)
|