dentaku 3.5.0 → 3.5.2
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 +22 -0
- data/README.md +2 -2
- data/lib/dentaku/ast/arithmetic.rb +46 -25
- data/lib/dentaku/ast/bitwise.rb +23 -6
- data/lib/dentaku/ast/comparators.rb +11 -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/ruby_math.rb +2 -0
- data/lib/dentaku/ast.rb +1 -0
- data/lib/dentaku/bulk_expression_solver.rb +1 -5
- data/lib/dentaku/calculator.rb +15 -8
- data/lib/dentaku/date_arithmetic.rb +5 -1
- data/lib/dentaku/exceptions.rb +11 -2
- data/lib/dentaku/parser.rb +19 -7
- data/lib/dentaku/print_visitor.rb +16 -5
- data/lib/dentaku/token_scanner.rb +8 -4
- 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/all_spec.rb +1 -1
- data/spec/ast/any_spec.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +20 -5
- data/spec/ast/avg_spec.rb +5 -0
- data/spec/ast/comparator_spec.rb +8 -0
- data/spec/ast/filter_spec.rb +1 -1
- data/spec/ast/map_spec.rb +1 -1
- data/spec/ast/or_spec.rb +1 -1
- data/spec/ast/pluck_spec.rb +1 -1
- data/spec/bulk_expression_solver_spec.rb +9 -0
- data/spec/calculator_spec.rb +70 -16
- data/spec/external_function_spec.rb +89 -18
- data/spec/print_visitor_spec.rb +6 -0
- data/spec/tokenizer_spec.rb +12 -0
- data/spec/visitor/infix_spec.rb +22 -1
- data/spec/visitor_spec.rb +3 -2
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b10f7d9e6a9d200c283dcf077fd56e8f4abe4922c02e3095ba20dbb29f6b81c
|
|
4
|
+
data.tar.gz: add2d3bf7c462edefb9a4c52d79595d3a161e1d78046dbb1fe90e8aa9979a13b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48c2571ea61f8bb9f8a7a4483b8d588741f2c44c4fdc2d04ecd2a5c5b75a94f414b5772a3e1dca21898f8d2ed6488e54925c6a689bfd721315c0c8e0992991d7
|
|
7
|
+
data.tar.gz: b469e9c4a69c6083b93cda29ea8bf5bf4ad9c91e4a6362e5880768492e21ecd476a09eb858134b8e6c78017207c6cf50479c7ebb3af661c7e20f3866f034b49a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## [v3.5.2]
|
|
4
|
+
- add ABS function
|
|
5
|
+
- add array support for AST visitors
|
|
6
|
+
- add support for function callbacks
|
|
7
|
+
- improve support for date / time values
|
|
8
|
+
- improve error messaging for invalid arity
|
|
9
|
+
- improve AVG function accuracy
|
|
10
|
+
- validate enum arguments at parse time
|
|
11
|
+
- support adding multiple functions at once to global registry
|
|
12
|
+
- fix bug in print visitor precedence checking
|
|
13
|
+
- fix handling of Math::DomainError
|
|
14
|
+
- fix invalid cast
|
|
15
|
+
|
|
16
|
+
## [v3.5.1]
|
|
17
|
+
- add bitwise shift left and shift right operators
|
|
18
|
+
- improve numeric conversions
|
|
19
|
+
- improve parse exceptions
|
|
20
|
+
- improve bitwise exceptions
|
|
21
|
+
- include variable name in bulk expression exceptions
|
|
22
|
+
|
|
3
23
|
## [v3.5.0]
|
|
4
24
|
- fix bug with function argument count
|
|
5
25
|
- add XOR operator
|
|
@@ -224,6 +244,8 @@
|
|
|
224
244
|
## [v0.1.0] 2012-01-20
|
|
225
245
|
- initial release
|
|
226
246
|
|
|
247
|
+
[v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
|
|
248
|
+
[v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
|
|
227
249
|
[v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
|
|
228
250
|
[v3.4.2]: https://github.com/rubysolo/dentaku/compare/v3.4.1...v3.4.2
|
|
229
251
|
[v3.4.1]: https://github.com/rubysolo/dentaku/compare/v3.4.0...v3.4.1
|
data/README.md
CHANGED
|
@@ -137,7 +137,7 @@ application, AST caching will consume more memory with each new formula.
|
|
|
137
137
|
BUILT-IN OPERATORS AND FUNCTIONS
|
|
138
138
|
---------------------------------
|
|
139
139
|
|
|
140
|
-
Math: `+`, `-`, `*`, `/`, `%`, `^`, `|`,
|
|
140
|
+
Math: `+`, `-`, `*`, `/`, `%`, `^`, `|`, `&`, `<<`, `>>`
|
|
141
141
|
|
|
142
142
|
Also, all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
|
|
143
143
|
|
|
@@ -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`
|
|
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,31 +32,43 @@ module Dentaku
|
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def value(context = {})
|
|
32
|
-
|
|
33
|
-
r = cast(right.value(context))
|
|
34
|
-
begin
|
|
35
|
-
l.public_send(operator, r)
|
|
36
|
-
rescue ::TypeError => e
|
|
37
|
-
# Right cannot be converted to a suitable type for left. e.g. [] + 1
|
|
38
|
-
raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
|
|
39
|
-
end
|
|
35
|
+
calculate(left.value(context), right.value(context))
|
|
40
36
|
end
|
|
41
37
|
|
|
42
38
|
private
|
|
43
39
|
|
|
44
|
-
def
|
|
40
|
+
def calculate(left_value, right_value)
|
|
41
|
+
l = cast(left_value)
|
|
42
|
+
r = cast(right_value)
|
|
43
|
+
|
|
44
|
+
l.public_send(operator, r)
|
|
45
|
+
rescue ::TypeError => e
|
|
46
|
+
# Right cannot be converted to a suitable type for left. e.g. [] + 1
|
|
47
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cast(val)
|
|
45
51
|
validate_value(val)
|
|
46
|
-
numeric(val
|
|
52
|
+
numeric(val)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def numeric(val)
|
|
56
|
+
case val.to_s
|
|
57
|
+
when DECIMAL then decimal(val)
|
|
58
|
+
when INTEGER then val.to_i
|
|
59
|
+
else val
|
|
60
|
+
end
|
|
47
61
|
end
|
|
48
62
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
def decimal(val)
|
|
64
|
+
BigDecimal(val.to_s, Float::DIG + 1)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def datetime?(val)
|
|
68
|
+
# val is a Date, Time, or DateTime
|
|
69
|
+
return true if val.respond_to?(:strftime)
|
|
70
|
+
|
|
71
|
+
val.to_s =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
def valid_node?(node)
|
|
@@ -101,10 +116,13 @@ module Dentaku
|
|
|
101
116
|
end
|
|
102
117
|
|
|
103
118
|
def value(context = {})
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
left_value = left.value(context)
|
|
120
|
+
right_value = right.value(context)
|
|
121
|
+
|
|
122
|
+
if left.type == :datetime || datetime?(left_value)
|
|
123
|
+
Dentaku::DateArithmetic.new(left_value).add(right_value)
|
|
106
124
|
else
|
|
107
|
-
|
|
125
|
+
calculate(left_value, right_value)
|
|
108
126
|
end
|
|
109
127
|
end
|
|
110
128
|
end
|
|
@@ -119,10 +137,13 @@ module Dentaku
|
|
|
119
137
|
end
|
|
120
138
|
|
|
121
139
|
def value(context = {})
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
left_value = left.value(context)
|
|
141
|
+
right_value = right.value(context)
|
|
142
|
+
|
|
143
|
+
if left.type == :datetime || datetime?(left_value)
|
|
144
|
+
Dentaku::DateArithmetic.new(left_value).sub(right_value)
|
|
124
145
|
else
|
|
125
|
-
|
|
146
|
+
calculate(left_value, right_value)
|
|
126
147
|
end
|
|
127
148
|
end
|
|
128
149
|
end
|
|
@@ -143,7 +164,7 @@ module Dentaku
|
|
|
143
164
|
end
|
|
144
165
|
|
|
145
166
|
def value(context = {})
|
|
146
|
-
r = cast(right.value(context)
|
|
167
|
+
r = decimal(cast(right.value(context)))
|
|
147
168
|
raise Dentaku::ZeroDivisionError if r.zero?
|
|
148
169
|
|
|
149
170
|
cast(cast(left.value(context)) / r)
|
data/lib/dentaku/ast/bitwise.rb
CHANGED
|
@@ -2,23 +2,40 @@ require_relative './operation'
|
|
|
2
2
|
|
|
3
3
|
module Dentaku
|
|
4
4
|
module AST
|
|
5
|
-
class
|
|
5
|
+
class Bitwise < Operation
|
|
6
6
|
def value(context = {})
|
|
7
|
-
|
|
7
|
+
left_value = left.value(context)
|
|
8
|
+
right_value = right.value(context)
|
|
9
|
+
|
|
10
|
+
left_value.public_send(operator, right_value)
|
|
11
|
+
rescue NoMethodError => e
|
|
12
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, value: left_value, for: left_value.class)
|
|
13
|
+
rescue TypeError => e
|
|
14
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, value: right_value, for: right_value.class)
|
|
8
15
|
end
|
|
16
|
+
end
|
|
9
17
|
|
|
18
|
+
class BitwiseOr < Bitwise
|
|
10
19
|
def operator
|
|
11
20
|
:|
|
|
12
21
|
end
|
|
13
22
|
end
|
|
14
23
|
|
|
15
|
-
class BitwiseAnd <
|
|
16
|
-
def
|
|
17
|
-
|
|
24
|
+
class BitwiseAnd < Bitwise
|
|
25
|
+
def operator
|
|
26
|
+
:&
|
|
18
27
|
end
|
|
28
|
+
end
|
|
19
29
|
|
|
30
|
+
class BitwiseShiftLeft < Bitwise
|
|
20
31
|
def operator
|
|
21
|
-
|
|
32
|
+
:<<
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class BitwiseShiftRight < Bitwise
|
|
37
|
+
def operator
|
|
38
|
+
:>>
|
|
22
39
|
end
|
|
23
40
|
end
|
|
24
41
|
end
|
|
@@ -16,8 +16,8 @@ module Dentaku
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def value(context = {})
|
|
19
|
-
l = validate_value(left.value(context))
|
|
20
|
-
r = validate_value(right.value(context))
|
|
19
|
+
l = validate_value(cast(left.value(context)))
|
|
20
|
+
r = validate_value(cast(right.value(context)))
|
|
21
21
|
|
|
22
22
|
l.public_send(operator, r)
|
|
23
23
|
rescue ::ArgumentError => e
|
|
@@ -26,6 +26,15 @@ module Dentaku
|
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
|
+
def cast(val)
|
|
30
|
+
return val unless val.is_a?(::String)
|
|
31
|
+
return val unless val.match?(Arithmetic::DECIMAL) || val.match?(Arithmetic::INTEGER)
|
|
32
|
+
|
|
33
|
+
v = BigDecimal(val, Float::DIG + 1)
|
|
34
|
+
v = v.to_i if v.frac.zero?
|
|
35
|
+
v
|
|
36
|
+
end
|
|
37
|
+
|
|
29
38
|
def validate_value(value)
|
|
30
39
|
unless value.respond_to?(operator)
|
|
31
40
|
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator),
|
|
@@ -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
|
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'
|
|
@@ -89,13 +89,9 @@ module Dentaku
|
|
|
89
89
|
def with_rescues(var_name, results, block)
|
|
90
90
|
yield
|
|
91
91
|
|
|
92
|
-
rescue UnboundVariableError,
|
|
92
|
+
rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
|
|
93
93
|
ex.recipient_variable = var_name
|
|
94
94
|
results[var_name] = block.call(ex)
|
|
95
|
-
|
|
96
|
-
rescue Dentaku::ArgumentError => ex
|
|
97
|
-
results[var_name] = block.call(ex)
|
|
98
|
-
|
|
99
95
|
ensure
|
|
100
96
|
if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
|
|
101
97
|
results[var_name] = calculator.memory[var_name.downcase]
|
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
|
|
|
@@ -94,9 +100,10 @@ module Dentaku
|
|
|
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)
|
data/lib/dentaku/exceptions.rb
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
module Dentaku
|
|
2
2
|
class Error < StandardError
|
|
3
|
+
attr_accessor :recipient_variable
|
|
3
4
|
end
|
|
4
5
|
|
|
5
6
|
class UnboundVariableError < Error
|
|
6
|
-
attr_accessor :recipient_variable
|
|
7
|
-
|
|
8
7
|
attr_reader :unbound_variables
|
|
9
8
|
|
|
10
9
|
def initialize(unbound_variables)
|
|
@@ -12,6 +11,15 @@ module Dentaku
|
|
|
12
11
|
end
|
|
13
12
|
end
|
|
14
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
|
+
|
|
15
23
|
class NodeError < Error
|
|
16
24
|
attr_reader :child, :expect, :actual
|
|
17
25
|
|
|
@@ -74,6 +82,7 @@ module Dentaku
|
|
|
74
82
|
|
|
75
83
|
class ArgumentError < ::ArgumentError
|
|
76
84
|
attr_reader :reason, :meta
|
|
85
|
+
attr_accessor :recipient_variable
|
|
77
86
|
|
|
78
87
|
def initialize(reason, **meta)
|
|
79
88
|
@reason = reason
|
data/lib/dentaku/parser.rb
CHANGED
|
@@ -10,8 +10,11 @@ module Dentaku
|
|
|
10
10
|
pow: AST::Exponentiation,
|
|
11
11
|
negate: AST::Negation,
|
|
12
12
|
mod: AST::Modulo,
|
|
13
|
+
|
|
13
14
|
bitor: AST::BitwiseOr,
|
|
14
15
|
bitand: AST::BitwiseAnd,
|
|
16
|
+
bitshiftleft: AST::BitwiseShiftLeft,
|
|
17
|
+
bitshiftright: AST::BitwiseShiftRight,
|
|
15
18
|
|
|
16
19
|
lt: AST::LessThan,
|
|
17
20
|
gt: AST::GreaterThan,
|
|
@@ -38,24 +41,33 @@ module Dentaku
|
|
|
38
41
|
|
|
39
42
|
def consume(count = 2)
|
|
40
43
|
operator = operations.pop
|
|
44
|
+
fail! :invalid_statement if operator.nil?
|
|
45
|
+
|
|
41
46
|
operator.peek(output)
|
|
42
47
|
|
|
48
|
+
output_size = output.length
|
|
43
49
|
args_size = operator.arity || count
|
|
44
50
|
min_size = operator.arity || operator.min_param_count || count
|
|
45
51
|
max_size = operator.arity || operator.max_param_count || count
|
|
46
52
|
|
|
47
|
-
if
|
|
48
|
-
|
|
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
|
|
49
56
|
end
|
|
50
57
|
|
|
51
|
-
if
|
|
52
|
-
|
|
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
|
|
53
61
|
end
|
|
54
62
|
|
|
55
|
-
fail! :invalid_statement if
|
|
63
|
+
fail! :invalid_statement if output_size < args_size
|
|
56
64
|
args = Array.new(args_size) { output.pop }.reverse
|
|
57
65
|
|
|
58
66
|
output.push operator.new(*args)
|
|
67
|
+
|
|
68
|
+
if operator.respond_to?(:callback) && !operator.callback.nil?
|
|
69
|
+
operator.callback.call(args)
|
|
70
|
+
end
|
|
59
71
|
rescue ::ArgumentError => e
|
|
60
72
|
raise Dentaku::ArgumentError, e.message
|
|
61
73
|
rescue NodeError => e
|
|
@@ -315,9 +327,9 @@ module Dentaku
|
|
|
315
327
|
when :node_invalid
|
|
316
328
|
"#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
|
|
317
329
|
when :too_few_operands
|
|
318
|
-
"#{meta.fetch(:operator)} has too few operands"
|
|
330
|
+
"#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
|
319
331
|
when :too_many_operands
|
|
320
|
-
"#{meta.fetch(:operator)} has too many operands"
|
|
332
|
+
"#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
|
321
333
|
when :undefined_function
|
|
322
334
|
"Undefined function #{meta.fetch(:function_name)}"
|
|
323
335
|
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
|
|
@@ -7,6 +7,8 @@ module Dentaku
|
|
|
7
7
|
class TokenScanner
|
|
8
8
|
extend StringCasing
|
|
9
9
|
|
|
10
|
+
DATE_TIME_REGEXP = /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?(?!\d)/.freeze
|
|
11
|
+
|
|
10
12
|
def initialize(category, regexp, converter = nil, condition = nil)
|
|
11
13
|
@category = category
|
|
12
14
|
@regexp = %r{\A(#{ regexp })}i
|
|
@@ -73,7 +75,9 @@ module Dentaku
|
|
|
73
75
|
|
|
74
76
|
def scanners(options = {})
|
|
75
77
|
@case_sensitive = options.fetch(:case_sensitive, false)
|
|
76
|
-
|
|
78
|
+
raw_date_literals = options.fetch(:raw_date_literals, true)
|
|
79
|
+
|
|
80
|
+
@scanners.select { |k, _| raw_date_literals || k != :datetime }.values
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
def whitespace
|
|
@@ -86,7 +90,7 @@ module Dentaku
|
|
|
86
90
|
|
|
87
91
|
# NOTE: Convert to DateTime as Array(Time) returns the parts of the time for some reason
|
|
88
92
|
def datetime
|
|
89
|
-
new(:datetime,
|
|
93
|
+
new(:datetime, DATE_TIME_REGEXP, lambda { |raw| Time.parse(raw).to_datetime })
|
|
90
94
|
end
|
|
91
95
|
|
|
92
96
|
def numeric
|
|
@@ -120,9 +124,9 @@ module Dentaku
|
|
|
120
124
|
|
|
121
125
|
def operator
|
|
122
126
|
names = {
|
|
123
|
-
pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&'
|
|
127
|
+
pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&', bitshiftleft: '<<', bitshiftright: '>>'
|
|
124
128
|
}.invert
|
|
125
|
-
new(:operator, '
|
|
129
|
+
new(:operator, '\^|\+|-|\*|\/|%|\||&|<<|>>', lambda { |raw| names[raw] })
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
def grouping
|
data/lib/dentaku/tokenizer.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'dentaku/token_scanner'
|
|
|
4
4
|
|
|
5
5
|
module Dentaku
|
|
6
6
|
class Tokenizer
|
|
7
|
-
attr_reader :
|
|
7
|
+
attr_reader :aliases
|
|
8
8
|
|
|
9
9
|
LPAREN = TokenMatcher.new(:grouping, :open)
|
|
10
10
|
RPAREN = TokenMatcher.new(:grouping, :close)
|
|
@@ -15,10 +15,14 @@ module Dentaku
|
|
|
15
15
|
@aliases = options.fetch(:aliases, global_aliases)
|
|
16
16
|
input = strip_comments(string.to_s.dup)
|
|
17
17
|
input = replace_aliases(input)
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
scanner_options = {
|
|
20
|
+
case_sensitive: options.fetch(:case_sensitive, false),
|
|
21
|
+
raw_date_literals: options.fetch(:raw_date_literals, true)
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
until input.empty?
|
|
21
|
-
scanned = TokenScanner.scanners(
|
|
25
|
+
scanned = TokenScanner.scanners(scanner_options).any? do |scanner|
|
|
22
26
|
scanned, input = scan(input, scanner)
|
|
23
27
|
scanned
|
|
24
28
|
end
|
data/lib/dentaku/version.rb
CHANGED