dentaku 3.5.1 → 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 +14 -1
- data/README.md +1 -1
- data/lib/dentaku/ast/arithmetic.rb +32 -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/ruby_math.rb +2 -0
- data/lib/dentaku/ast.rb +1 -0
- data/lib/dentaku/calculator.rb +15 -8
- data/lib/dentaku/date_arithmetic.rb +5 -1
- data/lib/dentaku/exceptions.rb +9 -0
- data/lib/dentaku/parser.rb +14 -7
- data/lib/dentaku/print_visitor.rb +16 -5
- data/lib/dentaku/token_scanner.rb +6 -2
- 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 +18 -10
- data/spec/ast/avg_spec.rb +5 -0
- data/spec/ast/filter_spec.rb +1 -1
- data/spec/ast/map_spec.rb +1 -1
- data/spec/ast/pluck_spec.rb +1 -1
- data/spec/calculator_spec.rb +57 -16
- data/spec/external_function_spec.rb +89 -18
- data/spec/print_visitor_spec.rb +6 -0
- data/spec/visitor/infix_spec.rb +22 -1
- data/spec/visitor_spec.rb +1 -1
- metadata +5 -2
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,18 @@
|
|
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
|
+
|
3
16
|
## [v3.5.1]
|
4
17
|
- add bitwise shift left and shift right operators
|
5
18
|
- improve numeric conversions
|
@@ -231,7 +244,7 @@
|
|
231
244
|
## [v0.1.0] 2012-01-20
|
232
245
|
- initial release
|
233
246
|
|
234
|
-
[
|
247
|
+
[v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
|
235
248
|
[v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
|
236
249
|
[v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
|
237
250
|
[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`
|
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,8 +54,8 @@ 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
|
@@ -57,6 +64,13 @@ module Dentaku
|
|
57
64
|
BigDecimal(val.to_s, Float::DIG + 1)
|
58
65
|
end
|
59
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
|
72
|
+
end
|
73
|
+
|
60
74
|
def valid_node?(node)
|
61
75
|
node && (node.type == :numeric || node.type == :integer || node.dependencies.any?)
|
62
76
|
end
|
@@ -102,10 +116,13 @@ module Dentaku
|
|
102
116
|
end
|
103
117
|
|
104
118
|
def value(context = {})
|
105
|
-
|
106
|
-
|
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)
|
107
124
|
else
|
108
|
-
|
125
|
+
calculate(left_value, right_value)
|
109
126
|
end
|
110
127
|
end
|
111
128
|
end
|
@@ -120,10 +137,13 @@ module Dentaku
|
|
120
137
|
end
|
121
138
|
|
122
139
|
def value(context = {})
|
123
|
-
|
124
|
-
|
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)
|
125
145
|
else
|
126
|
-
|
146
|
+
calculate(left_value, right_value)
|
127
147
|
end
|
128
148
|
end
|
129
149
|
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
|
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'
|
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
@@ -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
|
|
data/lib/dentaku/parser.rb
CHANGED
@@ -45,22 +45,29 @@ 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
|
-
fail! :invalid_statement if
|
63
|
+
fail! :invalid_statement if output_size < args_size
|
61
64
|
args = Array.new(args_size) { output.pop }.reverse
|
62
65
|
|
63
66
|
output.push operator.new(*args)
|
67
|
+
|
68
|
+
if operator.respond_to?(:callback) && !operator.callback.nil?
|
69
|
+
operator.callback.call(args)
|
70
|
+
end
|
64
71
|
rescue ::ArgumentError => e
|
65
72
|
raise Dentaku::ArgumentError, e.message
|
66
73
|
rescue NodeError => e
|
@@ -320,9 +327,9 @@ module Dentaku
|
|
320
327
|
when :node_invalid
|
321
328
|
"#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
|
322
329
|
when :too_few_operands
|
323
|
-
"#{meta.fetch(:operator)} has too few operands"
|
330
|
+
"#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
324
331
|
when :too_many_operands
|
325
|
-
"#{meta.fetch(:operator)} has too many operands"
|
332
|
+
"#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
326
333
|
when :undefined_function
|
327
334
|
"Undefined function #{meta.fetch(:function_name)}"
|
328
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
|
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
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/abs'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe 'Dentaku::AST::Function::Abs' do
|
6
|
+
it 'returns the absolute value of number' do
|
7
|
+
result = Dentaku('ABS(-4.2)')
|
8
|
+
expect(result).to eq(4.2)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns the correct value for positive number' do
|
12
|
+
result = Dentaku('ABS(1.3)')
|
13
|
+
expect(result).to eq(1.3)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the correct value for zero' do
|
17
|
+
result = Dentaku('ABS(0)')
|
18
|
+
expect(result).to eq(0)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'checking errors' do
|
22
|
+
it 'raises an error if argument is not numeric' do
|
23
|
+
expect { Dentaku!("ABS(2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/ast/all_spec.rb
CHANGED
@@ -19,7 +19,7 @@ describe Dentaku::AST::All do
|
|
19
19
|
|
20
20
|
it 'raises argument error if a string is passed as identifier' do
|
21
21
|
expect { calculator.evaluate!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
|
22
|
-
Dentaku::
|
22
|
+
Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
|
23
23
|
)
|
24
24
|
end
|
25
25
|
end
|
data/spec/ast/any_spec.rb
CHANGED
@@ -18,6 +18,6 @@ describe Dentaku::AST::Any do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'raises argument error if a string is passed as identifier' do
|
21
|
-
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::
|
21
|
+
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
|
22
22
|
end
|
23
23
|
end
|
data/spec/ast/arithmetic_spec.rb
CHANGED
@@ -4,12 +4,12 @@ require 'dentaku/ast/arithmetic'
|
|
4
4
|
require 'dentaku/token'
|
5
5
|
|
6
6
|
describe Dentaku::AST::Arithmetic do
|
7
|
-
let(:one) { Dentaku::AST::Numeric.new
|
8
|
-
let(:two) { Dentaku::AST::Numeric.new
|
9
|
-
let(:x) { Dentaku::AST::Identifier.new
|
10
|
-
let(:y) { Dentaku::AST::Identifier.new
|
7
|
+
let(:one) { Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, 1)) }
|
8
|
+
let(:two) { Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, 2)) }
|
9
|
+
let(:x) { Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, 'x')) }
|
10
|
+
let(:y) { Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, 'y')) }
|
11
11
|
let(:ctx) { {'x' => 1, 'y' => 2} }
|
12
|
-
let(:date) { Dentaku::AST::DateTime.new
|
12
|
+
let(:date) { Dentaku::AST::DateTime.new(Dentaku::Token.new(:datetime, DateTime.new(2020, 4, 16))) }
|
13
13
|
|
14
14
|
it 'performs an arithmetic operation with numeric operands' do
|
15
15
|
expect(add(one, two)).to eq(3)
|
@@ -46,12 +46,20 @@ describe Dentaku::AST::Arithmetic do
|
|
46
46
|
expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
|
47
47
|
expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
|
48
48
|
|
49
|
-
int_one = Dentaku::AST::Numeric.new
|
50
|
-
|
49
|
+
int_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "1"))
|
50
|
+
int_neg_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "-1"))
|
51
|
+
decimal_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "1.0"))
|
52
|
+
decimal_neg_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "-1.0"))
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
[int_one, int_neg_one].permutation(2).each do |(left, right)|
|
55
|
+
expect(add(left, right).class).to eq(Integer)
|
56
|
+
end
|
57
|
+
|
58
|
+
[decimal_one, decimal_neg_one].each do |left|
|
59
|
+
[int_one, int_neg_one, decimal_one, decimal_neg_one].each do |right|
|
60
|
+
expect(add(left, right).class).to eq(BigDecimal)
|
61
|
+
end
|
62
|
+
end
|
55
63
|
end
|
56
64
|
|
57
65
|
it 'performs arithmetic on arrays' do
|
data/spec/ast/avg_spec.rb
CHANGED
@@ -3,6 +3,11 @@ require 'dentaku/ast/functions/avg'
|
|
3
3
|
require 'dentaku'
|
4
4
|
|
5
5
|
describe 'Dentaku::AST::Function::Avg' do
|
6
|
+
it 'returns the average of an array of Numeric values as BigDecimal' do
|
7
|
+
result = Dentaku('AVG(1, 2)')
|
8
|
+
expect(result).to eq(1.5)
|
9
|
+
end
|
10
|
+
|
6
11
|
it 'returns the average of an array of Numeric values' do
|
7
12
|
result = Dentaku('AVG(1, x, 1.8)', x: 2.3)
|
8
13
|
expect(result).to eq(1.7)
|
data/spec/ast/filter_spec.rb
CHANGED
@@ -19,7 +19,7 @@ describe Dentaku::AST::Filter do
|
|
19
19
|
|
20
20
|
it 'raises argument error if a string is passed as identifier' do
|
21
21
|
expect { calculator.evaluate!('FILTER({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
|
22
|
-
Dentaku::
|
22
|
+
Dentaku::ParseError, 'FILTER() requires second argument to be an identifier'
|
23
23
|
)
|
24
24
|
end
|
25
25
|
end
|
data/spec/ast/map_spec.rb
CHANGED
@@ -21,7 +21,7 @@ describe Dentaku::AST::Map do
|
|
21
21
|
|
22
22
|
it 'raises argument error if a string is passed as identifier' do
|
23
23
|
expect { calculator.evaluate!('MAP({1, 2, 3}, "val", val + 1)') }.to raise_error(
|
24
|
-
Dentaku::
|
24
|
+
Dentaku::ParseError, 'MAP() requires second argument to be an identifier'
|
25
25
|
)
|
26
26
|
end
|
27
27
|
end
|
data/spec/ast/pluck_spec.rb
CHANGED
@@ -21,7 +21,7 @@ describe Dentaku::AST::Pluck do
|
|
21
21
|
expect do Dentaku.evaluate!('PLUCK(users, "age")', users: [
|
22
22
|
{name: "Bob", age: 44},
|
23
23
|
{name: "Jane", age: 27}
|
24
|
-
]) end.to raise_error(Dentaku::
|
24
|
+
]) end.to raise_error(Dentaku::ParseError, 'PLUCK() requires second argument to be an identifier')
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'raises argument error if a non array of hashes is passed as collection' do
|
data/spec/calculator_spec.rb
CHANGED
@@ -22,6 +22,7 @@ describe Dentaku::Calculator do
|
|
22
22
|
expect(calculator.evaluate('(2 + 3) - 1')).to eq(4)
|
23
23
|
expect(calculator.evaluate('(-2 + 3) - 1')).to eq(0)
|
24
24
|
expect(calculator.evaluate('(-2 - 3) - 1')).to eq(-6)
|
25
|
+
expect(calculator.evaluate('1353+91-1-3322-22')).to eq(-1901)
|
25
26
|
expect(calculator.evaluate('1 + -(2 ^ 2)')).to eq(-3)
|
26
27
|
expect(calculator.evaluate('3 + -num', num: 2)).to eq(1)
|
27
28
|
expect(calculator.evaluate('-num + 3', num: 2)).to eq(1)
|
@@ -329,6 +330,11 @@ describe Dentaku::Calculator do
|
|
329
330
|
}.not_to raise_error
|
330
331
|
end
|
331
332
|
|
333
|
+
it 'allows to compare "-" or "-."' do
|
334
|
+
expect { calculator.solve("IF('-' = '-', 0, 1)") }.not_to raise_error
|
335
|
+
expect { calculator.solve("IF('-.'= '-.', 0, 1)") }.not_to raise_error
|
336
|
+
end
|
337
|
+
|
332
338
|
it "integrates with custom functions" do
|
333
339
|
calculator.add_function(:custom, :integer, -> { 1 })
|
334
340
|
|
@@ -440,19 +446,42 @@ describe Dentaku::Calculator do
|
|
440
446
|
expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_truthy
|
441
447
|
end
|
442
448
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
449
|
+
describe 'disabling date literals' do
|
450
|
+
it 'does not parse formulas with minus signs as dates' do
|
451
|
+
calculator = described_class.new(raw_date_literals: false)
|
452
|
+
expect(calculator.evaluate!('2020-01-01')).to eq(2018)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
describe 'supports date arithmetic' do
|
457
|
+
it 'from hardcoded string' do
|
458
|
+
expect(calculator.evaluate!('2020-01-01 + 30').to_date).to eq(Time.local(2020, 1, 31).to_date)
|
459
|
+
expect(calculator.evaluate!('2020-01-01 - 1').to_date).to eq(Time.local(2019, 12, 31).to_date)
|
460
|
+
expect(calculator.evaluate!('2020-01-01 - 2019-12-31')).to eq(1)
|
461
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, day)').to_date).to eq(Time.local(2020, 1, 2).to_date)
|
462
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, day)').to_date).to eq(Time.local(2019, 12, 31).to_date)
|
463
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, days)').to_date).to eq(Time.local(2020, 1, 31).to_date)
|
464
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, month)').to_date).to eq(Time.local(2020, 2, 1).to_date)
|
465
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, month)').to_date).to eq(Time.local(2019, 12, 1).to_date)
|
466
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, months)').to_date).to eq(Time.local(2022, 7, 1).to_date)
|
467
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, year)').to_date).to eq(Time.local(2021, 1, 1).to_date)
|
468
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, year)').to_date).to eq(Time.local(2019, 1, 1).to_date)
|
469
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, years)').to_date).to eq(Time.local(2050, 1, 1).to_date)
|
470
|
+
end
|
471
|
+
|
472
|
+
it 'from string variable' do
|
473
|
+
value = '2023-01-01'
|
474
|
+
|
475
|
+
expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eql(Date.parse('2023-02-01'))
|
476
|
+
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eql(Date.parse('2022-12-01'))
|
477
|
+
end
|
478
|
+
|
479
|
+
it 'from date object' do
|
480
|
+
value = Date.parse('2023-01-01').to_date
|
481
|
+
|
482
|
+
expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eql(Date.parse('2023-02-01'))
|
483
|
+
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eql(Date.parse('2022-12-01'))
|
484
|
+
end
|
456
485
|
end
|
457
486
|
|
458
487
|
describe 'functions' do
|
@@ -471,6 +500,13 @@ describe Dentaku::Calculator do
|
|
471
500
|
expect(calculator.evaluate('ROUND(apples * 0.93)', apples: 10)).to eq(9)
|
472
501
|
end
|
473
502
|
|
503
|
+
it 'include ABS' do
|
504
|
+
expect(calculator.evaluate('abs(-2.2)')).to eq(2.2)
|
505
|
+
expect(calculator.evaluate('abs(5)')).to eq(5)
|
506
|
+
|
507
|
+
expect(calculator.evaluate('ABS(x * -1)', x: 10)).to eq(10)
|
508
|
+
end
|
509
|
+
|
474
510
|
it 'include NOT' do
|
475
511
|
expect(calculator.evaluate('NOT(some_boolean)', some_boolean: true)).to be_falsey
|
476
512
|
expect(calculator.evaluate('NOT(some_boolean)', some_boolean: false)).to be_truthy
|
@@ -758,9 +794,9 @@ describe Dentaku::Calculator do
|
|
758
794
|
end
|
759
795
|
end
|
760
796
|
|
761
|
-
describe 'math
|
797
|
+
describe 'math support' do
|
762
798
|
Math.methods(false).each do |method|
|
763
|
-
it method do
|
799
|
+
it "includes `#{method}`" do
|
764
800
|
if Math.method(method).arity == 2
|
765
801
|
expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq(Math.send(method, 1, 2))
|
766
802
|
expect(calculator.evaluate("#{method}(x,y) + 1", x: 1, y: '2')).to be_within(0.00001).of(Math.send(method, 1, 2) + 1)
|
@@ -774,11 +810,16 @@ describe Dentaku::Calculator do
|
|
774
810
|
end
|
775
811
|
end
|
776
812
|
|
777
|
-
it '
|
813
|
+
it 'defines a properly named class to support AST marshaling' do
|
778
814
|
expect {
|
779
815
|
Marshal.dump(calculator.ast('SQRT(20)'))
|
780
816
|
}.not_to raise_error
|
781
817
|
end
|
818
|
+
|
819
|
+
it 'properly handles a Math::DomainError' do
|
820
|
+
expect(calculator.evaluate('asin(2)')).to be_nil
|
821
|
+
expect { calculator.evaluate!('asin(2)') }.to raise_error(Dentaku::MathDomainError)
|
822
|
+
end
|
782
823
|
end
|
783
824
|
|
784
825
|
describe 'disable_cache' do
|
@@ -5,8 +5,7 @@ require 'dentaku/calculator'
|
|
5
5
|
describe Dentaku::Calculator do
|
6
6
|
describe 'functions' do
|
7
7
|
describe 'external functions' do
|
8
|
-
|
9
|
-
let(:with_external_funcs) do
|
8
|
+
let(:custom_calculator) do
|
10
9
|
c = described_class.new
|
11
10
|
|
12
11
|
c.add_function(:now, :string, -> { Time.now.to_s })
|
@@ -22,30 +21,30 @@ describe Dentaku::Calculator do
|
|
22
21
|
end
|
23
22
|
|
24
23
|
it 'includes NOW' do
|
25
|
-
now =
|
24
|
+
now = custom_calculator.evaluate('NOW()')
|
26
25
|
expect(now).not_to be_nil
|
27
26
|
expect(now).not_to be_empty
|
28
27
|
end
|
29
28
|
|
30
29
|
it 'includes POW' do
|
31
|
-
expect(
|
32
|
-
expect(
|
33
|
-
expect(
|
30
|
+
expect(custom_calculator.evaluate('POW(2,3)')).to eq(8)
|
31
|
+
expect(custom_calculator.evaluate('POW(3,2)')).to eq(9)
|
32
|
+
expect(custom_calculator.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
|
34
33
|
end
|
35
34
|
|
36
35
|
it 'includes BIGGEST' do
|
37
|
-
expect(
|
36
|
+
expect(custom_calculator.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
|
38
37
|
end
|
39
38
|
|
40
39
|
it 'includes SMALLEST' do
|
41
|
-
expect(
|
40
|
+
expect(custom_calculator.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
|
42
41
|
end
|
43
42
|
|
44
43
|
it 'includes OPTIONAL' do
|
45
|
-
expect(
|
46
|
-
expect(
|
47
|
-
expect {
|
48
|
-
expect {
|
44
|
+
expect(custom_calculator.evaluate('OPTIONAL(1,2)')).to eq(3)
|
45
|
+
expect(custom_calculator.evaluate('OPTIONAL(1,2,3)')).to eq(6)
|
46
|
+
expect { custom_calculator.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
|
47
|
+
expect { custom_calculator.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
|
49
48
|
end
|
50
49
|
|
51
50
|
it 'supports array parameters' do
|
@@ -62,6 +61,66 @@ describe Dentaku::Calculator do
|
|
62
61
|
end
|
63
62
|
end
|
64
63
|
|
64
|
+
describe 'with callbacks' do
|
65
|
+
let(:custom_calculator) do
|
66
|
+
c = described_class.new
|
67
|
+
|
68
|
+
@counts = Hash.new(0)
|
69
|
+
|
70
|
+
@initial_time = "2023-02-03"
|
71
|
+
@last_time = @initial_time
|
72
|
+
|
73
|
+
c.add_function(
|
74
|
+
:reverse,
|
75
|
+
:stringl,
|
76
|
+
->(a) { a.reverse },
|
77
|
+
lambda do |args|
|
78
|
+
args.each do |arg|
|
79
|
+
@counts[arg.value] += 1 if arg.type == :string
|
80
|
+
end
|
81
|
+
end
|
82
|
+
)
|
83
|
+
|
84
|
+
fns = [
|
85
|
+
[:biggest_callback, :numeric, ->(*args) { args.max }, ->(args) { args.each { |arg| raise Dentaku::ArgumentError unless arg.type == :numeric } }],
|
86
|
+
[:pythagoras, :numeric, ->(l1, l2) { Math.sqrt(l1**2 + l2**2) }, ->(e) { @last_time = Time.now.to_s }],
|
87
|
+
[:callback_lambda, :string, ->() { " " }, ->() { "lambda executed" }],
|
88
|
+
[:no_lambda_function, :numeric, ->(a) { a**a }],
|
89
|
+
]
|
90
|
+
|
91
|
+
c.add_functions(fns)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'includes BIGGEST_CALLBACK' do
|
95
|
+
expect(custom_calculator.evaluate('BIGGEST_CALLBACK(1, 2, 5, 4)')).to eq(5)
|
96
|
+
expect { custom_calculator.dependencies('BIGGEST_CALLBACK(1, 3, 6, "hi", 10)') }.to raise_error(Dentaku::ArgumentError)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'includes REVERSE' do
|
100
|
+
expect(custom_calculator.evaluate('REVERSE(\'Dentaku\')')).to eq('ukatneD')
|
101
|
+
expect { custom_calculator.evaluate('REVERSE(22)') }.to raise_error(NoMethodError)
|
102
|
+
expect(@counts["Dentaku"]).to eq(1)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'includes PYTHAGORAS' do
|
106
|
+
expect(custom_calculator.evaluate('PYTHAGORAS(8, 7)')).to eq(10.63014581273465)
|
107
|
+
expect(custom_calculator.evaluate('PYTHAGORAS(3, 4)')).to eq(5)
|
108
|
+
expect(@last_time).not_to eq(@initial_time)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'exposes the `callback` method of a function' do
|
112
|
+
expect(Dentaku::AST::Function::Callback_lambda.callback.call()).to eq("lambda executed")
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'does not add a `callback` method to built-in functions' do
|
116
|
+
expect { Dentaku::AST::If.callback.call }.to raise_error(NoMethodError)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'defaults `callback` method to nil if not specified' do
|
120
|
+
expect(Dentaku::AST::Function::No_lambda_function.callback).to eq(nil)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
65
124
|
it 'allows registering "bang" functions' do
|
66
125
|
calculator = described_class.new
|
67
126
|
calculator.add_function(:hey!, :string, -> { "hey!" })
|
@@ -82,24 +141,36 @@ describe Dentaku::Calculator do
|
|
82
141
|
end
|
83
142
|
|
84
143
|
it 'does not store functions across all calculators' do
|
85
|
-
calculator1 =
|
144
|
+
calculator1 = described_class.new
|
86
145
|
calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
|
87
146
|
|
88
|
-
calculator2 =
|
147
|
+
calculator2 = described_class.new
|
89
148
|
calculator2.add_function(:my_function, :numeric, ->(x) { 4 * x + 3 })
|
90
149
|
|
91
150
|
expect(calculator1.evaluate!("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
|
92
151
|
expect(calculator2.evaluate!("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
|
93
152
|
|
94
153
|
expect {
|
95
|
-
|
154
|
+
described_class.new.evaluate!("1 + my_function(2)")
|
96
155
|
}.to raise_error(Dentaku::ParseError)
|
97
156
|
end
|
98
157
|
|
99
158
|
describe 'Dentaku::Calculator.add_function' do
|
100
|
-
it 'adds to default/global function registry' do
|
101
|
-
|
102
|
-
expect(
|
159
|
+
it 'adds a function to default/global function registry' do
|
160
|
+
described_class.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
|
161
|
+
expect(described_class.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe 'Dentaku::Calculator.add_functions' do
|
166
|
+
it 'adds multiple functions to default/global function registry' do
|
167
|
+
described_class.add_functions([
|
168
|
+
[:cube, :numeric, ->(x) { x**3 }],
|
169
|
+
[:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c,i| i.even? ? c.upcase : c.downcase }.join() }],
|
170
|
+
])
|
171
|
+
|
172
|
+
expect(described_class.new.evaluate("1 + cube(3)")).to eq(28)
|
173
|
+
expect(described_class.new.evaluate("spongebob('How are you today?')")).to eq("HoW ArE YoU ToDaY?")
|
103
174
|
end
|
104
175
|
end
|
105
176
|
end
|
data/spec/print_visitor_spec.rb
CHANGED
@@ -8,6 +8,12 @@ describe Dentaku::PrintVisitor do
|
|
8
8
|
expect(repr).to eq('5 + 4')
|
9
9
|
end
|
10
10
|
|
11
|
+
it 'handles grouping correctly' do
|
12
|
+
formula = '10 - (0 - 10)'
|
13
|
+
repr = roundtrip(formula)
|
14
|
+
expect(repr).to eq(formula)
|
15
|
+
end
|
16
|
+
|
11
17
|
it 'quotes string literals' do
|
12
18
|
repr = roundtrip('Concat(\'a\', "B")')
|
13
19
|
expect(repr).to eq('CONCAT("a", "B")')
|
data/spec/visitor/infix_spec.rb
CHANGED
@@ -10,6 +10,21 @@ class ArrayProcessor
|
|
10
10
|
@expression = []
|
11
11
|
end
|
12
12
|
|
13
|
+
def visit_array(node)
|
14
|
+
@expression << "{"
|
15
|
+
|
16
|
+
head, *tail = node.value
|
17
|
+
|
18
|
+
process(head) if head
|
19
|
+
|
20
|
+
tail.each do |v|
|
21
|
+
@expression << ","
|
22
|
+
process(v)
|
23
|
+
end
|
24
|
+
|
25
|
+
@expression << "}"
|
26
|
+
end
|
27
|
+
|
13
28
|
def process(node)
|
14
29
|
@expression << node.to_s
|
15
30
|
end
|
@@ -22,10 +37,16 @@ RSpec.describe Dentaku::Visitor::Infix do
|
|
22
37
|
expect(processor.expression).to eq ['5', '+', '3']
|
23
38
|
end
|
24
39
|
|
40
|
+
it 'supports array nodes' do
|
41
|
+
processor = ArrayProcessor.new
|
42
|
+
processor.visit(ast('{1, 2, 3}'))
|
43
|
+
expect(processor.expression).to eq ['{', '1', ',', '2', ',', '3', '}']
|
44
|
+
end
|
45
|
+
|
25
46
|
private
|
26
47
|
|
27
48
|
def ast(expression)
|
28
49
|
tokens = Dentaku::Tokenizer.new.tokenize(expression)
|
29
50
|
Dentaku::Parser.new(tokens).parse
|
30
51
|
end
|
31
|
-
end
|
52
|
+
end
|
data/spec/visitor_spec.rb
CHANGED
@@ -114,7 +114,7 @@ describe TestVisitor do
|
|
114
114
|
visit_nodes('case (a % 5) when 0 then a else b end')
|
115
115
|
visit_nodes('0xCAFE & (0xDECAF << 3) | (0xBEEF >> 5)')
|
116
116
|
visit_nodes('2017-12-24 23:59:59')
|
117
|
-
visit_nodes('ALL({1, 2, 3},
|
117
|
+
visit_nodes('ALL({1, 2, 3}, val, val % 2 == 0)')
|
118
118
|
visit_nodes('ANY(vals, val, val > 1)')
|
119
119
|
visit_nodes('COUNT({1, 2, 3})')
|
120
120
|
visit_nodes('PLUCK(users, age)')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.5.
|
4
|
+
version: 3.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-12-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -170,6 +170,7 @@ files:
|
|
170
170
|
- lib/dentaku/ast/datetime.rb
|
171
171
|
- lib/dentaku/ast/function.rb
|
172
172
|
- lib/dentaku/ast/function_registry.rb
|
173
|
+
- lib/dentaku/ast/functions/abs.rb
|
173
174
|
- lib/dentaku/ast/functions/all.rb
|
174
175
|
- lib/dentaku/ast/functions/and.rb
|
175
176
|
- lib/dentaku/ast/functions/any.rb
|
@@ -220,6 +221,7 @@ files:
|
|
220
221
|
- lib/dentaku/tokenizer.rb
|
221
222
|
- lib/dentaku/version.rb
|
222
223
|
- lib/dentaku/visitor/infix.rb
|
224
|
+
- spec/ast/abs_spec.rb
|
223
225
|
- spec/ast/addition_spec.rb
|
224
226
|
- spec/ast/all_spec.rb
|
225
227
|
- spec/ast/and_function_spec.rb
|
@@ -288,6 +290,7 @@ signing_key:
|
|
288
290
|
specification_version: 4
|
289
291
|
summary: A formula language parser and evaluator
|
290
292
|
test_files:
|
293
|
+
- spec/ast/abs_spec.rb
|
291
294
|
- spec/ast/addition_spec.rb
|
292
295
|
- spec/ast/all_spec.rb
|
293
296
|
- spec/ast/and_function_spec.rb
|