dentaku 3.5.2 → 3.5.4
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/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)
|