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