dentaku 3.4.1 → 3.4.2
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/.travis.yml +4 -3
- data/CHANGELOG.md +11 -1
- data/README.md +2 -0
- data/dentaku.gemspec +2 -0
- data/lib/dentaku.rb +4 -3
- data/lib/dentaku/ast/functions/all.rb +2 -2
- data/lib/dentaku/ast/functions/any.rb +2 -2
- data/lib/dentaku/ast/functions/filter.rb +36 -0
- data/lib/dentaku/ast/functions/map.rb +2 -2
- data/lib/dentaku/ast/functions/mul.rb +2 -3
- data/lib/dentaku/ast/functions/ruby_math.rb +3 -1
- data/lib/dentaku/ast/functions/sum.rb +2 -3
- data/lib/dentaku/ast/identifier.rb +3 -3
- data/lib/dentaku/calculator.rb +1 -1
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/filter_spec.rb +18 -0
- data/spec/ast/map_spec.rb +15 -0
- data/spec/ast/max_spec.rb +13 -0
- data/spec/ast/min_spec.rb +13 -0
- data/spec/ast/mul_spec.rb +3 -2
- data/spec/ast/sum_spec.rb +3 -2
- data/spec/calculator_spec.rb +8 -0
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3dadbc209e58b2d2206dfaf48b83be3224db37f71d60cf590aadb63be9f4bebd
|
|
4
|
+
data.tar.gz: 86530c9c3333139994775a6bde16202005d89161a0b98670db58bcdda47f8e55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f132401168218a3c021124ddabb56bbf485550bdff60a03a291ad811d8628e25caab2ba36e4bed4f8148bde048e56bed1fa31480774f2c19617a79282da81280
|
|
7
|
+
data.tar.gz: 3b6a8fc5e40988a9442be9dbdf890dc813e13b95fc2d0cb2f60252b2492d07fdf4ba47cdfedd0a85c002aaaa23a83bffb141b8f8f31fbf518678439d55c1e795
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## [v3.4.2]
|
|
4
|
+
- add FILTER function
|
|
5
|
+
- add concurrent-ruby dependency to make global calculator object thread safe
|
|
6
|
+
- add Ruby 3 support
|
|
7
|
+
- allow formulas to access intermediate context values
|
|
8
|
+
- fix incorrect Ruby Math function return type
|
|
9
|
+
- fix context mutation bug
|
|
10
|
+
- fix dependency resolution bug
|
|
11
|
+
|
|
3
12
|
## [v3.4.1] 2020-12-12
|
|
4
13
|
- prevent extra evaluations in bulk expression solver
|
|
5
14
|
|
|
@@ -203,7 +212,8 @@
|
|
|
203
212
|
## [v0.1.0] 2012-01-20
|
|
204
213
|
- initial release
|
|
205
214
|
|
|
206
|
-
[
|
|
215
|
+
[v3.4.2]: https://github.com/rubysolo/dentaku/compare/v3.4.1...v3.4.2
|
|
216
|
+
[v3.4.1]: https://github.com/rubysolo/dentaku/compare/v3.4.0...v3.4.1
|
|
207
217
|
[v3.4.0]: https://github.com/rubysolo/dentaku/compare/v3.3.4...v3.4.0
|
|
208
218
|
[v3.3.4]: https://github.com/rubysolo/dentaku/compare/v3.3.3...v3.3.4
|
|
209
219
|
[v3.3.3]: https://github.com/rubysolo/dentaku/compare/v3.3.2...v3.3.3
|
data/README.md
CHANGED
|
@@ -152,6 +152,8 @@ Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/m
|
|
|
152
152
|
|
|
153
153
|
String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`, `CONTAINS`
|
|
154
154
|
|
|
155
|
+
Collection: `MAP`, `FILTER`, `ALL`, `ANY`, `PLUCK`
|
|
156
|
+
|
|
155
157
|
RESOLVING DEPENDENCIES
|
|
156
158
|
----------------------
|
|
157
159
|
|
data/dentaku.gemspec
CHANGED
|
@@ -14,6 +14,8 @@ 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('concurrent-ruby')
|
|
18
|
+
|
|
17
19
|
s.add_development_dependency('codecov')
|
|
18
20
|
s.add_development_dependency('pry')
|
|
19
21
|
s.add_development_dependency('pry-byebug')
|
data/lib/dentaku.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "bigdecimal"
|
|
2
|
+
require "concurrent"
|
|
2
3
|
require "dentaku/calculator"
|
|
3
4
|
require "dentaku/version"
|
|
4
5
|
|
|
@@ -9,11 +10,11 @@ module Dentaku
|
|
|
9
10
|
@aliases = {}
|
|
10
11
|
|
|
11
12
|
def self.evaluate(expression, data = {}, &block)
|
|
12
|
-
calculator.evaluate(expression, data, &block)
|
|
13
|
+
calculator.value.evaluate(expression, data, &block)
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def self.evaluate!(expression, data = {}, &block)
|
|
16
|
-
calculator.evaluate!(expression, data, &block)
|
|
17
|
+
calculator.value.evaluate!(expression, data, &block)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.enable_caching!
|
|
@@ -55,7 +56,7 @@ module Dentaku
|
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def self.calculator
|
|
58
|
-
@calculator ||= Dentaku::Calculator.new
|
|
59
|
+
@calculator ||= Concurrent::ThreadLocalVar.new { Dentaku::Calculator.new }
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
@@ -23,8 +23,8 @@ module Dentaku
|
|
|
23
23
|
|
|
24
24
|
Array(collection).all? do |item_value|
|
|
25
25
|
expression.value(
|
|
26
|
-
context.
|
|
27
|
-
FlatHash.
|
|
26
|
+
context.merge(
|
|
27
|
+
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
|
28
28
|
)
|
|
29
29
|
)
|
|
30
30
|
end
|
|
@@ -23,8 +23,8 @@ module Dentaku
|
|
|
23
23
|
|
|
24
24
|
Array(collection).any? do |item_value|
|
|
25
25
|
expression.value(
|
|
26
|
-
context.
|
|
27
|
-
FlatHash.
|
|
26
|
+
context.merge(
|
|
27
|
+
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
|
28
28
|
)
|
|
29
29
|
)
|
|
30
30
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
require_relative '../../exceptions'
|
|
3
|
+
|
|
4
|
+
module Dentaku
|
|
5
|
+
module AST
|
|
6
|
+
class Filter < 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).select do |item_value|
|
|
25
|
+
expression.value(
|
|
26
|
+
context.merge(
|
|
27
|
+
FlatHash.from_hash_with_intermediates(item_identifier => item_value)
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Dentaku::AST::Function.register_class(:filter, Dentaku::AST::Filter)
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:mul, :numeric, ->(*args) {
|
|
4
|
-
|
|
5
|
-
if flatten_args.empty?
|
|
4
|
+
if args.empty?
|
|
6
5
|
raise Dentaku::ArgumentError.for(
|
|
7
6
|
:too_few_arguments,
|
|
8
7
|
function_name: 'MUL()', at_least: 1, given: 0
|
|
9
8
|
), 'MUL() requires at least one argument'
|
|
10
9
|
end
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
|
|
13
12
|
})
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:sum, :numeric, ->(*args) {
|
|
4
|
-
|
|
5
|
-
if flatten_args.empty?
|
|
4
|
+
if args.empty?
|
|
6
5
|
raise Dentaku::ArgumentError.for(
|
|
7
6
|
:too_few_arguments,
|
|
8
7
|
function_name: 'SUM()', at_least: 1, given: 0
|
|
9
8
|
), 'SUM() requires at least one argument'
|
|
10
9
|
end
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+)
|
|
13
12
|
})
|
|
@@ -31,13 +31,13 @@ module Dentaku
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def dependencies(context = {})
|
|
34
|
-
context.key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
|
|
34
|
+
context.key?(identifier) ? dependencies_of(context[identifier], context) : [identifier]
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
private
|
|
38
38
|
|
|
39
|
-
def dependencies_of(node)
|
|
40
|
-
node.respond_to?(:dependencies) ? node.dependencies : []
|
|
39
|
+
def dependencies_of(node, context)
|
|
40
|
+
node.respond_to?(:dependencies) ? node.dependencies(context) : []
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
end
|
data/lib/dentaku/calculator.rb
CHANGED
|
@@ -59,7 +59,7 @@ module Dentaku
|
|
|
59
59
|
store(data) do
|
|
60
60
|
node = expression
|
|
61
61
|
node = ast(node) unless node.is_a?(AST::Node)
|
|
62
|
-
unbound = node.dependencies
|
|
62
|
+
unbound = node.dependencies(memory)
|
|
63
63
|
unless unbound.empty?
|
|
64
64
|
raise UnboundVariableError.new(unbound),
|
|
65
65
|
"no value provided for variables: #{unbound.uniq.join(', ')}"
|
data/lib/dentaku/version.rb
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/filter'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe Dentaku::AST::Filter do
|
|
6
|
+
it 'excludes unmatched values' do
|
|
7
|
+
result = Dentaku('SUM(FILTER(vals, val, val > 1))', vals: [1, 2, 3])
|
|
8
|
+
expect(result).to eq(5)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'works with a single value if needed for some reason' do
|
|
12
|
+
result = Dentaku('FILTER(vals, val, val > 1)', vals: 1)
|
|
13
|
+
expect(result).to eq([])
|
|
14
|
+
|
|
15
|
+
result = Dentaku('FILTER(vals, val, val > 1)', vals: 2)
|
|
16
|
+
expect(result).to eq([2])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/map'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe Dentaku::AST::Map do
|
|
6
|
+
it 'operates on each value in an array' do
|
|
7
|
+
result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
|
|
8
|
+
expect(result).to eq(9)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'works with an empty array' do
|
|
12
|
+
result = Dentaku('MAP(vals, val, val + 1)', vals: [])
|
|
13
|
+
expect(result).to eq([])
|
|
14
|
+
end
|
|
15
|
+
end
|
data/spec/ast/max_spec.rb
CHANGED
|
@@ -17,4 +17,17 @@ describe 'Dentaku::AST::Function::Max' do
|
|
|
17
17
|
result = Dentaku('MAX(1, x, 1.8)', x: [1.5, 2.3, 1.7])
|
|
18
18
|
expect(result).to eq(2.3)
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
it 'returns the largest value if only an Array is passed' do
|
|
22
|
+
result = Dentaku('MAX(x)', x: [1.5, 2.3, 1.7])
|
|
23
|
+
expect(result).to eq(2.3)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'checking errors' do
|
|
27
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
28
|
+
|
|
29
|
+
it 'does not raise an error if an empty array is passed' do
|
|
30
|
+
expect(calculator.evaluate!('MAX(x)', x: [])).to eq(nil)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
20
33
|
end
|
data/spec/ast/min_spec.rb
CHANGED
|
@@ -17,4 +17,17 @@ describe 'Dentaku::AST::Function::Min' do
|
|
|
17
17
|
result = Dentaku('MIN(1, x, 1.8)', x: [1.5, 0.3, 1.7])
|
|
18
18
|
expect(result).to eq(0.3)
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
it 'returns the smallest value if only an Array is passed' do
|
|
22
|
+
result = Dentaku('MIN(x)', x: [1.5, 2.3, 1.7])
|
|
23
|
+
expect(result).to eq(1.5)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'checking errors' do
|
|
27
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
28
|
+
|
|
29
|
+
it 'does not raise an error if an empty array is passed' do
|
|
30
|
+
expect(calculator.evaluate!('MIN(x)', x: [])).to eq(nil)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
20
33
|
end
|
data/spec/ast/mul_spec.rb
CHANGED
|
@@ -35,8 +35,9 @@ describe 'Dentaku::AST::Function::Mul' do
|
|
|
35
35
|
expect { calculator.evaluate!('MUL()') }.to raise_error(Dentaku::ArgumentError)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
it '
|
|
39
|
-
|
|
38
|
+
it 'does not raise an error if an empty array is passed' do
|
|
39
|
+
result = calculator.evaluate!('MUL(x)', x: [])
|
|
40
|
+
expect(result).to eq(1)
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
end
|
data/spec/ast/sum_spec.rb
CHANGED
|
@@ -35,8 +35,9 @@ describe 'Dentaku::AST::Function::Sum' do
|
|
|
35
35
|
expect { calculator.evaluate!('SUM()') }.to raise_error(Dentaku::ArgumentError)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
it '
|
|
39
|
-
|
|
38
|
+
it 'does not raise an error if an empty array is passed' do
|
|
39
|
+
result = calculator.evaluate!('SUM(x)', x: [])
|
|
40
|
+
expect(result).to eq(0)
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
end
|
data/spec/calculator_spec.rb
CHANGED
|
@@ -502,6 +502,10 @@ describe Dentaku::Calculator do
|
|
|
502
502
|
{"name" => "Bob", "age" => 44},
|
|
503
503
|
{"name" => "Jane", "age" => 27}
|
|
504
504
|
])).to eq(["Bob", "Jane"])
|
|
505
|
+
expect(calculator.evaluate!('map(users, u, IF(u.age < 30, u, null))', users: [
|
|
506
|
+
{"name" => "Bob", "age" => 44},
|
|
507
|
+
{"name" => "Jane", "age" => 27}
|
|
508
|
+
])).to eq([nil, { "name" => "Jane", "age" => 27 }])
|
|
505
509
|
end
|
|
506
510
|
end
|
|
507
511
|
|
|
@@ -696,9 +700,13 @@ describe Dentaku::Calculator do
|
|
|
696
700
|
it method do
|
|
697
701
|
if Math.method(method).arity == 2
|
|
698
702
|
expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq(Math.send(method, 1, 2))
|
|
703
|
+
expect(calculator.evaluate("#{method}(x,y) + 1", x: 1, y: '2')).to be_within(0.00001).of(Math.send(method, 1, 2) + 1)
|
|
699
704
|
expect { calculator.evaluate!("#{method}(x)", x: 1) }.to raise_error(Dentaku::ParseError)
|
|
700
705
|
else
|
|
701
706
|
expect(calculator.evaluate("#{method}(1)")).to eq(Math.send(method, 1))
|
|
707
|
+
unless [:atanh, :frexp, :lgamma].include?(method)
|
|
708
|
+
expect(calculator.evaluate("#{method}(1) + 1")).to be_within(0.00001).of(Math.send(method, 1) + 1)
|
|
709
|
+
end
|
|
702
710
|
end
|
|
703
711
|
end
|
|
704
712
|
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.4.
|
|
4
|
+
version: 3.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Solomon White
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-07-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: concurrent-ruby
|
|
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: codecov
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -162,6 +176,7 @@ files:
|
|
|
162
176
|
- lib/dentaku/ast/functions/avg.rb
|
|
163
177
|
- lib/dentaku/ast/functions/count.rb
|
|
164
178
|
- lib/dentaku/ast/functions/duration.rb
|
|
179
|
+
- lib/dentaku/ast/functions/filter.rb
|
|
165
180
|
- lib/dentaku/ast/functions/if.rb
|
|
166
181
|
- lib/dentaku/ast/functions/map.rb
|
|
167
182
|
- lib/dentaku/ast/functions/max.rb
|
|
@@ -210,7 +225,9 @@ files:
|
|
|
210
225
|
- spec/ast/comparator_spec.rb
|
|
211
226
|
- spec/ast/count_spec.rb
|
|
212
227
|
- spec/ast/division_spec.rb
|
|
228
|
+
- spec/ast/filter_spec.rb
|
|
213
229
|
- spec/ast/function_spec.rb
|
|
230
|
+
- spec/ast/map_spec.rb
|
|
214
231
|
- spec/ast/max_spec.rb
|
|
215
232
|
- spec/ast/min_spec.rb
|
|
216
233
|
- spec/ast/mul_spec.rb
|
|
@@ -269,7 +286,9 @@ test_files:
|
|
|
269
286
|
- spec/ast/comparator_spec.rb
|
|
270
287
|
- spec/ast/count_spec.rb
|
|
271
288
|
- spec/ast/division_spec.rb
|
|
289
|
+
- spec/ast/filter_spec.rb
|
|
272
290
|
- spec/ast/function_spec.rb
|
|
291
|
+
- spec/ast/map_spec.rb
|
|
273
292
|
- spec/ast/max_spec.rb
|
|
274
293
|
- spec/ast/min_spec.rb
|
|
275
294
|
- spec/ast/mul_spec.rb
|