dentaku 3.5.3 → 3.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -2
- data/dentaku.gemspec +1 -0
- data/lib/dentaku/ast/access.rb +0 -3
- data/lib/dentaku/ast/arithmetic.rb +23 -35
- data/lib/dentaku/ast/array.rb +1 -4
- data/lib/dentaku/ast/functions/all.rb +1 -5
- data/lib/dentaku/ast/functions/any.rb +1 -5
- data/lib/dentaku/ast/functions/enum.rb +13 -0
- data/lib/dentaku/ast/functions/map.rb +1 -5
- data/lib/dentaku/ast/functions/pluck.rb +6 -2
- data/lib/dentaku/ast/node.rb +2 -1
- data/lib/dentaku/bulk_expression_solver.rb +37 -7
- data/lib/dentaku/calculator.rb +21 -5
- data/lib/dentaku/date_arithmetic.rb +24 -15
- data/lib/dentaku/dependency_resolver.rb +9 -4
- data/lib/dentaku/parser.rb +1 -2
- data/lib/dentaku/token.rb +12 -0
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/all_spec.rb +13 -0
- data/spec/ast/any_spec.rb +13 -0
- data/spec/ast/map_spec.rb +13 -0
- data/spec/ast/pluck_spec.rb +17 -0
- data/spec/bulk_expression_solver_spec.rb +16 -0
- data/spec/calculator_spec.rb +21 -3
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/visitor_spec.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef9e8cb8d852db9a8e4c81c28815c6a7e7008c6e9bebcc8a11222768756fd7a8
|
4
|
+
data.tar.gz: f7d1c005ef1ba8bcfc77fcac8b885378a0e3e36fdba8f97a2c602b6b0ba6f34c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d616fd597440c13e8432ac6f9d5480592cdcbb7e9aec30c1f2c21e94e0a4bcf3ffced31cf486dc7390bd8668bb5e377745a7eebf467edfab1cd5fa0ae4f70e9
|
7
|
+
data.tar.gz: fca27bf921a54469f065f634a44d4a6481c6420ccbcaa39400ffe4a5cdd172ff6ae209a04aee96208a64979bbff814635719732b557df33f7d406f87123b1c3a
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## [
|
3
|
+
## [v3.5.4]
|
4
|
+
- add support for default value for PLUCK function
|
5
|
+
- improve error handling for MAP/ANY/ALL functions
|
6
|
+
- fix modulo / percentage operator determination
|
7
|
+
- fix string casing bug with bulk expressions
|
8
|
+
- add explicit gem dependency for BigDecimal
|
9
|
+
|
10
|
+
## [v3.5.3]
|
4
11
|
- add support for empty array literals
|
5
12
|
- add support for quoted identifiers
|
6
13
|
- add REDUCE function
|
@@ -253,7 +260,8 @@
|
|
253
260
|
## [v0.1.0] 2012-01-20
|
254
261
|
- initial release
|
255
262
|
|
256
|
-
[
|
263
|
+
[v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
|
264
|
+
[v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
|
257
265
|
[v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
|
258
266
|
[v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
|
259
267
|
[v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
|
data/dentaku.gemspec
CHANGED
data/lib/dentaku/ast/access.rb
CHANGED
@@ -179,50 +179,42 @@ module Dentaku
|
|
179
179
|
|
180
180
|
class Modulo < Arithmetic
|
181
181
|
def self.arity
|
182
|
-
|
182
|
+
2
|
183
183
|
end
|
184
184
|
|
185
|
-
def self.
|
186
|
-
|
187
|
-
@arity = 2 if input.length > 1
|
185
|
+
def self.precedence
|
186
|
+
20
|
188
187
|
end
|
189
188
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
189
|
+
def self.resolve_class(next_token)
|
190
|
+
next_token.nil? || next_token.operator? || next_token.close? ? Percentage : self
|
191
|
+
end
|
192
|
+
|
193
|
+
def operator
|
194
|
+
:%
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class Percentage < Arithmetic
|
199
|
+
def self.arity
|
200
|
+
1
|
201
|
+
end
|
202
|
+
|
203
|
+
def initialize(child)
|
204
|
+
@right = child
|
197
205
|
|
198
|
-
unless valid_left?
|
199
|
-
raise NodeError.new(%i[numeric nil], left.type, :left),
|
200
|
-
"#{self.class} requires numeric operands or nil"
|
201
|
-
end
|
202
206
|
unless valid_right?
|
203
207
|
raise NodeError.new(:numeric, right.type, :right),
|
204
|
-
"#{self.class} requires numeric
|
208
|
+
"#{self.class} requires a numeric operand"
|
205
209
|
end
|
206
210
|
end
|
207
211
|
|
208
212
|
def dependencies(context = {})
|
209
|
-
|
210
|
-
@right.dependencies(context)
|
211
|
-
else
|
212
|
-
super
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
def percent?
|
217
|
-
left.nil?
|
213
|
+
@right.dependencies(context)
|
218
214
|
end
|
219
215
|
|
220
216
|
def value(context = {})
|
221
|
-
|
222
|
-
cast(right.value(context)) * 0.01
|
223
|
-
else
|
224
|
-
super
|
225
|
-
end
|
217
|
+
cast(right.value(context)) * 0.01
|
226
218
|
end
|
227
219
|
|
228
220
|
def operator
|
@@ -230,11 +222,7 @@ module Dentaku
|
|
230
222
|
end
|
231
223
|
|
232
224
|
def self.precedence
|
233
|
-
|
234
|
-
end
|
235
|
-
|
236
|
-
def valid_left?
|
237
|
-
valid_node?(left) || left.nil?
|
225
|
+
30
|
238
226
|
end
|
239
227
|
end
|
240
228
|
|
data/lib/dentaku/ast/array.rb
CHANGED
@@ -14,9 +14,6 @@ module Dentaku
|
|
14
14
|
Float::INFINITY
|
15
15
|
end
|
16
16
|
|
17
|
-
def self.peek(*)
|
18
|
-
end
|
19
|
-
|
20
17
|
def initialize(*elements)
|
21
18
|
@elements = *elements
|
22
19
|
end
|
@@ -32,7 +29,7 @@ module Dentaku
|
|
32
29
|
def type
|
33
30
|
nil
|
34
31
|
end
|
35
|
-
|
32
|
+
|
36
33
|
def accept(visitor)
|
37
34
|
visitor.visit_array(self)
|
38
35
|
end
|
@@ -9,11 +9,7 @@ module Dentaku
|
|
9
9
|
expression = @args[2]
|
10
10
|
|
11
11
|
collection.all? do |item_value|
|
12
|
-
expression
|
13
|
-
context.merge(
|
14
|
-
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
15
|
-
)
|
16
|
-
)
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -9,11 +9,7 @@ module Dentaku
|
|
9
9
|
expression = @args[2]
|
10
10
|
|
11
11
|
collection.any? do |item_value|
|
12
|
-
expression
|
13
|
-
context.merge(
|
14
|
-
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
15
|
-
)
|
16
|
-
)
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -33,6 +33,19 @@ module Dentaku
|
|
33
33
|
def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
|
34
34
|
raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
|
35
35
|
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def mapped_value(expression, context, item_context)
|
40
|
+
expression.value(
|
41
|
+
context.merge(
|
42
|
+
FlatHash.from_hash_with_intermediates(item_context)
|
43
|
+
)
|
44
|
+
)
|
45
|
+
rescue => e
|
46
|
+
raise e if context["__evaluation_mode"] == :strict
|
47
|
+
nil
|
48
|
+
end
|
36
49
|
end
|
37
50
|
end
|
38
51
|
end
|
@@ -9,11 +9,7 @@ module Dentaku
|
|
9
9
|
expression = @args[2]
|
10
10
|
|
11
11
|
collection.map do |item_value|
|
12
|
-
expression
|
13
|
-
context.merge(
|
14
|
-
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
15
|
-
)
|
16
|
-
)
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -9,19 +9,23 @@ module Dentaku
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.max_param_count
|
12
|
-
|
12
|
+
3
|
13
13
|
end
|
14
14
|
|
15
15
|
def value(context = {})
|
16
16
|
collection = Array(@args[0].value(context))
|
17
|
+
|
17
18
|
unless collection.all? { |elem| elem.is_a?(Hash) }
|
18
19
|
raise ArgumentError.for(:incompatible_type, value: collection),
|
19
20
|
'PLUCK() requires first argument to be an array of hashes'
|
20
21
|
end
|
21
22
|
|
22
23
|
pluck_path = @args[1].identifier
|
24
|
+
default = @args[2]
|
23
25
|
|
24
|
-
collection.map { |h|
|
26
|
+
collection.map { |h|
|
27
|
+
h.transform_keys(&:to_s).fetch(pluck_path, default&.value(context))
|
28
|
+
}
|
25
29
|
end
|
26
30
|
end
|
27
31
|
end
|
data/lib/dentaku/ast/node.rb
CHANGED
@@ -6,16 +6,41 @@ require 'dentaku/tokenizer'
|
|
6
6
|
|
7
7
|
module Dentaku
|
8
8
|
class BulkExpressionSolver
|
9
|
+
class StrictEvaluator
|
10
|
+
def initialize(calculator)
|
11
|
+
@calculator = calculator
|
12
|
+
end
|
13
|
+
|
14
|
+
def evaluate(*args)
|
15
|
+
@calculator.evaluate!(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class PermissiveEvaluator
|
20
|
+
def initialize(calculator, block)
|
21
|
+
@calculator = calculator
|
22
|
+
@block = block || ->(*) { :undefined }
|
23
|
+
end
|
24
|
+
|
25
|
+
def evaluate(*args)
|
26
|
+
@calculator.evaluate(*args) { |expr, ex|
|
27
|
+
@block.call(ex)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
9
32
|
def initialize(expressions, calculator)
|
10
33
|
@expression_hash = FlatHash.from_hash(expressions)
|
11
34
|
@calculator = calculator
|
12
35
|
end
|
13
36
|
|
14
37
|
def solve!
|
38
|
+
@evaluator = StrictEvaluator.new(calculator)
|
15
39
|
solve(&raise_exception_handler)
|
16
40
|
end
|
17
41
|
|
18
42
|
def solve(&block)
|
43
|
+
@evaluator ||= PermissiveEvaluator.new(calculator, block)
|
19
44
|
error_handler = block || return_undefined_handler
|
20
45
|
results = load_results(&error_handler)
|
21
46
|
|
@@ -42,7 +67,7 @@ module Dentaku
|
|
42
67
|
@dep_cache ||= {}
|
43
68
|
end
|
44
69
|
|
45
|
-
attr_reader :expression_hash, :calculator
|
70
|
+
attr_reader :expression_hash, :calculator, :evaluator
|
46
71
|
|
47
72
|
def return_undefined_handler
|
48
73
|
->(*) { :undefined }
|
@@ -52,8 +77,11 @@ module Dentaku
|
|
52
77
|
->(ex) { raise ex }
|
53
78
|
end
|
54
79
|
|
55
|
-
def expression_with_exception_handler(&block)
|
56
|
-
->(_expr, ex) {
|
80
|
+
def expression_with_exception_handler(var_name, &block)
|
81
|
+
->(_expr, ex) {
|
82
|
+
ex.recipient_variable = var_name
|
83
|
+
block.call(ex)
|
84
|
+
}
|
57
85
|
end
|
58
86
|
|
59
87
|
def load_results(&block)
|
@@ -73,11 +101,14 @@ module Dentaku
|
|
73
101
|
next if expressions[var_name].nil?
|
74
102
|
|
75
103
|
with_rescues(var_name, results, block) do
|
76
|
-
results[var_name] = evaluated_facts[var_name] ||
|
104
|
+
results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
|
77
105
|
expressions[var_name],
|
78
106
|
context.merge(results),
|
79
|
-
&expression_with_exception_handler(&block)
|
80
|
-
)
|
107
|
+
&expression_with_exception_handler(var_name, &block)
|
108
|
+
).tap { |res|
|
109
|
+
res.recipient_variable = var_name if res.respond_to?(:recipient_variable=)
|
110
|
+
res
|
111
|
+
}
|
81
112
|
end
|
82
113
|
end
|
83
114
|
|
@@ -88,7 +119,6 @@ module Dentaku
|
|
88
119
|
|
89
120
|
def with_rescues(var_name, results, block)
|
90
121
|
yield
|
91
|
-
|
92
122
|
rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
|
93
123
|
ex.recipient_variable = var_name
|
94
124
|
results[var_name] = block.call(ex)
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -52,27 +52,39 @@ module Dentaku
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def evaluate(expression, data = {}, &block)
|
55
|
-
|
55
|
+
context = evaluation_context(data, :permissive)
|
56
|
+
return evaluate_array(expression, context, &block) if expression.is_a?(Array)
|
57
|
+
|
58
|
+
evaluate!(expression, context)
|
56
59
|
rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
|
57
60
|
block.call(expression, ex) if block_given?
|
58
61
|
end
|
59
62
|
|
63
|
+
private def evaluate_array(expression, data = {}, &block)
|
64
|
+
expression.map { |e| evaluate(e, data, &block) }
|
65
|
+
end
|
66
|
+
|
60
67
|
def evaluate!(expression, data = {}, &block)
|
61
|
-
|
62
|
-
|
63
|
-
} if expression.is_a? Array
|
68
|
+
context = evaluation_context(data, :strict)
|
69
|
+
return evaluate_array!(expression, context, &block) if expression.is_a? Array
|
64
70
|
|
65
|
-
store(
|
71
|
+
store(context) do
|
66
72
|
node = ast(expression)
|
67
73
|
unbound = node.dependencies(memory)
|
74
|
+
|
68
75
|
unless unbound.empty?
|
69
76
|
raise UnboundVariableError.new(unbound),
|
70
77
|
"no value provided for variables: #{unbound.uniq.join(', ')}"
|
71
78
|
end
|
79
|
+
|
72
80
|
node.value(memory)
|
73
81
|
end
|
74
82
|
end
|
75
83
|
|
84
|
+
private def evaluate_array!(expression, data = {}, &block)
|
85
|
+
expression.map { |e| evaluate!(e, data, &block) }
|
86
|
+
end
|
87
|
+
|
76
88
|
def solve!(expression_hash)
|
77
89
|
BulkExpressionSolver.new(expression_hash, self).solve!
|
78
90
|
end
|
@@ -130,6 +142,10 @@ module Dentaku
|
|
130
142
|
end
|
131
143
|
end
|
132
144
|
|
145
|
+
def evaluation_context(data, evaluation_mode)
|
146
|
+
data.key?(:__evaluation_mode) ? data : data.merge(__evaluation_mode: evaluation_mode)
|
147
|
+
end
|
148
|
+
|
133
149
|
def store(key_or_hash, value = nil)
|
134
150
|
restore = Hash[memory]
|
135
151
|
|
@@ -13,13 +13,11 @@ module Dentaku
|
|
13
13
|
when Numeric
|
14
14
|
@base + duration
|
15
15
|
when Dentaku::AST::Duration::Value
|
16
|
-
case
|
17
|
-
when
|
18
|
-
|
19
|
-
|
20
|
-
@base
|
21
|
-
when :day
|
22
|
-
@base + duration.value
|
16
|
+
case @base
|
17
|
+
when Time
|
18
|
+
change_datetime(@base.to_datetime, duration.unit, duration.value).to_time
|
19
|
+
else
|
20
|
+
change_datetime(@base, duration.unit, duration.value)
|
23
21
|
end
|
24
22
|
else
|
25
23
|
raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
|
@@ -29,16 +27,14 @@ module Dentaku
|
|
29
27
|
|
30
28
|
def sub(duration)
|
31
29
|
case duration
|
32
|
-
when Date, DateTime, Numeric
|
30
|
+
when Date, DateTime, Numeric, Time
|
33
31
|
@base - duration
|
34
32
|
when Dentaku::AST::Duration::Value
|
35
|
-
case
|
36
|
-
when
|
37
|
-
|
38
|
-
|
39
|
-
@base
|
40
|
-
when :day
|
41
|
-
@base - duration.value
|
33
|
+
case @base
|
34
|
+
when Time
|
35
|
+
change_datetime(@base.to_datetime, duration.unit, -duration.value).to_time
|
36
|
+
else
|
37
|
+
change_datetime(@base, duration.unit, -duration.value)
|
42
38
|
end
|
43
39
|
when Dentaku::TokenScanner::DATE_TIME_REGEXP
|
44
40
|
@base - Time.parse(duration).to_datetime
|
@@ -47,5 +43,18 @@ module Dentaku
|
|
47
43
|
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
48
44
|
end
|
49
45
|
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def change_datetime(base, unit, value)
|
50
|
+
case unit
|
51
|
+
when :year
|
52
|
+
base >> (value * 12)
|
53
|
+
when :month
|
54
|
+
base >> value
|
55
|
+
when :day
|
56
|
+
base + value
|
57
|
+
end
|
58
|
+
end
|
50
59
|
end
|
51
60
|
end
|
@@ -4,13 +4,18 @@ module Dentaku
|
|
4
4
|
class DependencyResolver
|
5
5
|
include TSort
|
6
6
|
|
7
|
-
def self.find_resolve_order(vars_to_dependencies_hash)
|
8
|
-
self.new(vars_to_dependencies_hash).
|
7
|
+
def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
|
8
|
+
self.new(vars_to_dependencies_hash).sort
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize(vars_to_dependencies_hash)
|
12
|
-
|
13
|
-
|
12
|
+
@key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [k.downcase, k] }]
|
13
|
+
# ensure variables are normalized strings
|
14
|
+
@vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.downcase.to_s, v] }]
|
15
|
+
end
|
16
|
+
|
17
|
+
def sort
|
18
|
+
tsort.map { |k| @key_mapping.fetch(k, k) }
|
14
19
|
end
|
15
20
|
|
16
21
|
def tsort_each_node(&block)
|
data/lib/dentaku/parser.rb
CHANGED
@@ -43,8 +43,6 @@ module Dentaku
|
|
43
43
|
operator = operations.pop
|
44
44
|
fail! :invalid_statement if operator.nil?
|
45
45
|
|
46
|
-
operator.peek(output)
|
47
|
-
|
48
46
|
output_size = output.length
|
49
47
|
args_size = operator.arity || count
|
50
48
|
min_size = operator.arity || operator.min_param_count || count
|
@@ -100,6 +98,7 @@ module Dentaku
|
|
100
98
|
|
101
99
|
when :operator, :comparator, :combinator
|
102
100
|
op_class = operation(token)
|
101
|
+
op_class = op_class.resolve_class(input.first)
|
103
102
|
|
104
103
|
if op_class.right_associative?
|
105
104
|
while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
data/lib/dentaku/token.rb
CHANGED
@@ -20,10 +20,22 @@ module Dentaku
|
|
20
20
|
length.zero?
|
21
21
|
end
|
22
22
|
|
23
|
+
def operator?
|
24
|
+
is?(:operator)
|
25
|
+
end
|
26
|
+
|
23
27
|
def grouping?
|
24
28
|
is?(:grouping)
|
25
29
|
end
|
26
30
|
|
31
|
+
def open?
|
32
|
+
grouping? && value == :open
|
33
|
+
end
|
34
|
+
|
35
|
+
def close?
|
36
|
+
grouping? && value == :close
|
37
|
+
end
|
38
|
+
|
27
39
|
def is?(c)
|
28
40
|
category == c
|
29
41
|
end
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/all_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
|
|
4
4
|
|
5
5
|
describe Dentaku::AST::All do
|
6
6
|
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
7
8
|
it 'performs ALL operation' do
|
8
9
|
result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
|
9
10
|
expect(result).to eq(false)
|
@@ -22,4 +23,16 @@ describe Dentaku::AST::All do
|
|
22
23
|
Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
|
23
24
|
)
|
24
25
|
end
|
26
|
+
|
27
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
28
|
+
expect(
|
29
|
+
calculator.evaluate('ALL(items, item, item.value)', items: [{value: 1}, {}])
|
30
|
+
).to be_falsy
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
34
|
+
expect {
|
35
|
+
calculator.evaluate!('ALL(items, item, item.value)', items: [{value: 1}, {}])
|
36
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
37
|
+
end
|
25
38
|
end
|
data/spec/ast/any_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
|
|
4
4
|
|
5
5
|
describe Dentaku::AST::Any do
|
6
6
|
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
7
8
|
it 'performs ANY operation' do
|
8
9
|
result = Dentaku('ANY(vals, val, val > 1)', vals: [1, 2, 3])
|
9
10
|
expect(result).to eq(true)
|
@@ -20,4 +21,16 @@ describe Dentaku::AST::Any do
|
|
20
21
|
it 'raises argument error if a string is passed as identifier' do
|
21
22
|
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
|
22
23
|
end
|
24
|
+
|
25
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
26
|
+
expect(
|
27
|
+
calculator.evaluate('ANY(items, item, item.value)', items: [{value: 1}, {}])
|
28
|
+
).to be_truthy
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
32
|
+
expect {
|
33
|
+
calculator.evaluate!('ANY(items, item, item.value)', items: [{}, {value: 1}])
|
34
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
35
|
+
end
|
23
36
|
end
|
data/spec/ast/map_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
|
|
4
4
|
|
5
5
|
describe Dentaku::AST::Map do
|
6
6
|
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
7
8
|
it 'operates on each value in an array' do
|
8
9
|
result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
|
9
10
|
expect(result).to eq(9)
|
@@ -24,4 +25,16 @@ describe Dentaku::AST::Map do
|
|
24
25
|
Dentaku::ParseError, 'MAP() requires second argument to be an identifier'
|
25
26
|
)
|
26
27
|
end
|
28
|
+
|
29
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
30
|
+
expect(
|
31
|
+
calculator.evaluate('MAP(items, item, item.value)', items: [{value: 1}, {}])
|
32
|
+
).to eq([1, nil])
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
36
|
+
expect {
|
37
|
+
calculator.evaluate!('MAP(items, item, item.value)', items: [{value: 1}, {}])
|
38
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
39
|
+
end
|
27
40
|
end
|
data/spec/ast/pluck_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
|
|
4
4
|
|
5
5
|
describe Dentaku::AST::Pluck do
|
6
6
|
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
7
8
|
it 'operates on each value in an array' do
|
8
9
|
result = Dentaku('PLUCK(users, age)', users: [
|
9
10
|
{name: "Bob", age: 44},
|
@@ -12,6 +13,22 @@ describe Dentaku::AST::Pluck do
|
|
12
13
|
expect(result).to eq([44, 27])
|
13
14
|
end
|
14
15
|
|
16
|
+
it 'allows specifying a default for missing values' do
|
17
|
+
result = Dentaku!('PLUCK(users, age, -1)', users: [
|
18
|
+
{name: "Bob"},
|
19
|
+
{name: "Jane", age: 27}
|
20
|
+
])
|
21
|
+
expect(result).to eq([-1, 27])
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns nil if pluck key is missing from a hash' do
|
25
|
+
result = Dentaku!('PLUCK(users, age)', users: [
|
26
|
+
{name: "Bob"},
|
27
|
+
{name: "Jane", age: 27}
|
28
|
+
])
|
29
|
+
expect(result).to eq([nil, 27])
|
30
|
+
end
|
31
|
+
|
15
32
|
it 'works with an empty array' do
|
16
33
|
result = Dentaku('PLUCK(users, age)', users: [])
|
17
34
|
expect(result).to eq([])
|
@@ -107,6 +107,22 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
107
107
|
end
|
108
108
|
|
109
109
|
describe "#solve" do
|
110
|
+
it 'resolves capitalized keys when they are declared out of order' do
|
111
|
+
expressions = {
|
112
|
+
FIRST: "SECOND * 2",
|
113
|
+
SECOND: "THIRD * 2",
|
114
|
+
THIRD: 2,
|
115
|
+
}
|
116
|
+
|
117
|
+
result = described_class.new(expressions, calculator).solve
|
118
|
+
|
119
|
+
expect(result).to eq(
|
120
|
+
FIRST: 8,
|
121
|
+
SECOND: 4,
|
122
|
+
THIRD: 2
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
110
126
|
it "returns :undefined when variables are unbound" do
|
111
127
|
expressions = {more_apples: "apples + 1"}
|
112
128
|
expect(described_class.new(expressions, calculator).solve)
|
data/spec/calculator_spec.rb
CHANGED
@@ -30,7 +30,6 @@ describe Dentaku::Calculator do
|
|
30
30
|
expect(calculator.evaluate('0 * 10 ^ -5')).to eq(0)
|
31
31
|
expect(calculator.evaluate('3 + 0 * -3')).to eq(3)
|
32
32
|
expect(calculator.evaluate('3 + 0 / -3')).to eq(3)
|
33
|
-
expect(calculator.evaluate('15 % 8')).to eq(7)
|
34
33
|
expect(calculator.evaluate('(((695759/735000)^(1/(1981-1991)))-1)*1000').round(4)).to eq(5.5018)
|
35
34
|
expect(calculator.evaluate('0.253/0.253')).to eq(1)
|
36
35
|
expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
|
@@ -40,11 +39,20 @@ describe Dentaku::Calculator do
|
|
40
39
|
expect(calculator.evaluate('t + 1*24*60*60', t: Time.local(2017, 1, 1))).to eq(Time.local(2017, 1, 2))
|
41
40
|
expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
|
42
41
|
expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
|
43
|
-
expect(calculator.evaluate("5%")).to eq (0.05)
|
44
42
|
expect(calculator.evaluate('1 << 3')).to eq (8)
|
45
43
|
expect(calculator.evaluate('0xFF >> 6')).to eq (3)
|
46
44
|
end
|
47
45
|
|
46
|
+
it "differentiates between percentage and modulo operators" do
|
47
|
+
expect(calculator.evaluate('15 % 8')).to eq(7)
|
48
|
+
expect(calculator.evaluate('15 % (4 * 2)')).to eq(7)
|
49
|
+
expect(calculator.evaluate("5%")).to eq (0.05)
|
50
|
+
expect(calculator.evaluate("400/60%").round(2)).to eq (666.67)
|
51
|
+
expect(calculator.evaluate("(400/60%)*1").round(2)).to eq (666.67)
|
52
|
+
expect(calculator.evaluate("60% * 1").round(2)).to eq (0.60)
|
53
|
+
expect(calculator.evaluate("50% + 50%")).to eq (1.0)
|
54
|
+
end
|
55
|
+
|
48
56
|
describe 'evaluate' do
|
49
57
|
it 'returns nil when formula has error' do
|
50
58
|
expect(calculator.evaluate('1 + + 1')).to be_nil
|
@@ -165,7 +173,6 @@ describe Dentaku::Calculator do
|
|
165
173
|
expect(calculator.solve(diff: "d1 - d2")).to eq(diff: -4)
|
166
174
|
end
|
167
175
|
|
168
|
-
|
169
176
|
it 'stores nested hashes' do
|
170
177
|
calculator.store(a: {basket: {of: 'apples'}}, b: 2)
|
171
178
|
expect(calculator.evaluate!('a.basket.of')).to eq('apples')
|
@@ -516,6 +523,17 @@ describe Dentaku::Calculator do
|
|
516
523
|
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
|
517
524
|
expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
|
518
525
|
end
|
526
|
+
|
527
|
+
it 'from time object' do
|
528
|
+
value = Time.local(2023, 7, 13, 10, 42, 11)
|
529
|
+
value2 = Time.local(2023, 12, 1, 9, 42, 10)
|
530
|
+
|
531
|
+
expect(calculator.evaluate!('value + duration(1, month)', { value: value })).to eq(Time.local(2023, 8, 13, 10, 42, 11))
|
532
|
+
expect(calculator.evaluate!('value - duration(1, day)', { value: value })).to eq(Time.local(2023, 7, 12, 10, 42, 11))
|
533
|
+
expect(calculator.evaluate!('value - duration(1, year)', { value: value })).to eq(Time.local(2022, 7, 13, 10, 42, 11))
|
534
|
+
expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq(12_182_399.0)
|
535
|
+
expect(calculator.evaluate!('value - 7200', { value: value })).to eq(Time.local(2023, 7, 13, 8, 42, 11))
|
536
|
+
end
|
519
537
|
end
|
520
538
|
|
521
539
|
describe 'functions' do
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/dependency_resolver'
|
3
|
+
|
4
|
+
describe Dentaku::DependencyResolver do
|
5
|
+
it 'sorts expressions in dependency order' do
|
6
|
+
dependencies = {"first" => ["second"], "second" => ["third"], "third" => []}
|
7
|
+
expect(described_class.find_resolve_order(dependencies)).to eq(
|
8
|
+
["third", "second", "first"]
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'handles case differences' do
|
13
|
+
dependencies = {"FIRST" => ["second"], "SeCoNd" => ["third"], "THIRD" => []}
|
14
|
+
expect(described_class.find_resolve_order(dependencies)).to eq(
|
15
|
+
["THIRD", "SeCoNd", "FIRST"]
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
data/spec/visitor_spec.rb
CHANGED
@@ -108,7 +108,7 @@ describe TestVisitor do
|
|
108
108
|
it 'visits all concrete AST node types' do
|
109
109
|
@visited = Set.new
|
110
110
|
|
111
|
-
visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples')
|
111
|
+
visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples * 5%')
|
112
112
|
visit_nodes('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
|
113
113
|
visit_nodes('IF(a[0] = NULL, "five", \'seven\')')
|
114
114
|
visit_nodes('case (a % 5) when 0 then a else b end')
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bigdecimal
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: concurrent-ruby
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -259,6 +273,7 @@ files:
|
|
259
273
|
- spec/bulk_expression_solver_spec.rb
|
260
274
|
- spec/calculator_spec.rb
|
261
275
|
- spec/dentaku_spec.rb
|
276
|
+
- spec/dependency_resolver_spec.rb
|
262
277
|
- spec/exceptions_spec.rb
|
263
278
|
- spec/external_function_spec.rb
|
264
279
|
- spec/parser_spec.rb
|
@@ -330,6 +345,7 @@ test_files:
|
|
330
345
|
- spec/bulk_expression_solver_spec.rb
|
331
346
|
- spec/calculator_spec.rb
|
332
347
|
- spec/dentaku_spec.rb
|
348
|
+
- spec/dependency_resolver_spec.rb
|
333
349
|
- spec/exceptions_spec.rb
|
334
350
|
- spec/external_function_spec.rb
|
335
351
|
- spec/parser_spec.rb
|