dentaku 3.3.4 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -7
- data/.travis.yml +3 -4
- data/CHANGELOG.md +13 -0
- data/dentaku.gemspec +0 -2
- data/lib/dentaku.rb +14 -4
- data/lib/dentaku/ast.rb +4 -0
- data/lib/dentaku/ast/access.rb +3 -1
- data/lib/dentaku/ast/arithmetic.rb +7 -2
- data/lib/dentaku/ast/array.rb +3 -1
- data/lib/dentaku/ast/function.rb +10 -1
- data/lib/dentaku/ast/functions/all.rb +36 -0
- data/lib/dentaku/ast/functions/any.rb +36 -0
- data/lib/dentaku/ast/functions/avg.rb +2 -2
- data/lib/dentaku/ast/functions/map.rb +36 -0
- data/lib/dentaku/ast/functions/mul.rb +3 -2
- data/lib/dentaku/ast/functions/pluck.rb +29 -0
- data/lib/dentaku/ast/functions/round.rb +1 -1
- data/lib/dentaku/ast/functions/rounddown.rb +1 -1
- data/lib/dentaku/ast/functions/roundup.rb +1 -1
- data/lib/dentaku/ast/functions/ruby_math.rb +47 -3
- data/lib/dentaku/ast/functions/string_functions.rb +4 -4
- data/lib/dentaku/ast/functions/sum.rb +3 -2
- data/lib/dentaku/ast/grouping.rb +3 -1
- data/lib/dentaku/ast/identifier.rb +3 -1
- data/lib/dentaku/bulk_expression_solver.rb +34 -25
- data/lib/dentaku/calculator.rb +13 -5
- data/lib/dentaku/date_arithmetic.rb +1 -1
- data/lib/dentaku/exceptions.rb +3 -3
- data/lib/dentaku/flat_hash.rb +7 -0
- data/lib/dentaku/parser.rb +2 -1
- data/lib/dentaku/tokenizer.rb +1 -1
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +19 -5
- data/spec/ast/avg_spec.rb +4 -0
- data/spec/ast/mul_spec.rb +4 -0
- data/spec/ast/negation_spec.rb +18 -2
- data/spec/ast/round_spec.rb +10 -0
- data/spec/ast/rounddown_spec.rb +10 -0
- data/spec/ast/roundup_spec.rb +10 -0
- data/spec/ast/string_functions_spec.rb +35 -0
- data/spec/ast/sum_spec.rb +4 -0
- data/spec/bulk_expression_solver_spec.rb +17 -0
- data/spec/calculator_spec.rb +112 -0
- data/spec/dentaku_spec.rb +14 -8
- data/spec/parser_spec.rb +13 -0
- data/spec/tokenizer_spec.rb +24 -5
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1eb86933b899627333d93e993cd2d093a11f4ca54b2e0f6668c3580a8f133975
|
|
4
|
+
data.tar.gz: 004bd53fa5c3c6815bafdbf98788de96757b688a6ba217b7f8e96be31a4f7d5d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32f0e44638fc3738a3df5ba6a04abdb4ee66177049242260e26fea1ad6844a66a35e65ca5a27448f118838e6ccecd133f46650e9fbff5d878bbc14b1e7d952e8
|
|
7
|
+
data.tar.gz: 8081bcf51e30daa33dbaba3f8796951151c1bfd2e3975f63e5291aac7e04e09f2fc2f00aa956cfdad99caf1b8a8fb4f04b2c0789d8909b0982372e75dc7f7fc0
|
data/.rubocop.yml
CHANGED
|
@@ -8,11 +8,6 @@ AllCops:
|
|
|
8
8
|
Style/AndOr:
|
|
9
9
|
Enabled: true
|
|
10
10
|
|
|
11
|
-
# Do not use braces for hash literals when they are the last argument of a
|
|
12
|
-
# method call.
|
|
13
|
-
Style/BracesAroundHashParameters:
|
|
14
|
-
Enabled: true
|
|
15
|
-
|
|
16
11
|
# Align `when` with `case`.
|
|
17
12
|
Layout/CaseIndentation:
|
|
18
13
|
Enabled: true
|
|
@@ -93,11 +88,11 @@ Style/StringLiterals:
|
|
|
93
88
|
EnforcedStyle: double_quotes
|
|
94
89
|
|
|
95
90
|
# Detect hard tabs, no hard tabs.
|
|
96
|
-
Layout/
|
|
91
|
+
Layout/IndentationStyle:
|
|
97
92
|
Enabled: true
|
|
98
93
|
|
|
99
94
|
# Blank lines should not have any spaces.
|
|
100
|
-
Layout/
|
|
95
|
+
Layout/TrailingEmptyLines:
|
|
101
96
|
Enabled: true
|
|
102
97
|
|
|
103
98
|
# No trailing whitespace.
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## [v3.4.0] 2020-12-07
|
|
4
|
+
- allow access to intermediate values of flattened hashes
|
|
5
|
+
- catch invalid array syntax in the parse phase
|
|
6
|
+
- drop support for Ruby < 2.5, add support for Ruby 2.7
|
|
7
|
+
- add support for subtracting date literals
|
|
8
|
+
- improve error handling
|
|
9
|
+
- improve math function implementation
|
|
10
|
+
- add caching for calculated variable values
|
|
11
|
+
- allow custom unbound variable handling block at Dentaku module level
|
|
12
|
+
- add enum functions `ANY`, `ALL`, `MAP` and `PLUCK`
|
|
13
|
+
- allow self-referential formulas in bulk expression solver
|
|
14
|
+
- misc internal fixes and enhancements
|
|
3
15
|
## [v3.3.4] 2019-11-21
|
|
4
16
|
- bugfix release
|
|
5
17
|
|
|
@@ -187,6 +199,7 @@
|
|
|
187
199
|
## [v0.1.0] 2012-01-20
|
|
188
200
|
- initial release
|
|
189
201
|
|
|
202
|
+
[v3.4.0]: https://github.com/rubysolo/dentaku/compare/v3.3.4...v3.4.0
|
|
190
203
|
[v3.3.4]: https://github.com/rubysolo/dentaku/compare/v3.3.3...v3.3.4
|
|
191
204
|
[v3.3.3]: https://github.com/rubysolo/dentaku/compare/v3.3.2...v3.3.3
|
|
192
205
|
[v3.3.2]: https://github.com/rubysolo/dentaku/compare/v3.3.1...v3.3.2
|
data/dentaku.gemspec
CHANGED
|
@@ -14,8 +14,6 @@ Gem::Specification.new do |s|
|
|
|
14
14
|
Dentaku is a parser and evaluator for mathematical formulas
|
|
15
15
|
DESC
|
|
16
16
|
|
|
17
|
-
s.rubyforge_project = "dentaku"
|
|
18
|
-
|
|
19
17
|
s.add_development_dependency('codecov')
|
|
20
18
|
s.add_development_dependency('pry')
|
|
21
19
|
s.add_development_dependency('pry-byebug')
|
data/lib/dentaku.rb
CHANGED
|
@@ -5,19 +5,21 @@ require "dentaku/version"
|
|
|
5
5
|
module Dentaku
|
|
6
6
|
@enable_ast_caching = false
|
|
7
7
|
@enable_dependency_order_caching = false
|
|
8
|
+
@enable_identifier_caching = false
|
|
8
9
|
@aliases = {}
|
|
9
10
|
|
|
10
|
-
def self.evaluate(expression, data = {})
|
|
11
|
-
calculator.evaluate(expression, data)
|
|
11
|
+
def self.evaluate(expression, data = {}, &block)
|
|
12
|
+
calculator.evaluate(expression, data, &block)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
def self.evaluate!(expression, data = {})
|
|
15
|
-
calculator.evaluate!(expression, data)
|
|
15
|
+
def self.evaluate!(expression, data = {}, &block)
|
|
16
|
+
calculator.evaluate!(expression, data, &block)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def self.enable_caching!
|
|
19
20
|
enable_ast_cache!
|
|
20
21
|
enable_dependency_order_cache!
|
|
22
|
+
enable_identifier_cache!
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def self.enable_ast_cache!
|
|
@@ -36,6 +38,14 @@ module Dentaku
|
|
|
36
38
|
@enable_dependency_order_caching
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
def self.enable_identifier_cache!
|
|
42
|
+
@enable_identifier_caching = true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.cache_identifier?
|
|
46
|
+
@enable_identifier_caching
|
|
47
|
+
end
|
|
48
|
+
|
|
39
49
|
def self.aliases
|
|
40
50
|
@aliases
|
|
41
51
|
end
|
data/lib/dentaku/ast.rb
CHANGED
|
@@ -15,15 +15,19 @@ require_relative './ast/array'
|
|
|
15
15
|
require_relative './ast/grouping'
|
|
16
16
|
require_relative './ast/case'
|
|
17
17
|
require_relative './ast/function_registry'
|
|
18
|
+
require_relative './ast/functions/all'
|
|
18
19
|
require_relative './ast/functions/and'
|
|
20
|
+
require_relative './ast/functions/any'
|
|
19
21
|
require_relative './ast/functions/avg'
|
|
20
22
|
require_relative './ast/functions/count'
|
|
21
23
|
require_relative './ast/functions/duration'
|
|
22
24
|
require_relative './ast/functions/if'
|
|
25
|
+
require_relative './ast/functions/map'
|
|
23
26
|
require_relative './ast/functions/max'
|
|
24
27
|
require_relative './ast/functions/min'
|
|
25
28
|
require_relative './ast/functions/not'
|
|
26
29
|
require_relative './ast/functions/or'
|
|
30
|
+
require_relative './ast/functions/pluck'
|
|
27
31
|
require_relative './ast/functions/round'
|
|
28
32
|
require_relative './ast/functions/rounddown'
|
|
29
33
|
require_relative './ast/functions/roundup'
|
data/lib/dentaku/ast/access.rb
CHANGED
|
@@ -31,7 +31,12 @@ module Dentaku
|
|
|
31
31
|
def value(context = {})
|
|
32
32
|
l = cast(left.value(context))
|
|
33
33
|
r = cast(right.value(context))
|
|
34
|
-
|
|
34
|
+
begin
|
|
35
|
+
l.public_send(operator, r)
|
|
36
|
+
rescue ::TypeError => e
|
|
37
|
+
# Right cannot be converted to a suitable type for left. e.g. [] + 1
|
|
38
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
private
|
|
@@ -60,7 +65,7 @@ module Dentaku
|
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
def valid_right?
|
|
63
|
-
valid_node?(right) || right.type == :duration
|
|
68
|
+
valid_node?(right) || right.type == :duration || right.type == :datetime
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
def validate_value(val)
|
data/lib/dentaku/ast/array.rb
CHANGED
data/lib/dentaku/ast/function.rb
CHANGED
|
@@ -12,7 +12,16 @@ module Dentaku
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def dependencies(context = {})
|
|
15
|
-
|
|
15
|
+
deferred = deferred_args
|
|
16
|
+
@args.each_with_index
|
|
17
|
+
.reject { |_, i| deferred.include? i }
|
|
18
|
+
.flat_map { |a, _| a.dependencies(context) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# override if your function implementation needs to defer evaluation of
|
|
22
|
+
# any arguments
|
|
23
|
+
def deferred_args
|
|
24
|
+
[]
|
|
16
25
|
end
|
|
17
26
|
|
|
18
27
|
def self.get(name)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
require_relative '../../exceptions'
|
|
3
|
+
|
|
4
|
+
module Dentaku
|
|
5
|
+
module AST
|
|
6
|
+
class All < Function
|
|
7
|
+
def self.min_param_count
|
|
8
|
+
3
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.max_param_count
|
|
12
|
+
3
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def deferred_args
|
|
16
|
+
[1, 2]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value(context = {})
|
|
20
|
+
collection = @args[0].value(context)
|
|
21
|
+
item_identifier = @args[1].identifier
|
|
22
|
+
expression = @args[2]
|
|
23
|
+
|
|
24
|
+
Array(collection).all? do |item_value|
|
|
25
|
+
expression.value(
|
|
26
|
+
context.update(
|
|
27
|
+
FlatHash.from_hash(item_identifier => item_value)
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Dentaku::AST::Function.register_class(:all, Dentaku::AST::All)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
require_relative '../../exceptions'
|
|
3
|
+
|
|
4
|
+
module Dentaku
|
|
5
|
+
module AST
|
|
6
|
+
class Any < Function
|
|
7
|
+
def self.min_param_count
|
|
8
|
+
3
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.max_param_count
|
|
12
|
+
3
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def deferred_args
|
|
16
|
+
[1, 2]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value(context = {})
|
|
20
|
+
collection = @args[0].value(context)
|
|
21
|
+
item_identifier = @args[1].identifier
|
|
22
|
+
expression = @args[2]
|
|
23
|
+
|
|
24
|
+
Array(collection).any? do |item_value|
|
|
25
|
+
expression.value(
|
|
26
|
+
context.update(
|
|
27
|
+
FlatHash.from_hash(item_identifier => item_value)
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Dentaku::AST::Function.register_class(:any, Dentaku::AST::Any)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
|
|
4
|
-
|
|
4
|
+
flatten_args = args.flatten
|
|
5
|
+
if flatten_args.empty?
|
|
5
6
|
raise Dentaku::ArgumentError.for(
|
|
6
7
|
:too_few_arguments,
|
|
7
8
|
function_name: 'AVG()', at_least: 1, given: 0
|
|
8
9
|
), 'AVG() requires at least one argument'
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
flatten_args = args.flatten
|
|
12
12
|
flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / flatten_args.length
|
|
13
13
|
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
require_relative '../../exceptions'
|
|
3
|
+
|
|
4
|
+
module Dentaku
|
|
5
|
+
module AST
|
|
6
|
+
class Map < Function
|
|
7
|
+
def self.min_param_count
|
|
8
|
+
3
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.max_param_count
|
|
12
|
+
3
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def deferred_args
|
|
16
|
+
[1, 2]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value(context = {})
|
|
20
|
+
collection = @args[0].value(context)
|
|
21
|
+
item_identifier = @args[1].identifier
|
|
22
|
+
expression = @args[2]
|
|
23
|
+
|
|
24
|
+
collection.map do |item_value|
|
|
25
|
+
expression.value(
|
|
26
|
+
context.update(
|
|
27
|
+
FlatHash.from_hash(item_identifier => item_value)
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Dentaku::AST::Function.register_class(:map, Dentaku::AST::Map)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:mul, :numeric, ->(*args) {
|
|
4
|
-
|
|
4
|
+
flatten_args = args.flatten
|
|
5
|
+
if flatten_args.empty?
|
|
5
6
|
raise Dentaku::ArgumentError.for(
|
|
6
7
|
:too_few_arguments,
|
|
7
8
|
function_name: 'MUL()', at_least: 1, given: 0
|
|
8
9
|
), 'MUL() requires at least one argument'
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
|
|
12
13
|
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
require_relative '../../exceptions'
|
|
3
|
+
|
|
4
|
+
module Dentaku
|
|
5
|
+
module AST
|
|
6
|
+
class Pluck < Function
|
|
7
|
+
def self.min_param_count
|
|
8
|
+
2
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.max_param_count
|
|
12
|
+
2
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def deferred_args
|
|
16
|
+
[1]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value(context = {})
|
|
20
|
+
collection = @args[0].value(context)
|
|
21
|
+
pluck_path = @args[1].identifier
|
|
22
|
+
|
|
23
|
+
collection.map { |h| h.transform_keys(&:to_s)[pluck_path] }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Dentaku::AST::Function.register_class(:pluck, Dentaku::AST::Pluck)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:round, :numeric, lambda { |numeric, places = 0|
|
|
4
|
-
Dentaku::AST::Function.numeric(numeric).round(places.to_i)
|
|
4
|
+
Dentaku::AST::Function.numeric(numeric).round(Dentaku::AST::Function.numeric(places || 0).to_i)
|
|
5
5
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:rounddown, :numeric, lambda { |numeric, precision = 0|
|
|
4
|
-
precision = precision.to_i
|
|
4
|
+
precision = Dentaku::AST::Function.numeric(precision || 0).to_i
|
|
5
5
|
tens = 10.0**precision
|
|
6
6
|
result = (Dentaku::AST::Function.numeric(numeric) * tens).floor / tens
|
|
7
7
|
precision <= 0 ? result.to_i : result
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:roundup, :numeric, lambda { |numeric, precision = 0|
|
|
4
|
-
precision = precision.to_i
|
|
4
|
+
precision = Dentaku::AST::Function.numeric(precision || 0).to_i
|
|
5
5
|
tens = 10.0**precision
|
|
6
6
|
result = (Dentaku::AST::Function.numeric(numeric) * tens).ceil / tens
|
|
7
7
|
precision <= 0 ? result.to_i : result
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
# import all functions from Ruby's Math module
|
|
2
2
|
require_relative '../function'
|
|
3
3
|
|
|
4
|
+
module Dentaku
|
|
5
|
+
module AST
|
|
6
|
+
class RubyMath < Function
|
|
7
|
+
def self.[](method)
|
|
8
|
+
klass = Class.new(self)
|
|
9
|
+
klass.implement(method)
|
|
10
|
+
klass
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.implement(method)
|
|
14
|
+
@name = method
|
|
15
|
+
@implementation = Math.method(method)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.name
|
|
19
|
+
@name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.arity
|
|
23
|
+
@implementation.arity < 0 ? nil : @implementation.arity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.min_param_count
|
|
27
|
+
@implementation.parameters.select { |type, _name| type == :req }.count
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.max_param_count
|
|
31
|
+
@implementation.parameters.select { |type, _name| type == :rest }.any? ? Float::INFINITY : @implementation.parameters.count
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.call(*args)
|
|
35
|
+
@implementation.call(*args)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def value(context = {})
|
|
39
|
+
args = @args.flatten.map { |a| Dentaku::AST::Function.numeric(a.value(context)) }
|
|
40
|
+
self.class.call(*args)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def type
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
4
50
|
Math.methods(false).each do |method|
|
|
5
|
-
Dentaku::AST::Function.
|
|
6
|
-
Math.send(method, *args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) })
|
|
7
|
-
})
|
|
51
|
+
Dentaku::AST::Function.register_class(method, Dentaku::AST::RubyMath[method])
|
|
8
52
|
end
|