dentaku 3.3.4 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
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