dentaku 3.5.3 → 3.5.5
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/.github/workflows/rspec.yml +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/CHANGELOG.md +22 -6
- data/README.md +1 -4
- data/dentaku.gemspec +1 -1
- data/lib/dentaku/ast/access.rb +0 -3
- data/lib/dentaku/ast/arithmetic.rb +36 -50
- data/lib/dentaku/ast/array.rb +1 -4
- data/lib/dentaku/ast/case.rb +12 -0
- 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/ast/operation.rb +5 -0
- data/lib/dentaku/ast.rb +1 -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 +206 -213
- data/lib/dentaku/print_visitor.rb +2 -2
- data/lib/dentaku/token.rb +12 -0
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +1 -1
- data/spec/ast/addition_spec.rb +12 -7
- data/spec/ast/all_spec.rb +13 -0
- data/spec/ast/any_spec.rb +13 -0
- data/spec/ast/arithmetic_spec.rb +7 -0
- data/spec/ast/division_spec.rb +10 -6
- data/spec/ast/map_spec.rb +13 -0
- data/spec/ast/pluck_spec.rb +17 -0
- data/spec/bulk_expression_solver_spec.rb +24 -1
- data/spec/calculator_spec.rb +21 -3
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/external_function_spec.rb +1 -1
- data/spec/parser_spec.rb +11 -2
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/visitor_spec.rb +1 -1
- metadata +10 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42f22227c2d7aef33e71f5c177f65ca3e7855115466d4d04851e6286bcbe1955
|
4
|
+
data.tar.gz: e779f142e5f458aed8437539bfe98ee73b4bf86a0ddb280cceec586ebcd62190
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c4305e2d7f5c8289edcc5f530dbb0d98cbc95f6db59a8ba98561506f2ec7e41ef89c6ab7271d8754f0a0dfc5ade6325833269f38595593c39eca44c8dcce7c5
|
7
|
+
data.tar.gz: 9a186d4164b031bbe27fb1490184d9c23387849267d4b1067c6bec6a39afdee4e6bf97d8bbbb364f36474fdad0e37e59f4ef63dae1454055244bc06f6791905b
|
@@ -0,0 +1,26 @@
|
|
1
|
+
name: rspec
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
pull_request:
|
5
|
+
jobs:
|
6
|
+
rspec:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
ruby:
|
11
|
+
- '2.5'
|
12
|
+
- '2.6'
|
13
|
+
- '2.7'
|
14
|
+
- '3.0'
|
15
|
+
- '3.1'
|
16
|
+
- '3.2'
|
17
|
+
- '3.3'
|
18
|
+
- '3.4'
|
19
|
+
fail-fast: false
|
20
|
+
steps:
|
21
|
+
- uses: actions/checkout@v4
|
22
|
+
- uses: ruby/setup-ruby@v1
|
23
|
+
with:
|
24
|
+
ruby-version: "${{ matrix.ruby }}"
|
25
|
+
bundler-cache: true
|
26
|
+
- run: bundle exec rspec --format documentation
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## [
|
3
|
+
## [v3.5.5] 2025-08-20
|
4
|
+
- fix percentages in print visitor
|
5
|
+
- repo cleanup
|
6
|
+
- fix modulo zero
|
7
|
+
- fix array arithmetic
|
8
|
+
- refactor parser
|
9
|
+
|
10
|
+
## [v3.5.4] 2024-08-13
|
11
|
+
- add support for default value for PLUCK function
|
12
|
+
- improve error handling for MAP/ANY/ALL functions
|
13
|
+
- fix modulo / percentage operator determination
|
14
|
+
- fix string casing bug with bulk expressions
|
15
|
+
- add explicit gem dependency for BigDecimal
|
16
|
+
|
17
|
+
## [v3.5.3] 2024-07-04
|
4
18
|
- add support for empty array literals
|
5
19
|
- add support for quoted identifiers
|
6
20
|
- add REDUCE function
|
@@ -9,7 +23,7 @@
|
|
9
23
|
- improve custom class arithmetic
|
10
24
|
- fix IF dependency
|
11
25
|
|
12
|
-
## [v3.5.2]
|
26
|
+
## [v3.5.2] 2023-12-06
|
13
27
|
- add ABS function
|
14
28
|
- add array support for AST visitors
|
15
29
|
- add support for function callbacks
|
@@ -22,14 +36,14 @@
|
|
22
36
|
- fix handling of Math::DomainError
|
23
37
|
- fix invalid cast
|
24
38
|
|
25
|
-
## [v3.5.1]
|
39
|
+
## [v3.5.1] 2022-10-24
|
26
40
|
- add bitwise shift left and shift right operators
|
27
41
|
- improve numeric conversions
|
28
42
|
- improve parse exceptions
|
29
43
|
- improve bitwise exceptions
|
30
44
|
- include variable name in bulk expression exceptions
|
31
45
|
|
32
|
-
## [v3.5.0]
|
46
|
+
## [v3.5.0] 2022-03-17
|
33
47
|
- fix bug with function argument count
|
34
48
|
- add XOR operator
|
35
49
|
- make function args publicly accessible
|
@@ -41,7 +55,7 @@
|
|
41
55
|
- respect case sensitivity in nested case statments
|
42
56
|
- add visitor pattern
|
43
57
|
|
44
|
-
## [v3.4.2]
|
58
|
+
## [v3.4.2] 2021-07-14
|
45
59
|
- add FILTER function
|
46
60
|
- add concurrent-ruby dependency to make global calculator object thread safe
|
47
61
|
- add Ruby 3 support
|
@@ -253,7 +267,9 @@
|
|
253
267
|
## [v0.1.0] 2012-01-20
|
254
268
|
- initial release
|
255
269
|
|
256
|
-
[
|
270
|
+
[v3.5.5]: https://github.com/rubysolo/dentaku/compare/v3.5.4...v3.5.5
|
271
|
+
[v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
|
272
|
+
[v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
|
257
273
|
[v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
|
258
274
|
[v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
|
259
275
|
[v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
|
data/README.md
CHANGED
@@ -3,9 +3,6 @@ Dentaku
|
|
3
3
|
|
4
4
|
[](https://gitter.im/rubysolo/dentaku?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
5
5
|
[](http://badge.fury.io/rb/dentaku)
|
6
|
-
[](https://travis-ci.org/rubysolo/dentaku)
|
7
|
-
[](https://codeclimate.com/github/rubysolo/dentaku)
|
8
|
-
[](https://codecov.io/gh/rubysolo/dentaku)
|
9
6
|
|
10
7
|
|
11
8
|
DESCRIPTION
|
@@ -147,7 +144,7 @@ Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`
|
|
147
144
|
|
148
145
|
Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`
|
149
146
|
|
150
|
-
Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/
|
147
|
+
Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/main/lib/dentaku/ast/case.rb))
|
151
148
|
|
152
149
|
String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`, `CONTAINS`
|
153
150
|
|
data/dentaku.gemspec
CHANGED
@@ -14,9 +14,9 @@ Gem::Specification.new do |s|
|
|
14
14
|
Dentaku is a parser and evaluator for mathematical formulas
|
15
15
|
DESC
|
16
16
|
|
17
|
+
s.add_dependency('bigdecimal')
|
17
18
|
s.add_dependency('concurrent-ruby')
|
18
19
|
|
19
|
-
s.add_development_dependency('codecov')
|
20
20
|
s.add_development_dependency('pry')
|
21
21
|
s.add_development_dependency('pry-byebug')
|
22
22
|
s.add_development_dependency('pry-stack_explorer')
|
data/lib/dentaku/ast/access.rb
CHANGED
@@ -9,20 +9,6 @@ module Dentaku
|
|
9
9
|
DECIMAL = /\A-?\d*\.\d+\z/.freeze
|
10
10
|
INTEGER = /\A-?\d+\z/.freeze
|
11
11
|
|
12
|
-
def initialize(*)
|
13
|
-
super
|
14
|
-
|
15
|
-
unless valid_left?
|
16
|
-
raise NodeError.new(:numeric, left.type, :left),
|
17
|
-
"#{self.class} requires numeric operands"
|
18
|
-
end
|
19
|
-
|
20
|
-
unless valid_right?
|
21
|
-
raise NodeError.new(:numeric, right.type, :right),
|
22
|
-
"#{self.class} requires numeric operands"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
12
|
def type
|
27
13
|
:numeric
|
28
14
|
end
|
@@ -69,8 +55,9 @@ module Dentaku
|
|
69
55
|
def datetime?(val)
|
70
56
|
# val is a Date, Time, or DateTime
|
71
57
|
return true if val.respond_to?(:strftime)
|
58
|
+
return false unless val.is_a?(::String)
|
72
59
|
|
73
|
-
val
|
60
|
+
val =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
|
74
61
|
end
|
75
62
|
|
76
63
|
def valid_node?(node)
|
@@ -179,62 +166,61 @@ module Dentaku
|
|
179
166
|
|
180
167
|
class Modulo < Arithmetic
|
181
168
|
def self.arity
|
182
|
-
|
169
|
+
2
|
183
170
|
end
|
184
171
|
|
185
|
-
def self.
|
186
|
-
|
187
|
-
@arity = 2 if input.length > 1
|
172
|
+
def self.precedence
|
173
|
+
20
|
188
174
|
end
|
189
175
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
193
|
-
@right = right
|
194
|
-
else
|
195
|
-
@right = left
|
196
|
-
end
|
176
|
+
def self.resolve_class(next_token)
|
177
|
+
next_token.nil? || next_token.operator? || next_token.close? ? Percentage : self
|
178
|
+
end
|
197
179
|
|
198
|
-
|
199
|
-
|
200
|
-
"#{self.class} requires numeric operands or nil"
|
201
|
-
end
|
202
|
-
unless valid_right?
|
203
|
-
raise NodeError.new(:numeric, right.type, :right),
|
204
|
-
"#{self.class} requires numeric operands"
|
205
|
-
end
|
180
|
+
def operator
|
181
|
+
:%
|
206
182
|
end
|
207
183
|
|
208
|
-
def
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
184
|
+
def value(context = {})
|
185
|
+
r = decimal(cast(right.value(context)))
|
186
|
+
raise Dentaku::ZeroDivisionError if r.zero?
|
187
|
+
|
188
|
+
cast(cast(left.value(context)) % r)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class Percentage < Arithmetic
|
193
|
+
def self.arity
|
194
|
+
1
|
195
|
+
end
|
196
|
+
|
197
|
+
def initialize(child)
|
198
|
+
@left = child
|
199
|
+
|
200
|
+
unless valid_left?
|
201
|
+
raise NodeError.new(:numeric, left.type, :left),
|
202
|
+
"#{self.class} requires a numeric operand"
|
213
203
|
end
|
214
204
|
end
|
215
205
|
|
216
|
-
def
|
217
|
-
left.
|
206
|
+
def dependencies(context = {})
|
207
|
+
@left.dependencies(context)
|
218
208
|
end
|
219
209
|
|
220
210
|
def value(context = {})
|
221
|
-
|
222
|
-
cast(right.value(context)) * 0.01
|
223
|
-
else
|
224
|
-
super
|
225
|
-
end
|
211
|
+
cast(left.value(context)) * 0.01
|
226
212
|
end
|
227
213
|
|
228
214
|
def operator
|
229
215
|
:%
|
230
216
|
end
|
231
217
|
|
232
|
-
def
|
233
|
-
|
218
|
+
def operator_spacing
|
219
|
+
""
|
234
220
|
end
|
235
221
|
|
236
|
-
def
|
237
|
-
|
222
|
+
def self.precedence
|
223
|
+
30
|
238
224
|
end
|
239
225
|
end
|
240
226
|
|
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
|
data/lib/dentaku/ast/case.rb
CHANGED
@@ -7,6 +7,18 @@ require 'dentaku/exceptions'
|
|
7
7
|
|
8
8
|
module Dentaku
|
9
9
|
module AST
|
10
|
+
# Examples of using in a formula:
|
11
|
+
#
|
12
|
+
# CASE x WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE END
|
13
|
+
#
|
14
|
+
# CASE fruit
|
15
|
+
# WHEN 'apple'
|
16
|
+
# THEN 1 * quantity
|
17
|
+
# WHEN 'banana'
|
18
|
+
# THEN 2 * quantity
|
19
|
+
# ELSE
|
20
|
+
# 3 * quantity
|
21
|
+
# END
|
10
22
|
class Case < Node
|
11
23
|
attr_reader :switch, :conditions, :else
|
12
24
|
|
@@ -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
data/lib/dentaku/ast.rb
CHANGED
@@ -39,4 +39,4 @@ require_relative './ast/functions/ruby_math'
|
|
39
39
|
require_relative './ast/functions/string_functions'
|
40
40
|
require_relative './ast/functions/sum'
|
41
41
|
require_relative './ast/functions/switch'
|
42
|
-
require_relative './ast/functions/xor'
|
42
|
+
require_relative './ast/functions/xor'
|
@@ -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
|