dentaku 3.5.1 → 3.5.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|