dentaku 3.3.2 → 3.3.3
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/.rubocop.yml +1 -1
- data/CHANGELOG.md +10 -1
- data/lib/dentaku/ast.rb +1 -0
- data/lib/dentaku/ast/access.rb +12 -0
- data/lib/dentaku/ast/arithmetic.rb +22 -4
- data/lib/dentaku/ast/array.rb +12 -0
- data/lib/dentaku/ast/case.rb +8 -0
- data/lib/dentaku/ast/case/case_conditional.rb +8 -0
- data/lib/dentaku/ast/function_registry.rb +21 -0
- data/lib/dentaku/ast/functions/count.rb +8 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/if.rb +15 -2
- data/lib/dentaku/ast/functions/string_functions.rb +16 -0
- data/lib/dentaku/ast/identifier.rb +2 -0
- data/lib/dentaku/ast/node.rb +4 -0
- data/lib/dentaku/ast/operation.rb +8 -0
- data/lib/dentaku/date_arithmetic.rb +45 -0
- data/lib/dentaku/exceptions.rb +1 -1
- data/lib/dentaku/parser.rb +12 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +18 -10
- data/spec/ast/function_spec.rb +1 -1
- data/spec/ast/node_spec.rb +4 -1
- data/spec/calculator_spec.rb +22 -3
- data/spec/dentaku_spec.rb +7 -0
- data/spec/external_function_spec.rb +29 -5
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3028783062dbbe592ef71ed7bc388e57fb3af383e8cb897893dc858c4b772f63
|
|
4
|
+
data.tar.gz: c940b16dcfdc9e0d1658a6b38b7521e7a17882c9519a1771d4117600a614100a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e9633152d5e43e11b52db3a91e8c012816c2cf956e5e3906ce2c058c44d5733fc2668d57049dbc3c44d07155c12bd3bcd8f416e4012986ddcd937bd46cae49c
|
|
7
|
+
data.tar.gz: bf89bf9096af4720832a7c904ce981a98beb69388996a1d29b190ce279fb8248eddc56175c5cc77646a36c65a0acc78dcd6c98fffb04ad0773cae22630eb3fdd
|
data/.rubocop.yml
CHANGED
|
@@ -105,7 +105,7 @@ Layout/TrailingWhitespace:
|
|
|
105
105
|
Enabled: true
|
|
106
106
|
|
|
107
107
|
# Use quotes for string literals when they are enough.
|
|
108
|
-
Style/
|
|
108
|
+
Style/RedundantPercentQ:
|
|
109
109
|
Enabled: true
|
|
110
110
|
|
|
111
111
|
# Align `end` with the matching keyword or starting expression except for
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
- date / duration addition and subtraction
|
|
5
|
+
- validate arity for custom functions with variable arity
|
|
6
|
+
- make AST serializable with Marshal.dump
|
|
7
|
+
- performance optimization for arithmetic node validation
|
|
8
|
+
- support lazy evaluation for expensive values
|
|
9
|
+
- short-circuit IF function
|
|
10
|
+
- better error when empty string is used in arithmetic operation
|
|
11
|
+
|
|
3
12
|
## [v3.3.2] 2019-06-10
|
|
4
13
|
- add ability to pre-load AST cache
|
|
5
14
|
- fix negation node bug
|
|
@@ -175,7 +184,7 @@
|
|
|
175
184
|
## [v0.1.0] 2012-01-20
|
|
176
185
|
- initial release
|
|
177
186
|
|
|
178
|
-
[
|
|
187
|
+
[Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.3.2...HEAD
|
|
179
188
|
[v3.3.2]: https://github.com/rubysolo/dentaku/compare/v3.3.1...v3.3.2
|
|
180
189
|
[v3.3.1]: https://github.com/rubysolo/dentaku/compare/v3.3.0...v3.3.1
|
|
181
190
|
[v3.3.0]: https://github.com/rubysolo/dentaku/compare/v3.2.1...v3.3.0
|
data/lib/dentaku/ast.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative './ast/function_registry'
|
|
|
18
18
|
require_relative './ast/functions/and'
|
|
19
19
|
require_relative './ast/functions/avg'
|
|
20
20
|
require_relative './ast/functions/count'
|
|
21
|
+
require_relative './ast/functions/duration'
|
|
21
22
|
require_relative './ast/functions/if'
|
|
22
23
|
require_relative './ast/functions/max'
|
|
23
24
|
require_relative './ast/functions/min'
|
data/lib/dentaku/ast/access.rb
CHANGED
|
@@ -5,6 +5,14 @@ module Dentaku
|
|
|
5
5
|
2
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
def self.min_param_count
|
|
9
|
+
arity
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.max_param_count
|
|
13
|
+
arity
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
def self.peek(*)
|
|
9
17
|
end
|
|
10
18
|
|
|
@@ -22,6 +30,10 @@ module Dentaku
|
|
|
22
30
|
def dependencies(context = {})
|
|
23
31
|
@structure.dependencies(context) + @index.dependencies(context)
|
|
24
32
|
end
|
|
33
|
+
|
|
34
|
+
def type
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
25
37
|
end
|
|
26
38
|
end
|
|
27
39
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative './operation'
|
|
2
|
+
require_relative '../date_arithmetic'
|
|
2
3
|
require 'bigdecimal'
|
|
3
4
|
require 'bigdecimal/util'
|
|
4
5
|
|
|
@@ -12,6 +13,7 @@ module Dentaku
|
|
|
12
13
|
raise NodeError.new(:numeric, left.type, :left),
|
|
13
14
|
"#{self.class} requires numeric operands"
|
|
14
15
|
end
|
|
16
|
+
|
|
15
17
|
unless valid_right?
|
|
16
18
|
raise NodeError.new(:numeric, right.type, :right),
|
|
17
19
|
"#{self.class} requires numeric operands"
|
|
@@ -50,15 +52,15 @@ module Dentaku
|
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def valid_node?(node)
|
|
53
|
-
node && (node.
|
|
55
|
+
node && (node.type == :numeric || node.dependencies.any?)
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def valid_left?
|
|
57
|
-
valid_node?(left)
|
|
59
|
+
valid_node?(left) || left.type == :datetime
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
def valid_right?
|
|
61
|
-
valid_node?(right)
|
|
63
|
+
valid_node?(right) || right.type == :duration
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def validate_value(val)
|
|
@@ -77,7 +79,7 @@ module Dentaku
|
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
def validate_format(string)
|
|
80
|
-
unless string =~ /\A-?\d*(\.\d+)?\z/
|
|
82
|
+
unless string =~ /\A-?\d*(\.\d+)?\z/ && !string.empty?
|
|
81
83
|
raise Dentaku::ArgumentError.for(:invalid_value, value: string, for: BigDecimal),
|
|
82
84
|
"String input '#{string}' is not coercible to numeric"
|
|
83
85
|
end
|
|
@@ -92,6 +94,14 @@ module Dentaku
|
|
|
92
94
|
def self.precedence
|
|
93
95
|
10
|
|
94
96
|
end
|
|
97
|
+
|
|
98
|
+
def value(context = {})
|
|
99
|
+
if left.type == :datetime
|
|
100
|
+
Dentaku::DateArithmetic.new(left.value(context)).add(right.value(context))
|
|
101
|
+
else
|
|
102
|
+
super
|
|
103
|
+
end
|
|
104
|
+
end
|
|
95
105
|
end
|
|
96
106
|
|
|
97
107
|
class Subtraction < Arithmetic
|
|
@@ -102,6 +112,14 @@ module Dentaku
|
|
|
102
112
|
def self.precedence
|
|
103
113
|
10
|
|
104
114
|
end
|
|
115
|
+
|
|
116
|
+
def value(context = {})
|
|
117
|
+
if left.type == :datetime
|
|
118
|
+
Dentaku::DateArithmetic.new(left.value(context)).sub(right.value(context))
|
|
119
|
+
else
|
|
120
|
+
super
|
|
121
|
+
end
|
|
122
|
+
end
|
|
105
123
|
end
|
|
106
124
|
|
|
107
125
|
class Multiplication < Arithmetic
|
data/lib/dentaku/ast/array.rb
CHANGED
|
@@ -4,6 +4,14 @@ module Dentaku
|
|
|
4
4
|
def self.arity
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
def self.min_param_count
|
|
8
|
+
0
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.max_param_count
|
|
12
|
+
Float::INFINITY
|
|
13
|
+
end
|
|
14
|
+
|
|
7
15
|
def self.peek(*)
|
|
8
16
|
end
|
|
9
17
|
|
|
@@ -18,6 +26,10 @@ module Dentaku
|
|
|
18
26
|
def dependencies(context = {})
|
|
19
27
|
@elements.flat_map { |el| el.dependencies(context) }
|
|
20
28
|
end
|
|
29
|
+
|
|
30
|
+
def type
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
21
33
|
end
|
|
22
34
|
end
|
|
23
35
|
end
|
data/lib/dentaku/ast/case.rb
CHANGED
|
@@ -38,6 +38,14 @@ module Dentaku
|
|
|
38
38
|
@implementation.arity < 0 ? nil : @implementation.arity
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def self.min_param_count
|
|
42
|
+
@implementation.parameters.select { |type, _name| type == :req }.count
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.max_param_count
|
|
46
|
+
@implementation.parameters.select { |type, _name| type == :rest }.any? ? Float::INFINITY : @implementation.parameters.count
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
def value(context = {})
|
|
42
50
|
args = @args.map { |a| a.value(context) }
|
|
43
51
|
self.class.implementation.call(*args)
|
|
@@ -48,6 +56,8 @@ module Dentaku
|
|
|
48
56
|
end
|
|
49
57
|
end
|
|
50
58
|
|
|
59
|
+
define_class(name, function)
|
|
60
|
+
|
|
51
61
|
function.name = name
|
|
52
62
|
function.type = type
|
|
53
63
|
function.implementation = implementation
|
|
@@ -72,6 +82,17 @@ module Dentaku
|
|
|
72
82
|
def function_name(name)
|
|
73
83
|
name.to_s.downcase
|
|
74
84
|
end
|
|
85
|
+
|
|
86
|
+
def normalize_name(function_name)
|
|
87
|
+
function_name.to_s.capitalize.gsub(/\W/, '_')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def define_class(function_name, function)
|
|
91
|
+
class_name = normalize_name(function_name)
|
|
92
|
+
return if Dentaku::AST::Function.const_defined?(class_name)
|
|
93
|
+
|
|
94
|
+
Dentaku::AST::Function.const_set(class_name, function)
|
|
95
|
+
end
|
|
75
96
|
end
|
|
76
97
|
end
|
|
77
98
|
end
|
|
@@ -3,6 +3,14 @@ require_relative '../function'
|
|
|
3
3
|
module Dentaku
|
|
4
4
|
module AST
|
|
5
5
|
class Count < Function
|
|
6
|
+
def self.min_param_count
|
|
7
|
+
0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.max_param_count
|
|
11
|
+
Float::INFINITY
|
|
12
|
+
end
|
|
13
|
+
|
|
6
14
|
def value(context = {})
|
|
7
15
|
if @args.length == 1
|
|
8
16
|
first_arg = @args[0].value(context)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class Duration < Function
|
|
6
|
+
def self.min_param_count
|
|
7
|
+
1
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.max_param_count
|
|
11
|
+
1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Value
|
|
15
|
+
attr_reader :value, :unit
|
|
16
|
+
|
|
17
|
+
def initialize(value, unit)
|
|
18
|
+
@value = value
|
|
19
|
+
@unit = validate_unit(unit)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate_unit(unit)
|
|
23
|
+
case unit.downcase
|
|
24
|
+
when /years?/ then :year
|
|
25
|
+
when /months?/ then :month
|
|
26
|
+
when /days?/ then :day
|
|
27
|
+
else
|
|
28
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: unit, for: Duration),
|
|
29
|
+
"'#{unit || unit.class}' is not a valid duration unit"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def type
|
|
35
|
+
:duration
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def value(context = {})
|
|
39
|
+
value_node, unit_node = *@args
|
|
40
|
+
Value.new(value_node.value(context), unit_node.identifier)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dependencies(context = {})
|
|
44
|
+
value_node = @args.first
|
|
45
|
+
value_node.dependencies(context)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Dentaku::AST::Function.register_class(:duration, Dentaku::AST::Duration)
|
|
@@ -5,6 +5,14 @@ module Dentaku
|
|
|
5
5
|
class If < Function
|
|
6
6
|
attr_reader :predicate, :left, :right
|
|
7
7
|
|
|
8
|
+
def self.min_param_count
|
|
9
|
+
3
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.max_param_count
|
|
13
|
+
3
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
def initialize(predicate, left, right)
|
|
9
17
|
@predicate = predicate
|
|
10
18
|
@left = left
|
|
@@ -24,8 +32,13 @@ module Dentaku
|
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
def dependencies(context = {})
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
deps = predicate.dependencies(context)
|
|
36
|
+
|
|
37
|
+
if deps.empty?
|
|
38
|
+
predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
|
|
39
|
+
else
|
|
40
|
+
(deps + left.dependencies(context) + right.dependencies(context)).uniq
|
|
41
|
+
end
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
end
|
|
@@ -17,6 +17,14 @@ module Dentaku
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
class Left < Base
|
|
20
|
+
def self.min_param_count
|
|
21
|
+
2
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.max_param_count
|
|
25
|
+
2
|
|
26
|
+
end
|
|
27
|
+
|
|
20
28
|
def initialize(*args)
|
|
21
29
|
super
|
|
22
30
|
@string, @length = *@args
|
|
@@ -111,6 +119,14 @@ module Dentaku
|
|
|
111
119
|
end
|
|
112
120
|
|
|
113
121
|
class Concat < Base
|
|
122
|
+
def self.min_param_count
|
|
123
|
+
1
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.max_param_count
|
|
127
|
+
Float::INFINITY
|
|
128
|
+
end
|
|
129
|
+
|
|
114
130
|
def initialize(*args)
|
|
115
131
|
super
|
|
116
132
|
end
|
data/lib/dentaku/ast/node.rb
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Dentaku
|
|
2
|
+
class DateArithmetic
|
|
3
|
+
def initialize(date)
|
|
4
|
+
@base = date
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def add(duration)
|
|
8
|
+
case duration
|
|
9
|
+
when Numeric
|
|
10
|
+
@base + duration
|
|
11
|
+
when Dentaku::AST::Duration::Value
|
|
12
|
+
case duration.unit
|
|
13
|
+
when :year
|
|
14
|
+
Time.local(@base.year + duration.value, @base.month, @base.day).to_datetime
|
|
15
|
+
when :month
|
|
16
|
+
@base >> duration.value
|
|
17
|
+
when :day
|
|
18
|
+
@base + duration.value
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
|
22
|
+
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sub(duration)
|
|
27
|
+
case duration
|
|
28
|
+
when Numeric
|
|
29
|
+
@base - duration
|
|
30
|
+
when Dentaku::AST::Duration::Value
|
|
31
|
+
case duration.unit
|
|
32
|
+
when :year
|
|
33
|
+
Time.local(@base.year - duration.value, @base.month, @base.day).to_datetime
|
|
34
|
+
when :month
|
|
35
|
+
@base << duration.value
|
|
36
|
+
when :day
|
|
37
|
+
@base - duration.value
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
|
41
|
+
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/dentaku/exceptions.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Dentaku
|
|
|
33
33
|
private_class_method :new
|
|
34
34
|
|
|
35
35
|
VALID_REASONS = %i[
|
|
36
|
-
node_invalid too_few_operands undefined_function
|
|
36
|
+
node_invalid too_few_operands too_many_operands undefined_function
|
|
37
37
|
unprocessed_token unknown_case_token unbalanced_bracket
|
|
38
38
|
unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
|
|
39
39
|
invalid_statement
|
data/lib/dentaku/parser.rb
CHANGED
|
@@ -40,9 +40,17 @@ module Dentaku
|
|
|
40
40
|
operator.peek(output)
|
|
41
41
|
|
|
42
42
|
args_size = operator.arity || count
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
min_size = operator.arity || operator.min_param_count || count
|
|
44
|
+
max_size = operator.arity || operator.max_param_count || count
|
|
45
|
+
|
|
46
|
+
if output.length < min_size
|
|
47
|
+
fail! :too_few_operands, operator: operator, expect: min_size, actual: output.length
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if output.length > max_size && operations.empty?
|
|
51
|
+
fail! :too_many_operands, operator: operator, expect: max_size, actual: output.length
|
|
45
52
|
end
|
|
53
|
+
|
|
46
54
|
args = Array.new(args_size) { output.pop }.reverse
|
|
47
55
|
|
|
48
56
|
output.push operator.new(*args)
|
|
@@ -305,6 +313,8 @@ module Dentaku
|
|
|
305
313
|
"#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
|
|
306
314
|
when :too_few_operands
|
|
307
315
|
"#{meta.fetch(:operator)} has too few operands"
|
|
316
|
+
when :too_many_operands
|
|
317
|
+
"#{meta.fetch(:operator)} has too many operands"
|
|
308
318
|
when :undefined_function
|
|
309
319
|
"Undefined function #{meta.fetch(:function_name)}"
|
|
310
320
|
when :unprocessed_token
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/arithmetic_spec.rb
CHANGED
|
@@ -38,25 +38,33 @@ describe Dentaku::AST::Arithmetic do
|
|
|
38
38
|
expect(neg(x)).to eq(-1)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
it 'correctly parses string operands to numeric values' do
|
|
42
|
+
expect(add(x, one, 'x' => '1')).to eq(2)
|
|
43
|
+
expect(add(x, one, 'x' => '1.1')).to eq(2.1)
|
|
44
|
+
expect(add(x, one, 'x' => '.1')).to eq(1.1)
|
|
45
|
+
expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
|
|
46
|
+
expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
private
|
|
42
50
|
|
|
43
|
-
def add(left, right)
|
|
44
|
-
Dentaku::AST::Addition.new(left, right).value(
|
|
51
|
+
def add(left, right, context = ctx)
|
|
52
|
+
Dentaku::AST::Addition.new(left, right).value(context)
|
|
45
53
|
end
|
|
46
54
|
|
|
47
|
-
def sub(left, right)
|
|
48
|
-
Dentaku::AST::Subtraction.new(left, right).value(
|
|
55
|
+
def sub(left, right, context = ctx)
|
|
56
|
+
Dentaku::AST::Subtraction.new(left, right).value(context)
|
|
49
57
|
end
|
|
50
58
|
|
|
51
|
-
def mul(left, right)
|
|
52
|
-
Dentaku::AST::Multiplication.new(left, right).value(
|
|
59
|
+
def mul(left, right, context = ctx)
|
|
60
|
+
Dentaku::AST::Multiplication.new(left, right).value(context)
|
|
53
61
|
end
|
|
54
62
|
|
|
55
|
-
def div(left, right)
|
|
56
|
-
Dentaku::AST::Division.new(left, right).value(
|
|
63
|
+
def div(left, right, context = ctx)
|
|
64
|
+
Dentaku::AST::Division.new(left, right).value(context)
|
|
57
65
|
end
|
|
58
66
|
|
|
59
|
-
def neg(node)
|
|
60
|
-
Dentaku::AST::Negation.new(node).value(
|
|
67
|
+
def neg(node, context = ctx)
|
|
68
|
+
Dentaku::AST::Negation.new(node).value(context)
|
|
61
69
|
end
|
|
62
70
|
end
|
data/spec/ast/function_spec.rb
CHANGED
|
@@ -22,7 +22,7 @@ describe Dentaku::AST::Function do
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
describe "#arity" do
|
|
25
|
-
it "
|
|
25
|
+
it "returns the correct arity for custom functions" do
|
|
26
26
|
zero = described_class.register("zero", :numeric, ->() { 0 })
|
|
27
27
|
expect(zero.arity).to eq(0)
|
|
28
28
|
|
data/spec/ast/node_spec.rb
CHANGED
|
@@ -21,7 +21,10 @@ describe Dentaku::AST::Node do
|
|
|
21
21
|
expect(node.dependencies).to eq(['x', 'y', 'z'])
|
|
22
22
|
|
|
23
23
|
node = make_node('if(x > 5, y, z)')
|
|
24
|
-
expect(node.dependencies('x' => 7)).to eq(['y'
|
|
24
|
+
expect(node.dependencies('x' => 7)).to eq(['y'])
|
|
25
|
+
|
|
26
|
+
node = make_node('if(x > 5, y, z)')
|
|
27
|
+
expect(node.dependencies('x' => 2)).to eq(['z'])
|
|
25
28
|
|
|
26
29
|
node = make_node('')
|
|
27
30
|
expect(node.dependencies).to eq([])
|
data/spec/calculator_spec.rb
CHANGED
|
@@ -74,6 +74,11 @@ describe Dentaku::Calculator do
|
|
|
74
74
|
expect(calculator.evaluate('NOT(a)', a: nil, b: nil)).to be_truthy
|
|
75
75
|
expect(calculator.evaluate('OR(a,b)', a: nil, b: nil)).to be_falsy
|
|
76
76
|
end
|
|
77
|
+
|
|
78
|
+
it 'supports lazy evaluation of variables' do
|
|
79
|
+
expect(calculator.evaluate('x + 1', x: -> { 1 })).to eq(2)
|
|
80
|
+
expect { calculator.evaluate('2', x: -> { raise 'boom' }) }.not_to raise_error
|
|
81
|
+
end
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
describe 'evaluate!' do
|
|
@@ -105,7 +110,7 @@ describe Dentaku::Calculator do
|
|
|
105
110
|
end
|
|
106
111
|
|
|
107
112
|
it 'raises argument error if a function is called with incorrect arity' do
|
|
108
|
-
expect { calculator.evaluate!('IF(a,b)', a: 1, b: 1) }.to raise_error(Dentaku::
|
|
113
|
+
expect { calculator.evaluate!('IF(a,b)', a: 1, b: 1) }.to raise_error(Dentaku::ParseError)
|
|
109
114
|
end
|
|
110
115
|
end
|
|
111
116
|
|
|
@@ -369,20 +374,34 @@ describe Dentaku::Calculator do
|
|
|
369
374
|
expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: false)).to be_falsey
|
|
370
375
|
end
|
|
371
376
|
|
|
372
|
-
it 'compares
|
|
377
|
+
it 'compares time variables' do
|
|
373
378
|
expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_truthy
|
|
374
379
|
expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_falsy
|
|
375
380
|
expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_falsy
|
|
376
381
|
expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_truthy
|
|
377
382
|
end
|
|
378
383
|
|
|
379
|
-
it 'compares
|
|
384
|
+
it 'compares time literals with time variables' do
|
|
380
385
|
expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_truthy
|
|
381
386
|
expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_falsy
|
|
382
387
|
expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_falsy
|
|
383
388
|
expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_truthy
|
|
384
389
|
end
|
|
385
390
|
|
|
391
|
+
it 'supports date arithmetic' do
|
|
392
|
+
expect(calculator.evaluate!('2020-01-01 + 30').to_date).to eq(Time.local(2020, 1, 31).to_date)
|
|
393
|
+
expect(calculator.evaluate!('2020-01-01 - 1').to_date).to eq(Time.local(2019, 12, 31).to_date)
|
|
394
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, day)').to_date).to eq(Time.local(2020, 1, 2).to_date)
|
|
395
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, day)').to_date).to eq(Time.local(2019, 12, 31).to_date)
|
|
396
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, days)').to_date).to eq(Time.local(2020, 1, 31).to_date)
|
|
397
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, month)').to_date).to eq(Time.local(2020, 2, 1).to_date)
|
|
398
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, month)').to_date).to eq(Time.local(2019, 12, 1).to_date)
|
|
399
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, months)').to_date).to eq(Time.local(2022, 7, 1).to_date)
|
|
400
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, year)').to_date).to eq(Time.local(2021, 1, 1).to_date)
|
|
401
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, year)').to_date).to eq(Time.local(2019, 1, 1).to_date)
|
|
402
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, years)').to_date).to eq(Time.local(2050, 1, 1).to_date)
|
|
403
|
+
end
|
|
404
|
+
|
|
386
405
|
describe 'functions' do
|
|
387
406
|
it 'include IF' do
|
|
388
407
|
expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 2)).to eq(10)
|
data/spec/dentaku_spec.rb
CHANGED
|
@@ -36,4 +36,11 @@ describe Dentaku do
|
|
|
36
36
|
Dentaku.aliases = { roundup: ['roundupup'] }
|
|
37
37
|
expect(Dentaku.evaluate('roundupup(6.1)')).to eq(7)
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
it 'sets caching opt-in flags' do
|
|
41
|
+
expect {
|
|
42
|
+
Dentaku.enable_caching!
|
|
43
|
+
}.to change { Dentaku.cache_ast? }.from(false).to(true)
|
|
44
|
+
.and change { Dentaku.cache_dependency_order? }.from(false).to(true)
|
|
45
|
+
end
|
|
39
46
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
|
+
require 'dentaku'
|
|
2
3
|
require 'dentaku/calculator'
|
|
3
4
|
|
|
4
5
|
describe Dentaku::Calculator do
|
|
@@ -14,6 +15,7 @@ describe Dentaku::Calculator do
|
|
|
14
15
|
[:pow, :numeric, ->(mantissa, exponent) { mantissa**exponent }],
|
|
15
16
|
[:biggest, :numeric, ->(*args) { args.max }],
|
|
16
17
|
[:smallest, :numeric, ->(*args) { args.min }],
|
|
18
|
+
[:optional, :numeric, ->(x, y, z = 0) { x + y + z }],
|
|
17
19
|
]
|
|
18
20
|
|
|
19
21
|
c.add_functions(fns)
|
|
@@ -39,6 +41,13 @@ describe Dentaku::Calculator do
|
|
|
39
41
|
expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
it 'includes OPTIONAL' do
|
|
45
|
+
expect(with_external_funcs.evaluate('OPTIONAL(1,2)')).to eq(3)
|
|
46
|
+
expect(with_external_funcs.evaluate('OPTIONAL(1,2,3)')).to eq(6)
|
|
47
|
+
expect { with_external_funcs.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
|
|
48
|
+
expect { with_external_funcs.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
|
|
49
|
+
end
|
|
50
|
+
|
|
42
51
|
it 'supports array parameters' do
|
|
43
52
|
calculator = described_class.new
|
|
44
53
|
calculator.add_function(
|
|
@@ -59,6 +68,19 @@ describe Dentaku::Calculator do
|
|
|
59
68
|
expect(calculator.evaluate("hey!()")).to eq("hey!")
|
|
60
69
|
end
|
|
61
70
|
|
|
71
|
+
it 'defines for a given function a properly named class that represents it to support AST marshaling' do
|
|
72
|
+
calculator = described_class.new
|
|
73
|
+
expect {
|
|
74
|
+
calculator.add_function(:ho, :string, -> {})
|
|
75
|
+
}.to change {
|
|
76
|
+
Dentaku::AST::Function.const_defined?("Ho")
|
|
77
|
+
}.from(false).to(true)
|
|
78
|
+
|
|
79
|
+
expect {
|
|
80
|
+
Marshal.dump(calculator.ast('MAX(1, 2)'))
|
|
81
|
+
}.not_to raise_error
|
|
82
|
+
end
|
|
83
|
+
|
|
62
84
|
it 'does not store functions across all calculators' do
|
|
63
85
|
calculator1 = Dentaku::Calculator.new
|
|
64
86
|
calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
|
|
@@ -66,17 +88,19 @@ describe Dentaku::Calculator do
|
|
|
66
88
|
calculator2 = Dentaku::Calculator.new
|
|
67
89
|
calculator2.add_function(:my_function, :numeric, ->(x) { 4 * x + 3 })
|
|
68
90
|
|
|
69
|
-
expect(calculator1.evaluate("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
|
|
70
|
-
expect(calculator2.evaluate("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
|
|
91
|
+
expect(calculator1.evaluate!("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
|
|
92
|
+
expect(calculator2.evaluate!("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
|
|
71
93
|
|
|
72
94
|
expect {
|
|
73
95
|
Dentaku::Calculator.new.evaluate!("1 + my_function(2)")
|
|
74
96
|
}.to raise_error(Dentaku::ParseError)
|
|
75
97
|
end
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
describe 'Dentaku::Calculator.add_function' do
|
|
100
|
+
it 'adds to default/global function registry' do
|
|
101
|
+
Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
|
|
102
|
+
expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
|
|
103
|
+
end
|
|
80
104
|
end
|
|
81
105
|
end
|
|
82
106
|
end
|
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.3.
|
|
4
|
+
version: 3.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Solomon White
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2019-
|
|
11
|
+
date: 2019-11-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: codecov
|
|
@@ -159,6 +159,7 @@ files:
|
|
|
159
159
|
- lib/dentaku/ast/functions/and.rb
|
|
160
160
|
- lib/dentaku/ast/functions/avg.rb
|
|
161
161
|
- lib/dentaku/ast/functions/count.rb
|
|
162
|
+
- lib/dentaku/ast/functions/duration.rb
|
|
162
163
|
- lib/dentaku/ast/functions/if.rb
|
|
163
164
|
- lib/dentaku/ast/functions/max.rb
|
|
164
165
|
- lib/dentaku/ast/functions/min.rb
|
|
@@ -184,6 +185,7 @@ files:
|
|
|
184
185
|
- lib/dentaku/ast/string.rb
|
|
185
186
|
- lib/dentaku/bulk_expression_solver.rb
|
|
186
187
|
- lib/dentaku/calculator.rb
|
|
188
|
+
- lib/dentaku/date_arithmetic.rb
|
|
187
189
|
- lib/dentaku/dependency_resolver.rb
|
|
188
190
|
- lib/dentaku/exceptions.rb
|
|
189
191
|
- lib/dentaku/flat_hash.rb
|
|
@@ -249,8 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
249
251
|
- !ruby/object:Gem::Version
|
|
250
252
|
version: '0'
|
|
251
253
|
requirements: []
|
|
252
|
-
|
|
253
|
-
rubygems_version: 2.7.6
|
|
254
|
+
rubygems_version: 3.0.3
|
|
254
255
|
signing_key:
|
|
255
256
|
specification_version: 4
|
|
256
257
|
summary: A formula language parser and evaluator
|