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