dentaku 3.5.1 → 3.5.3
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 +24 -1
- data/README.md +1 -1
- data/lib/dentaku/ast/arithmetic.rb +34 -12
- data/lib/dentaku/ast/comparators.rb +1 -2
- data/lib/dentaku/ast/function_registry.rb +10 -1
- data/lib/dentaku/ast/functions/abs.rb +5 -0
- data/lib/dentaku/ast/functions/avg.rb +1 -1
- data/lib/dentaku/ast/functions/enum.rb +5 -4
- data/lib/dentaku/ast/functions/if.rb +3 -7
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/reduce.rb +61 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +2 -0
- data/lib/dentaku/ast.rb +4 -1
- data/lib/dentaku/calculator.rb +17 -10
- data/lib/dentaku/date_arithmetic.rb +8 -2
- data/lib/dentaku/exceptions.rb +17 -3
- data/lib/dentaku/parser.rb +20 -9
- data/lib/dentaku/print_visitor.rb +16 -5
- data/lib/dentaku/token_scanner.rb +12 -3
- data/lib/dentaku/tokenizer.rb +7 -3
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +4 -0
- data/spec/ast/abs_spec.rb +26 -0
- data/spec/ast/addition_spec.rb +4 -4
- data/spec/ast/all_spec.rb +1 -1
- data/spec/ast/any_spec.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +61 -12
- data/spec/ast/avg_spec.rb +5 -0
- data/spec/ast/division_spec.rb +25 -0
- data/spec/ast/filter_spec.rb +1 -1
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +1 -1
- data/spec/ast/pluck_spec.rb +1 -1
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/bulk_expression_solver_spec.rb +17 -0
- data/spec/calculator_spec.rb +99 -17
- data/spec/external_function_spec.rb +89 -18
- data/spec/parser_spec.rb +3 -0
- data/spec/print_visitor_spec.rb +6 -0
- data/spec/tokenizer_spec.rb +6 -4
- data/spec/visitor/infix_spec.rb +22 -1
- data/spec/visitor_spec.rb +2 -1
- metadata +12 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a51418767c413ccded1f235a56d34866005edf21cbc0d4cf5848d4cff4bc801f
|
|
4
|
+
data.tar.gz: 2009c09a76a5cc0b85cf04769c93f0d372cc8613f410a18401de53ca268b134d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cfeabc676fa63016096d2f3c291d60a68ce247fe7c75ba195cdc2968c28af85de9ab9eeaaa9e88749cbf7920a120b1e6fe4d78464b98f9c825180d42aafc4f1
|
|
7
|
+
data.tar.gz: 42863ad11afff2dc72dfe906d1fdc1c07821f4391dcb7fc67921d862c736350be15a6671beac9c16de9e1345f17869e0de40bb131bc1fcfea70190538274bd53
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
- add support for empty array literals
|
|
5
|
+
- add support for quoted identifiers
|
|
6
|
+
- add REDUCE function
|
|
7
|
+
- add INTERCEPT function
|
|
8
|
+
- improve date/time parsing an arithmetic
|
|
9
|
+
- improve custom class arithmetic
|
|
10
|
+
- fix IF dependency
|
|
11
|
+
|
|
12
|
+
## [v3.5.2]
|
|
13
|
+
- add ABS function
|
|
14
|
+
- add array support for AST visitors
|
|
15
|
+
- add support for function callbacks
|
|
16
|
+
- improve support for date / time values
|
|
17
|
+
- improve error messaging for invalid arity
|
|
18
|
+
- improve AVG function accuracy
|
|
19
|
+
- validate enum arguments at parse time
|
|
20
|
+
- support adding multiple functions at once to global registry
|
|
21
|
+
- fix bug in print visitor precedence checking
|
|
22
|
+
- fix handling of Math::DomainError
|
|
23
|
+
- fix invalid cast
|
|
24
|
+
|
|
3
25
|
## [v3.5.1]
|
|
4
26
|
- add bitwise shift left and shift right operators
|
|
5
27
|
- improve numeric conversions
|
|
@@ -231,7 +253,8 @@
|
|
|
231
253
|
## [v0.1.0] 2012-01-20
|
|
232
254
|
- initial release
|
|
233
255
|
|
|
234
|
-
[Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.
|
|
256
|
+
[Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.2...HEAD
|
|
257
|
+
[v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
|
|
235
258
|
[v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
|
|
236
259
|
[v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
|
|
237
260
|
[v3.4.2]: https://github.com/rubysolo/dentaku/compare/v3.4.1...v3.4.2
|
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`
|
|
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
|
|
|
@@ -6,6 +6,9 @@ require 'bigdecimal/util'
|
|
|
6
6
|
module Dentaku
|
|
7
7
|
module AST
|
|
8
8
|
class Arithmetic < Operation
|
|
9
|
+
DECIMAL = /\A-?\d*\.\d+\z/.freeze
|
|
10
|
+
INTEGER = /\A-?\d+\z/.freeze
|
|
11
|
+
|
|
9
12
|
def initialize(*)
|
|
10
13
|
super
|
|
11
14
|
|
|
@@ -29,8 +32,14 @@ module Dentaku
|
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def value(context = {})
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
calculate(left.value(context), right.value(context))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def calculate(left_value, right_value)
|
|
41
|
+
l = cast(left_value)
|
|
42
|
+
r = cast(right_value)
|
|
34
43
|
|
|
35
44
|
l.public_send(operator, r)
|
|
36
45
|
rescue ::TypeError => e
|
|
@@ -38,8 +47,6 @@ module Dentaku
|
|
|
38
47
|
raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
|
|
39
48
|
end
|
|
40
49
|
|
|
41
|
-
private
|
|
42
|
-
|
|
43
50
|
def cast(val)
|
|
44
51
|
validate_value(val)
|
|
45
52
|
numeric(val)
|
|
@@ -47,14 +54,23 @@ module Dentaku
|
|
|
47
54
|
|
|
48
55
|
def numeric(val)
|
|
49
56
|
case val.to_s
|
|
50
|
-
when
|
|
51
|
-
when
|
|
57
|
+
when DECIMAL then decimal(val)
|
|
58
|
+
when INTEGER then val.to_i
|
|
52
59
|
else val
|
|
53
60
|
end
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
def decimal(val)
|
|
57
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
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def datetime?(val)
|
|
70
|
+
# val is a Date, Time, or DateTime
|
|
71
|
+
return true if val.respond_to?(:strftime)
|
|
72
|
+
|
|
73
|
+
val.to_s =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
|
|
58
74
|
end
|
|
59
75
|
|
|
60
76
|
def valid_node?(node)
|
|
@@ -102,10 +118,13 @@ module Dentaku
|
|
|
102
118
|
end
|
|
103
119
|
|
|
104
120
|
def value(context = {})
|
|
105
|
-
|
|
106
|
-
|
|
121
|
+
left_value = left.value(context)
|
|
122
|
+
right_value = right.value(context)
|
|
123
|
+
|
|
124
|
+
if left.type == :datetime || datetime?(left_value)
|
|
125
|
+
Dentaku::DateArithmetic.new(left_value).add(right_value)
|
|
107
126
|
else
|
|
108
|
-
|
|
127
|
+
calculate(left_value, right_value)
|
|
109
128
|
end
|
|
110
129
|
end
|
|
111
130
|
end
|
|
@@ -120,10 +139,13 @@ module Dentaku
|
|
|
120
139
|
end
|
|
121
140
|
|
|
122
141
|
def value(context = {})
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
left_value = left.value(context)
|
|
143
|
+
right_value = right.value(context)
|
|
144
|
+
|
|
145
|
+
if left.type == :datetime || datetime?(left_value)
|
|
146
|
+
Dentaku::DateArithmetic.new(left_value).sub(right_value)
|
|
125
147
|
else
|
|
126
|
-
|
|
148
|
+
calculate(left_value, right_value)
|
|
127
149
|
end
|
|
128
150
|
end
|
|
129
151
|
end
|
|
@@ -28,8 +28,7 @@ module Dentaku
|
|
|
28
28
|
|
|
29
29
|
def cast(val)
|
|
30
30
|
return val unless val.is_a?(::String)
|
|
31
|
-
return val
|
|
32
|
-
return val unless val.match?(/\A-?\d*(\.\d+)?\z/)
|
|
31
|
+
return val unless val.match?(Arithmetic::DECIMAL) || val.match?(Arithmetic::INTEGER)
|
|
33
32
|
|
|
34
33
|
v = BigDecimal(val, Float::DIG + 1)
|
|
35
34
|
v = v.to_i if v.frac.zero?
|
|
@@ -8,7 +8,7 @@ module Dentaku
|
|
|
8
8
|
nil
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def register(name, type, implementation)
|
|
11
|
+
def register(name, type, implementation, callback = nil)
|
|
12
12
|
function = Class.new(Function) do
|
|
13
13
|
def self.name=(name)
|
|
14
14
|
@name = name
|
|
@@ -34,6 +34,14 @@ module Dentaku
|
|
|
34
34
|
@type
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def self.callback=(callback)
|
|
38
|
+
@callback = callback
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.callback
|
|
42
|
+
@callback
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
def self.arity
|
|
38
46
|
@implementation.arity < 0 ? nil : @implementation.arity
|
|
39
47
|
end
|
|
@@ -61,6 +69,7 @@ module Dentaku
|
|
|
61
69
|
function.name = name
|
|
62
70
|
function.type = type
|
|
63
71
|
function.implementation = implementation
|
|
72
|
+
function.callback = callback
|
|
64
73
|
|
|
65
74
|
self[function_name(name)] = function
|
|
66
75
|
end
|
|
@@ -9,5 +9,5 @@ Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
|
|
|
9
9
|
), 'AVG() requires at least one argument'
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / flatten_args.length
|
|
12
|
+
flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / BigDecimal(flatten_args.length)
|
|
13
13
|
})
|
|
@@ -12,9 +12,12 @@ module Dentaku
|
|
|
12
12
|
3
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def
|
|
15
|
+
def initialize(*args)
|
|
16
|
+
super
|
|
16
17
|
validate_identifier(@args[1])
|
|
18
|
+
end
|
|
17
19
|
|
|
20
|
+
def dependencies(context = {})
|
|
18
21
|
collection = @args[0]
|
|
19
22
|
item_identifier = @args[1].identifier
|
|
20
23
|
expression = @args[2]
|
|
@@ -28,9 +31,7 @@ module Dentaku
|
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
|
|
31
|
-
unless arg.is_a?(Identifier)
|
|
32
|
-
raise ArgumentError.for(:incompatible_type, value: arg, for: Identifier), message
|
|
33
|
-
end
|
|
34
|
+
raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
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
|
+
})
|
|
@@ -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.rb
CHANGED
|
@@ -15,6 +15,7 @@ require_relative './ast/array'
|
|
|
15
15
|
require_relative './ast/grouping'
|
|
16
16
|
require_relative './ast/case'
|
|
17
17
|
require_relative './ast/function_registry'
|
|
18
|
+
require_relative './ast/functions/abs'
|
|
18
19
|
require_relative './ast/functions/all'
|
|
19
20
|
require_relative './ast/functions/and'
|
|
20
21
|
require_relative './ast/functions/any'
|
|
@@ -23,12 +24,14 @@ require_relative './ast/functions/count'
|
|
|
23
24
|
require_relative './ast/functions/duration'
|
|
24
25
|
require_relative './ast/functions/filter'
|
|
25
26
|
require_relative './ast/functions/if'
|
|
27
|
+
require_relative './ast/functions/intercept'
|
|
26
28
|
require_relative './ast/functions/map'
|
|
27
29
|
require_relative './ast/functions/max'
|
|
28
30
|
require_relative './ast/functions/min'
|
|
29
31
|
require_relative './ast/functions/not'
|
|
30
32
|
require_relative './ast/functions/or'
|
|
31
33
|
require_relative './ast/functions/pluck'
|
|
34
|
+
require_relative './ast/functions/reduce'
|
|
32
35
|
require_relative './ast/functions/round'
|
|
33
36
|
require_relative './ast/functions/rounddown'
|
|
34
37
|
require_relative './ast/functions/roundup'
|
|
@@ -36,4 +39,4 @@ require_relative './ast/functions/ruby_math'
|
|
|
36
39
|
require_relative './ast/functions/string_functions'
|
|
37
40
|
require_relative './ast/functions/sum'
|
|
38
41
|
require_relative './ast/functions/switch'
|
|
39
|
-
require_relative './ast/functions/xor'
|
|
42
|
+
require_relative './ast/functions/xor'
|
data/lib/dentaku/calculator.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Dentaku
|
|
|
10
10
|
class Calculator
|
|
11
11
|
include StringCasing
|
|
12
12
|
attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
|
|
13
|
-
:nested_data_support, :ast_cache
|
|
13
|
+
:nested_data_support, :ast_cache, :raw_date_literals
|
|
14
14
|
|
|
15
15
|
def initialize(options = {})
|
|
16
16
|
clear
|
|
@@ -19,22 +19,28 @@ module Dentaku
|
|
|
19
19
|
@aliases = options.delete(:aliases) || Dentaku.aliases
|
|
20
20
|
@nested_data_support = options.fetch(:nested_data_support, true)
|
|
21
21
|
options.delete(:nested_data_support)
|
|
22
|
+
@raw_date_literals = options.fetch(:raw_date_literals, true)
|
|
23
|
+
options.delete(:raw_date_literals)
|
|
22
24
|
@ast_cache = options
|
|
23
25
|
@disable_ast_cache = false
|
|
24
26
|
@function_registry = Dentaku::AST::FunctionRegistry.new
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
def self.add_function(name, type, body)
|
|
28
|
-
Dentaku::AST::FunctionRegistry.default.register(name, type, body)
|
|
29
|
+
def self.add_function(name, type, body, callback = nil)
|
|
30
|
+
Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
+
def self.add_functions(functions)
|
|
34
|
+
functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_function(name, type, body, callback = nil)
|
|
38
|
+
@function_registry.register(name, type, body, callback)
|
|
33
39
|
self
|
|
34
40
|
end
|
|
35
41
|
|
|
36
|
-
def add_functions(
|
|
37
|
-
|
|
42
|
+
def add_functions(functions)
|
|
43
|
+
functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
|
|
38
44
|
self
|
|
39
45
|
end
|
|
40
46
|
|
|
@@ -57,8 +63,7 @@ module Dentaku
|
|
|
57
63
|
} if expression.is_a? Array
|
|
58
64
|
|
|
59
65
|
store(data) do
|
|
60
|
-
node = expression
|
|
61
|
-
node = ast(node) unless node.is_a?(AST::Node)
|
|
66
|
+
node = ast(expression)
|
|
62
67
|
unbound = node.dependencies(memory)
|
|
63
68
|
unless unbound.empty?
|
|
64
69
|
raise UnboundVariableError.new(unbound),
|
|
@@ -90,13 +95,15 @@ module Dentaku
|
|
|
90
95
|
end
|
|
91
96
|
|
|
92
97
|
def ast(expression)
|
|
98
|
+
return expression if expression.is_a?(AST::Node)
|
|
93
99
|
return expression.map { |e| ast(e) } if expression.is_a? Array
|
|
94
100
|
|
|
95
101
|
@ast_cache.fetch(expression) {
|
|
96
102
|
options = {
|
|
103
|
+
aliases: aliases,
|
|
97
104
|
case_sensitive: case_sensitive,
|
|
98
105
|
function_registry: @function_registry,
|
|
99
|
-
|
|
106
|
+
raw_date_literals: raw_date_literals
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
tokens = tokenizer.tokenize(expression, options)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module Dentaku
|
|
2
2
|
class DateArithmetic
|
|
3
3
|
def initialize(date)
|
|
4
|
-
|
|
4
|
+
if date.respond_to?(:strftime)
|
|
5
|
+
@base = date
|
|
6
|
+
else
|
|
7
|
+
@base = Time.parse(date).to_datetime
|
|
8
|
+
end
|
|
5
9
|
end
|
|
6
10
|
|
|
7
11
|
def add(duration)
|
|
@@ -25,7 +29,7 @@ module Dentaku
|
|
|
25
29
|
|
|
26
30
|
def sub(duration)
|
|
27
31
|
case duration
|
|
28
|
-
when DateTime, Numeric
|
|
32
|
+
when Date, DateTime, Numeric
|
|
29
33
|
@base - duration
|
|
30
34
|
when Dentaku::AST::Duration::Value
|
|
31
35
|
case duration.unit
|
|
@@ -36,6 +40,8 @@ module Dentaku
|
|
|
36
40
|
when :day
|
|
37
41
|
@base - duration.value
|
|
38
42
|
end
|
|
43
|
+
when Dentaku::TokenScanner::DATE_TIME_REGEXP
|
|
44
|
+
@base - Time.parse(duration).to_datetime
|
|
39
45
|
else
|
|
40
46
|
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
|
41
47
|
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
data/lib/dentaku/exceptions.rb
CHANGED
|
@@ -11,6 +11,15 @@ module Dentaku
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
class MathDomainError < Error
|
|
15
|
+
attr_reader :function_name, :args
|
|
16
|
+
|
|
17
|
+
def initialize(function_name, args)
|
|
18
|
+
@function_name = function_name
|
|
19
|
+
@args = args
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
14
23
|
class NodeError < Error
|
|
15
24
|
attr_reader :child, :expect, :actual
|
|
16
25
|
|
|
@@ -58,7 +67,9 @@ module Dentaku
|
|
|
58
67
|
private_class_method :new
|
|
59
68
|
|
|
60
69
|
VALID_REASONS = %i[
|
|
61
|
-
parse_error
|
|
70
|
+
parse_error
|
|
71
|
+
too_many_closing_parentheses
|
|
72
|
+
too_many_opening_parentheses
|
|
62
73
|
unexpected_zero_width_match
|
|
63
74
|
].freeze
|
|
64
75
|
|
|
@@ -83,8 +94,11 @@ module Dentaku
|
|
|
83
94
|
private_class_method :new
|
|
84
95
|
|
|
85
96
|
VALID_REASONS = %i[
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
incompatible_type
|
|
98
|
+
invalid_operator
|
|
99
|
+
invalid_value
|
|
100
|
+
too_few_arguments
|
|
101
|
+
wrong_number_of_arguments
|
|
88
102
|
].freeze
|
|
89
103
|
|
|
90
104
|
def self.for(reason, **meta)
|
data/lib/dentaku/parser.rb
CHANGED
|
@@ -45,22 +45,33 @@ module Dentaku
|
|
|
45
45
|
|
|
46
46
|
operator.peek(output)
|
|
47
47
|
|
|
48
|
+
output_size = output.length
|
|
48
49
|
args_size = operator.arity || count
|
|
49
50
|
min_size = operator.arity || operator.min_param_count || count
|
|
50
51
|
max_size = operator.arity || operator.max_param_count || count
|
|
51
52
|
|
|
52
|
-
if
|
|
53
|
-
|
|
53
|
+
if output_size < min_size || args_size < min_size
|
|
54
|
+
expect = min_size == max_size ? min_size : min_size..max_size
|
|
55
|
+
fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
if
|
|
57
|
-
|
|
58
|
+
if output_size > max_size && operations.empty? || args_size > max_size
|
|
59
|
+
expect = min_size == max_size ? min_size : min_size..max_size
|
|
60
|
+
fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
|
|
58
61
|
end
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
if operator == AST::Array && output.empty?
|
|
64
|
+
output.push(operator.new())
|
|
65
|
+
else
|
|
66
|
+
fail! :invalid_statement if output_size < args_size
|
|
67
|
+
args = Array.new(args_size) { output.pop }.reverse
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
output.push operator.new(*args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if operator.respond_to?(:callback) && !operator.callback.nil?
|
|
73
|
+
operator.callback.call(args)
|
|
74
|
+
end
|
|
64
75
|
rescue ::ArgumentError => e
|
|
65
76
|
raise Dentaku::ArgumentError, e.message
|
|
66
77
|
rescue NodeError => e
|
|
@@ -320,9 +331,9 @@ module Dentaku
|
|
|
320
331
|
when :node_invalid
|
|
321
332
|
"#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
|
|
322
333
|
when :too_few_operands
|
|
323
|
-
"#{meta.fetch(:operator)} has too few operands"
|
|
334
|
+
"#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
|
324
335
|
when :too_many_operands
|
|
325
|
-
"#{meta.fetch(:operator)} has too many operands"
|
|
336
|
+
"#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
|
326
337
|
when :undefined_function
|
|
327
338
|
"Undefined function #{meta.fetch(:function_name)}"
|
|
328
339
|
when :unprocessed_token
|
|
@@ -7,24 +7,31 @@ module Dentaku
|
|
|
7
7
|
|
|
8
8
|
def visit_operation(node)
|
|
9
9
|
if node.left
|
|
10
|
-
visit_operand(node.left, node.class.precedence, suffix: " ")
|
|
10
|
+
visit_operand(node.left, node.class.precedence, suffix: " ", dir: :left)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
@output << node.display_operator
|
|
14
14
|
|
|
15
15
|
if node.right
|
|
16
|
-
visit_operand(node.right, node.class.precedence, prefix: " ")
|
|
16
|
+
visit_operand(node.right, node.class.precedence, prefix: " ", dir: :right)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def visit_operand(node, precedence, prefix: "", suffix: "")
|
|
20
|
+
def visit_operand(node, precedence, prefix: "", suffix: "", dir: :none)
|
|
21
21
|
@output << prefix
|
|
22
|
-
@output << "(" if
|
|
22
|
+
@output << "(" if should_output?(node, precedence, dir == :right)
|
|
23
23
|
node.accept(self)
|
|
24
|
-
@output << ")" if
|
|
24
|
+
@output << ")" if should_output?(node, precedence, dir == :right)
|
|
25
25
|
@output << suffix
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def should_output?(node, precedence, output_on_equal)
|
|
29
|
+
return false unless node.is_a?(Dentaku::AST::Operation)
|
|
30
|
+
|
|
31
|
+
target_precedence = node.class.precedence
|
|
32
|
+
target_precedence < precedence || (output_on_equal && target_precedence == precedence)
|
|
33
|
+
end
|
|
34
|
+
|
|
28
35
|
def visit_function(node)
|
|
29
36
|
@output << node.name
|
|
30
37
|
@output << "("
|
|
@@ -94,6 +101,10 @@ module Dentaku
|
|
|
94
101
|
@output << "NULL"
|
|
95
102
|
end
|
|
96
103
|
|
|
104
|
+
def visit_array(node)
|
|
105
|
+
@output << node.value.to_s
|
|
106
|
+
end
|
|
107
|
+
|
|
97
108
|
def to_s
|
|
98
109
|
@output
|
|
99
110
|
end
|