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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -7
  3. data/.travis.yml +3 -4
  4. data/CHANGELOG.md +13 -0
  5. data/dentaku.gemspec +0 -2
  6. data/lib/dentaku.rb +14 -4
  7. data/lib/dentaku/ast.rb +4 -0
  8. data/lib/dentaku/ast/access.rb +3 -1
  9. data/lib/dentaku/ast/arithmetic.rb +7 -2
  10. data/lib/dentaku/ast/array.rb +3 -1
  11. data/lib/dentaku/ast/function.rb +10 -1
  12. data/lib/dentaku/ast/functions/all.rb +36 -0
  13. data/lib/dentaku/ast/functions/any.rb +36 -0
  14. data/lib/dentaku/ast/functions/avg.rb +2 -2
  15. data/lib/dentaku/ast/functions/map.rb +36 -0
  16. data/lib/dentaku/ast/functions/mul.rb +3 -2
  17. data/lib/dentaku/ast/functions/pluck.rb +29 -0
  18. data/lib/dentaku/ast/functions/round.rb +1 -1
  19. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  20. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  21. data/lib/dentaku/ast/functions/ruby_math.rb +47 -3
  22. data/lib/dentaku/ast/functions/string_functions.rb +4 -4
  23. data/lib/dentaku/ast/functions/sum.rb +3 -2
  24. data/lib/dentaku/ast/grouping.rb +3 -1
  25. data/lib/dentaku/ast/identifier.rb +3 -1
  26. data/lib/dentaku/bulk_expression_solver.rb +34 -25
  27. data/lib/dentaku/calculator.rb +13 -5
  28. data/lib/dentaku/date_arithmetic.rb +1 -1
  29. data/lib/dentaku/exceptions.rb +3 -3
  30. data/lib/dentaku/flat_hash.rb +7 -0
  31. data/lib/dentaku/parser.rb +2 -1
  32. data/lib/dentaku/tokenizer.rb +1 -1
  33. data/lib/dentaku/version.rb +1 -1
  34. data/spec/ast/arithmetic_spec.rb +19 -5
  35. data/spec/ast/avg_spec.rb +4 -0
  36. data/spec/ast/mul_spec.rb +4 -0
  37. data/spec/ast/negation_spec.rb +18 -2
  38. data/spec/ast/round_spec.rb +10 -0
  39. data/spec/ast/rounddown_spec.rb +10 -0
  40. data/spec/ast/roundup_spec.rb +10 -0
  41. data/spec/ast/string_functions_spec.rb +35 -0
  42. data/spec/ast/sum_spec.rb +4 -0
  43. data/spec/bulk_expression_solver_spec.rb +17 -0
  44. data/spec/calculator_spec.rb +112 -0
  45. data/spec/dentaku_spec.rb +14 -8
  46. data/spec/parser_spec.rb +13 -0
  47. data/spec/tokenizer_spec.rb +24 -5
  48. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59795943c781e4fff4c3ea5a33a0035e6c9ac2cf3a4671ac9105201301601b3e
4
- data.tar.gz: cfc61c4b4e522b54754dd6c960a82bc0cddcedebe160ed111d8d68e4334b5752
3
+ metadata.gz: 1eb86933b899627333d93e993cd2d093a11f4ca54b2e0f6668c3580a8f133975
4
+ data.tar.gz: 004bd53fa5c3c6815bafdbf98788de96757b688a6ba217b7f8e96be31a4f7d5d
5
5
  SHA512:
6
- metadata.gz: c3d96b80d7a90ecbf946311743f92ce84e04d77bbb7e0c6ac44724e0f0b479b3ce27d19d6b191381c46c27b12bd352dd8881d786500c5c666eef923126c7e577
7
- data.tar.gz: 93a8ab932fda06e5121c98c39a8561b728da48fef9501127a218776c18d18cfd4086b5d3db938e6f8bc1bbdad59e8c3ba258b84e4d22c0ffcdd39d0d8aa20d54
6
+ metadata.gz: 32f0e44638fc3738a3df5ba6a04abdb4ee66177049242260e26fea1ad6844a66a35e65ca5a27448f118838e6ccecd133f46650e9fbff5d878bbc14b1e7d952e8
7
+ data.tar.gz: 8081bcf51e30daa33dbaba3f8796951151c1bfd2e3975f63e5291aac7e04e09f2fc2f00aa956cfdad99caf1b8a8fb4f04b2c0789d8909b0982372e75dc7f7fc0
@@ -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/Tab:
91
+ Layout/IndentationStyle:
97
92
  Enabled: true
98
93
 
99
94
  # Blank lines should not have any spaces.
100
- Layout/TrailingBlankLines:
95
+ Layout/TrailingEmptyLines:
101
96
  Enabled: true
102
97
 
103
98
  # No trailing whitespace.
@@ -1,10 +1,9 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - 2.3.8
5
- - 2.4.4
6
- - 2.5.3
7
- - 2.6.0
4
+ - 2.5.7
5
+ - 2.6.5
6
+ - 2.7.0
8
7
  before_install:
9
8
  - gem update bundler
10
9
  - gem update --system
@@ -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
@@ -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')
@@ -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
@@ -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'
@@ -1,6 +1,8 @@
1
+ require_relative "./node"
2
+
1
3
  module Dentaku
2
4
  module AST
3
- class Access
5
+ class Access < Node
4
6
  def self.arity
5
7
  2
6
8
  end
@@ -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
- l.public_send(operator, r)
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)
@@ -1,6 +1,8 @@
1
+ require_relative "./node"
2
+
1
3
  module Dentaku
2
4
  module AST
3
- class Array
5
+ class Array < Node
4
6
  def self.arity
5
7
  end
6
8
 
@@ -12,7 +12,16 @@ module Dentaku
12
12
  end
13
13
 
14
14
  def dependencies(context = {})
15
- @args.flat_map { |a| a.dependencies(context) }
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
- if args.empty?
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
- if args.empty?
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
- args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
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.register(method, :numeric, lambda { |*args|
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