dentaku 3.5.3 → 3.5.4
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 +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
|