dentaku 3.5.1 → 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 +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
|