dentaku 3.4.1 → 3.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0275b981a44fdb17f4eb1ec144c57baf3f5ab46f0bf65c6f98dda51b3bc80658
4
- data.tar.gz: b5120974bd987a35e5388a5932e0a01d9a0d05692df9d0281d65ad729484754c
3
+ metadata.gz: 3dadbc209e58b2d2206dfaf48b83be3224db37f71d60cf590aadb63be9f4bebd
4
+ data.tar.gz: 86530c9c3333139994775a6bde16202005d89161a0b98670db58bcdda47f8e55
5
5
  SHA512:
6
- metadata.gz: 76f427ebaed43836b95201f5939c90d3391b9887a7bfbbc3fc51973a7fdf7c25453be641fba639480e095284af26391437b5711731625e211ccf55cd616967c2
7
- data.tar.gz: 6934989420f0c0d86914bcd10c2f562c138e883b3bb4b0d33013553f2403d45064b472932d069d79cb69c82c24a672668b50cc46fbca7740e2bf71982913635c
6
+ metadata.gz: f132401168218a3c021124ddabb56bbf485550bdff60a03a291ad811d8628e25caab2ba36e4bed4f8148bde048e56bed1fa31480774f2c19617a79282da81280
7
+ data.tar.gz: 3b6a8fc5e40988a9442be9dbdf890dc813e13b95fc2d0cb2f60252b2492d07fdf4ba47cdfedd0a85c002aaaa23a83bffb141b8f8f31fbf518678439d55c1e795
data/.travis.yml CHANGED
@@ -1,9 +1,10 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - 2.5.7
5
- - 2.6.5
6
- - 2.7.0
4
+ - 2.5.9
5
+ - 2.6.7
6
+ - 2.7.3
7
+ - 3.0.1
7
8
  before_install:
8
9
  - gem update bundler
9
10
  - gem update --system
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
- [HEAD]: https://github.com/rubysolo/dentaku/compare/v3.4.0...v3.4.1
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.update(
27
- FlatHash.from_hash(item_identifier => item_value)
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.update(
27
- FlatHash.from_hash(item_identifier => item_value)
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)
@@ -23,8 +23,8 @@ module Dentaku
23
23
 
24
24
  collection.map do |item_value|
25
25
  expression.value(
26
- context.update(
27
- FlatHash.from_hash(item_identifier => item_value)
26
+ context.merge(
27
+ FlatHash.from_hash_with_intermediates(item_identifier => item_value)
28
28
  )
29
29
  )
30
30
  end
@@ -1,13 +1,12 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:mul, :numeric, ->(*args) {
4
- flatten_args = args.flatten
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
- flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
11
+ args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
13
12
  })
@@ -40,8 +40,10 @@ module Dentaku
40
40
  self.class.call(*args)
41
41
  end
42
42
 
43
+ ARRAY_RETURN_TYPES = [:frexp, :lgamma].freeze
44
+
43
45
  def type
44
- nil
46
+ ARRAY_RETURN_TYPES.include?(@name) ? :array : :numeric
45
47
  end
46
48
  end
47
49
  end
@@ -1,13 +1,12 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:sum, :numeric, ->(*args) {
4
- flatten_args = args.flatten
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
- flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+)
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
@@ -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 - memory.keys
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(', ')}"
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.4.1"
2
+ VERSION = "3.4.2"
3
3
  end
@@ -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 'raises an error if an empty array is passed' do
39
- expect { calculator.evaluate!('MUL(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
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 'raises an error if an empty array is passed' do
39
- expect { calculator.evaluate!('SUM(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
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
@@ -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.1
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: 2020-12-12 00:00:00.000000000 Z
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