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 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