dentaku 3.3.2 → 3.3.3

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: 1166f2fd23824ca3950d6db37cbd48d0198e939311b69541d37a832ce4106904
4
- data.tar.gz: 81452369184a17266465c58797ee4ff4643e7d47ab6762f4b38ac2a23cac6ad0
3
+ metadata.gz: 3028783062dbbe592ef71ed7bc388e57fb3af383e8cb897893dc858c4b772f63
4
+ data.tar.gz: c940b16dcfdc9e0d1658a6b38b7521e7a17882c9519a1771d4117600a614100a
5
5
  SHA512:
6
- metadata.gz: d576e283f2381a8bf270981e1451f0668fd321e34c8a47f868a578411c82d1567402fc8d7dd264feb32fc82c1b722b01a931b439854c826d229e140419cf2d9c
7
- data.tar.gz: bf601b8bdd70be48c02b801e05769bd8ce988552458d85512ef3445c39419f98d08e92915e6cc310cb772eb5d9c0b3db51c8dfec3dbe3665aa3533151110fbd8
6
+ metadata.gz: 7e9633152d5e43e11b52db3a91e8c012816c2cf956e5e3906ce2c058c44d5733fc2668d57049dbc3c44d07155c12bd3bcd8f416e4012986ddcd937bd46cae49c
7
+ data.tar.gz: bf89bf9096af4720832a7c904ce981a98beb69388996a1d29b190ce279fb8248eddc56175c5cc77646a36c65a0acc78dcd6c98fffb04ad0773cae22630eb3fdd
data/.rubocop.yml CHANGED
@@ -105,7 +105,7 @@ Layout/TrailingWhitespace:
105
105
  Enabled: true
106
106
 
107
107
  # Use quotes for string literals when they are enough.
108
- Style/UnneededPercentQ:
108
+ Style/RedundantPercentQ:
109
109
  Enabled: true
110
110
 
111
111
  # Align `end` with the matching keyword or starting expression except for
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Change Log
2
2
 
3
+ ## [Unreleased]
4
+ - date / duration addition and subtraction
5
+ - validate arity for custom functions with variable arity
6
+ - make AST serializable with Marshal.dump
7
+ - performance optimization for arithmetic node validation
8
+ - support lazy evaluation for expensive values
9
+ - short-circuit IF function
10
+ - better error when empty string is used in arithmetic operation
11
+
3
12
  ## [v3.3.2] 2019-06-10
4
13
  - add ability to pre-load AST cache
5
14
  - fix negation node bug
@@ -175,7 +184,7 @@
175
184
  ## [v0.1.0] 2012-01-20
176
185
  - initial release
177
186
 
178
- [HEAD]: https://github.com/rubysolo/dentaku/compare/v3.3.2...HEAD
187
+ [Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.3.2...HEAD
179
188
  [v3.3.2]: https://github.com/rubysolo/dentaku/compare/v3.3.1...v3.3.2
180
189
  [v3.3.1]: https://github.com/rubysolo/dentaku/compare/v3.3.0...v3.3.1
181
190
  [v3.3.0]: https://github.com/rubysolo/dentaku/compare/v3.2.1...v3.3.0
data/lib/dentaku/ast.rb CHANGED
@@ -18,6 +18,7 @@ require_relative './ast/function_registry'
18
18
  require_relative './ast/functions/and'
19
19
  require_relative './ast/functions/avg'
20
20
  require_relative './ast/functions/count'
21
+ require_relative './ast/functions/duration'
21
22
  require_relative './ast/functions/if'
22
23
  require_relative './ast/functions/max'
23
24
  require_relative './ast/functions/min'
@@ -5,6 +5,14 @@ module Dentaku
5
5
  2
6
6
  end
7
7
 
8
+ def self.min_param_count
9
+ arity
10
+ end
11
+
12
+ def self.max_param_count
13
+ arity
14
+ end
15
+
8
16
  def self.peek(*)
9
17
  end
10
18
 
@@ -22,6 +30,10 @@ module Dentaku
22
30
  def dependencies(context = {})
23
31
  @structure.dependencies(context) + @index.dependencies(context)
24
32
  end
33
+
34
+ def type
35
+ nil
36
+ end
25
37
  end
26
38
  end
27
39
  end
@@ -1,4 +1,5 @@
1
1
  require_relative './operation'
2
+ require_relative '../date_arithmetic'
2
3
  require 'bigdecimal'
3
4
  require 'bigdecimal/util'
4
5
 
@@ -12,6 +13,7 @@ module Dentaku
12
13
  raise NodeError.new(:numeric, left.type, :left),
13
14
  "#{self.class} requires numeric operands"
14
15
  end
16
+
15
17
  unless valid_right?
16
18
  raise NodeError.new(:numeric, right.type, :right),
17
19
  "#{self.class} requires numeric operands"
@@ -50,15 +52,15 @@ module Dentaku
50
52
  end
51
53
 
52
54
  def valid_node?(node)
53
- node && (node.dependencies.any? || node.type == :numeric)
55
+ node && (node.type == :numeric || node.dependencies.any?)
54
56
  end
55
57
 
56
58
  def valid_left?
57
- valid_node?(left)
59
+ valid_node?(left) || left.type == :datetime
58
60
  end
59
61
 
60
62
  def valid_right?
61
- valid_node?(right)
63
+ valid_node?(right) || right.type == :duration
62
64
  end
63
65
 
64
66
  def validate_value(val)
@@ -77,7 +79,7 @@ module Dentaku
77
79
  end
78
80
 
79
81
  def validate_format(string)
80
- unless string =~ /\A-?\d*(\.\d+)?\z/
82
+ unless string =~ /\A-?\d*(\.\d+)?\z/ && !string.empty?
81
83
  raise Dentaku::ArgumentError.for(:invalid_value, value: string, for: BigDecimal),
82
84
  "String input '#{string}' is not coercible to numeric"
83
85
  end
@@ -92,6 +94,14 @@ module Dentaku
92
94
  def self.precedence
93
95
  10
94
96
  end
97
+
98
+ def value(context = {})
99
+ if left.type == :datetime
100
+ Dentaku::DateArithmetic.new(left.value(context)).add(right.value(context))
101
+ else
102
+ super
103
+ end
104
+ end
95
105
  end
96
106
 
97
107
  class Subtraction < Arithmetic
@@ -102,6 +112,14 @@ module Dentaku
102
112
  def self.precedence
103
113
  10
104
114
  end
115
+
116
+ def value(context = {})
117
+ if left.type == :datetime
118
+ Dentaku::DateArithmetic.new(left.value(context)).sub(right.value(context))
119
+ else
120
+ super
121
+ end
122
+ end
105
123
  end
106
124
 
107
125
  class Multiplication < Arithmetic
@@ -4,6 +4,14 @@ module Dentaku
4
4
  def self.arity
5
5
  end
6
6
 
7
+ def self.min_param_count
8
+ 0
9
+ end
10
+
11
+ def self.max_param_count
12
+ Float::INFINITY
13
+ end
14
+
7
15
  def self.peek(*)
8
16
  end
9
17
 
@@ -18,6 +26,10 @@ module Dentaku
18
26
  def dependencies(context = {})
19
27
  @elements.flat_map { |el| el.dependencies(context) }
20
28
  end
29
+
30
+ def type
31
+ nil
32
+ end
21
33
  end
22
34
  end
23
35
  end
@@ -8,6 +8,14 @@ require 'dentaku/exceptions'
8
8
  module Dentaku
9
9
  module AST
10
10
  class Case < Node
11
+ def self.min_param_count
12
+ 2
13
+ end
14
+
15
+ def self.max_param_count
16
+ Float::INFINITY
17
+ end
18
+
11
19
  def initialize(*nodes)
12
20
  @switch = nodes.shift
13
21
 
@@ -6,6 +6,14 @@ module Dentaku
6
6
  attr_reader :when,
7
7
  :then
8
8
 
9
+ def self.min_param_count
10
+ 2
11
+ end
12
+
13
+ def self.max_param_count
14
+ 2
15
+ end
16
+
9
17
  def initialize(when_statement, then_statement)
10
18
  @when = when_statement
11
19
  unless @when.is_a?(AST::CaseWhen)
@@ -38,6 +38,14 @@ module Dentaku
38
38
  @implementation.arity < 0 ? nil : @implementation.arity
39
39
  end
40
40
 
41
+ def self.min_param_count
42
+ @implementation.parameters.select { |type, _name| type == :req }.count
43
+ end
44
+
45
+ def self.max_param_count
46
+ @implementation.parameters.select { |type, _name| type == :rest }.any? ? Float::INFINITY : @implementation.parameters.count
47
+ end
48
+
41
49
  def value(context = {})
42
50
  args = @args.map { |a| a.value(context) }
43
51
  self.class.implementation.call(*args)
@@ -48,6 +56,8 @@ module Dentaku
48
56
  end
49
57
  end
50
58
 
59
+ define_class(name, function)
60
+
51
61
  function.name = name
52
62
  function.type = type
53
63
  function.implementation = implementation
@@ -72,6 +82,17 @@ module Dentaku
72
82
  def function_name(name)
73
83
  name.to_s.downcase
74
84
  end
85
+
86
+ def normalize_name(function_name)
87
+ function_name.to_s.capitalize.gsub(/\W/, '_')
88
+ end
89
+
90
+ def define_class(function_name, function)
91
+ class_name = normalize_name(function_name)
92
+ return if Dentaku::AST::Function.const_defined?(class_name)
93
+
94
+ Dentaku::AST::Function.const_set(class_name, function)
95
+ end
75
96
  end
76
97
  end
77
98
  end
@@ -3,6 +3,14 @@ require_relative '../function'
3
3
  module Dentaku
4
4
  module AST
5
5
  class Count < Function
6
+ def self.min_param_count
7
+ 0
8
+ end
9
+
10
+ def self.max_param_count
11
+ Float::INFINITY
12
+ end
13
+
6
14
  def value(context = {})
7
15
  if @args.length == 1
8
16
  first_arg = @args[0].value(context)
@@ -0,0 +1,51 @@
1
+ require_relative '../function'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Duration < Function
6
+ def self.min_param_count
7
+ 1
8
+ end
9
+
10
+ def self.max_param_count
11
+ 1
12
+ end
13
+
14
+ class Value
15
+ attr_reader :value, :unit
16
+
17
+ def initialize(value, unit)
18
+ @value = value
19
+ @unit = validate_unit(unit)
20
+ end
21
+
22
+ def validate_unit(unit)
23
+ case unit.downcase
24
+ when /years?/ then :year
25
+ when /months?/ then :month
26
+ when /days?/ then :day
27
+ else
28
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: unit, for: Duration),
29
+ "'#{unit || unit.class}' is not a valid duration unit"
30
+ end
31
+ end
32
+ end
33
+
34
+ def type
35
+ :duration
36
+ end
37
+
38
+ def value(context = {})
39
+ value_node, unit_node = *@args
40
+ Value.new(value_node.value(context), unit_node.identifier)
41
+ end
42
+
43
+ def dependencies(context = {})
44
+ value_node = @args.first
45
+ value_node.dependencies(context)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ Dentaku::AST::Function.register_class(:duration, Dentaku::AST::Duration)
@@ -5,6 +5,14 @@ module Dentaku
5
5
  class If < Function
6
6
  attr_reader :predicate, :left, :right
7
7
 
8
+ def self.min_param_count
9
+ 3
10
+ end
11
+
12
+ def self.max_param_count
13
+ 3
14
+ end
15
+
8
16
  def initialize(predicate, left, right)
9
17
  @predicate = predicate
10
18
  @left = left
@@ -24,8 +32,13 @@ module Dentaku
24
32
  end
25
33
 
26
34
  def dependencies(context = {})
27
- # TODO : short-circuit?
28
- (predicate.dependencies(context) + left.dependencies(context) + right.dependencies(context)).uniq
35
+ deps = predicate.dependencies(context)
36
+
37
+ if deps.empty?
38
+ predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
39
+ else
40
+ (deps + left.dependencies(context) + right.dependencies(context)).uniq
41
+ end
29
42
  end
30
43
  end
31
44
  end
@@ -17,6 +17,14 @@ module Dentaku
17
17
  end
18
18
 
19
19
  class Left < Base
20
+ def self.min_param_count
21
+ 2
22
+ end
23
+
24
+ def self.max_param_count
25
+ 2
26
+ end
27
+
20
28
  def initialize(*args)
21
29
  super
22
30
  @string, @length = *@args
@@ -111,6 +119,14 @@ module Dentaku
111
119
  end
112
120
 
113
121
  class Concat < Base
122
+ def self.min_param_count
123
+ 1
124
+ end
125
+
126
+ def self.max_param_count
127
+ Float::INFINITY
128
+ end
129
+
114
130
  def initialize(*args)
115
131
  super
116
132
  end
@@ -21,6 +21,8 @@ module Dentaku
21
21
  case v
22
22
  when Node
23
23
  v.value(context)
24
+ when Proc
25
+ v.call
24
26
  else
25
27
  v
26
28
  end
@@ -15,6 +15,10 @@ module Dentaku
15
15
  def dependencies(context = {})
16
16
  []
17
17
  end
18
+
19
+ def type
20
+ nil
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -5,6 +5,14 @@ module Dentaku
5
5
  class Operation < Node
6
6
  attr_reader :left, :right
7
7
 
8
+ def self.min_param_count
9
+ arity
10
+ end
11
+
12
+ def self.max_param_count
13
+ arity
14
+ end
15
+
8
16
  def initialize(left, right)
9
17
  @left = left
10
18
  @right = right
@@ -0,0 +1,45 @@
1
+ module Dentaku
2
+ class DateArithmetic
3
+ def initialize(date)
4
+ @base = date
5
+ end
6
+
7
+ def add(duration)
8
+ case duration
9
+ when Numeric
10
+ @base + duration
11
+ when Dentaku::AST::Duration::Value
12
+ case duration.unit
13
+ when :year
14
+ Time.local(@base.year + duration.value, @base.month, @base.day).to_datetime
15
+ when :month
16
+ @base >> duration.value
17
+ when :day
18
+ @base + duration.value
19
+ end
20
+ else
21
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
22
+ "'#{duration || duration.class}' is not coercible for date arithmetic"
23
+ end
24
+ end
25
+
26
+ def sub(duration)
27
+ case duration
28
+ when Numeric
29
+ @base - duration
30
+ when Dentaku::AST::Duration::Value
31
+ case duration.unit
32
+ when :year
33
+ Time.local(@base.year - duration.value, @base.month, @base.day).to_datetime
34
+ when :month
35
+ @base << duration.value
36
+ when :day
37
+ @base - duration.value
38
+ end
39
+ else
40
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
41
+ "'#{duration || duration.class}' is not coercible for date arithmetic"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -33,7 +33,7 @@ module Dentaku
33
33
  private_class_method :new
34
34
 
35
35
  VALID_REASONS = %i[
36
- node_invalid too_few_operands undefined_function
36
+ node_invalid too_few_operands too_many_operands undefined_function
37
37
  unprocessed_token unknown_case_token unbalanced_bracket
38
38
  unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
39
39
  invalid_statement
@@ -40,9 +40,17 @@ module Dentaku
40
40
  operator.peek(output)
41
41
 
42
42
  args_size = operator.arity || count
43
- if args_size > output.length
44
- fail! :too_few_operands, operator: operator, expect: args_size, actual: output.length
43
+ min_size = operator.arity || operator.min_param_count || count
44
+ max_size = operator.arity || operator.max_param_count || count
45
+
46
+ if output.length < min_size
47
+ fail! :too_few_operands, operator: operator, expect: min_size, actual: output.length
48
+ end
49
+
50
+ if output.length > max_size && operations.empty?
51
+ fail! :too_many_operands, operator: operator, expect: max_size, actual: output.length
45
52
  end
53
+
46
54
  args = Array.new(args_size) { output.pop }.reverse
47
55
 
48
56
  output.push operator.new(*args)
@@ -305,6 +313,8 @@ module Dentaku
305
313
  "#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
306
314
  when :too_few_operands
307
315
  "#{meta.fetch(:operator)} has too few operands"
316
+ when :too_many_operands
317
+ "#{meta.fetch(:operator)} has too many operands"
308
318
  when :undefined_function
309
319
  "Undefined function #{meta.fetch(:function_name)}"
310
320
  when :unprocessed_token
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.3.2"
2
+ VERSION = "3.3.3"
3
3
  end
@@ -38,25 +38,33 @@ describe Dentaku::AST::Arithmetic do
38
38
  expect(neg(x)).to eq(-1)
39
39
  end
40
40
 
41
+ it 'correctly parses string operands to numeric values' do
42
+ expect(add(x, one, 'x' => '1')).to eq(2)
43
+ expect(add(x, one, 'x' => '1.1')).to eq(2.1)
44
+ expect(add(x, one, 'x' => '.1')).to eq(1.1)
45
+ expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
46
+ expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
47
+ end
48
+
41
49
  private
42
50
 
43
- def add(left, right)
44
- Dentaku::AST::Addition.new(left, right).value(ctx)
51
+ def add(left, right, context = ctx)
52
+ Dentaku::AST::Addition.new(left, right).value(context)
45
53
  end
46
54
 
47
- def sub(left, right)
48
- Dentaku::AST::Subtraction.new(left, right).value(ctx)
55
+ def sub(left, right, context = ctx)
56
+ Dentaku::AST::Subtraction.new(left, right).value(context)
49
57
  end
50
58
 
51
- def mul(left, right)
52
- Dentaku::AST::Multiplication.new(left, right).value(ctx)
59
+ def mul(left, right, context = ctx)
60
+ Dentaku::AST::Multiplication.new(left, right).value(context)
53
61
  end
54
62
 
55
- def div(left, right)
56
- Dentaku::AST::Division.new(left, right).value(ctx)
63
+ def div(left, right, context = ctx)
64
+ Dentaku::AST::Division.new(left, right).value(context)
57
65
  end
58
66
 
59
- def neg(node)
60
- Dentaku::AST::Negation.new(node).value(ctx)
67
+ def neg(node, context = ctx)
68
+ Dentaku::AST::Negation.new(node).value(context)
61
69
  end
62
70
  end
@@ -22,7 +22,7 @@ describe Dentaku::AST::Function do
22
22
  end
23
23
 
24
24
  describe "#arity" do
25
- it "gives the correct arity for custom functions" do
25
+ it "returns the correct arity for custom functions" do
26
26
  zero = described_class.register("zero", :numeric, ->() { 0 })
27
27
  expect(zero.arity).to eq(0)
28
28
 
@@ -21,7 +21,10 @@ describe Dentaku::AST::Node do
21
21
  expect(node.dependencies).to eq(['x', 'y', 'z'])
22
22
 
23
23
  node = make_node('if(x > 5, y, z)')
24
- expect(node.dependencies('x' => 7)).to eq(['y', 'z'])
24
+ expect(node.dependencies('x' => 7)).to eq(['y'])
25
+
26
+ node = make_node('if(x > 5, y, z)')
27
+ expect(node.dependencies('x' => 2)).to eq(['z'])
25
28
 
26
29
  node = make_node('')
27
30
  expect(node.dependencies).to eq([])
@@ -74,6 +74,11 @@ describe Dentaku::Calculator do
74
74
  expect(calculator.evaluate('NOT(a)', a: nil, b: nil)).to be_truthy
75
75
  expect(calculator.evaluate('OR(a,b)', a: nil, b: nil)).to be_falsy
76
76
  end
77
+
78
+ it 'supports lazy evaluation of variables' do
79
+ expect(calculator.evaluate('x + 1', x: -> { 1 })).to eq(2)
80
+ expect { calculator.evaluate('2', x: -> { raise 'boom' }) }.not_to raise_error
81
+ end
77
82
  end
78
83
 
79
84
  describe 'evaluate!' do
@@ -105,7 +110,7 @@ describe Dentaku::Calculator do
105
110
  end
106
111
 
107
112
  it 'raises argument error if a function is called with incorrect arity' do
108
- expect { calculator.evaluate!('IF(a,b)', a: 1, b: 1) }.to raise_error(Dentaku::ArgumentError)
113
+ expect { calculator.evaluate!('IF(a,b)', a: 1, b: 1) }.to raise_error(Dentaku::ParseError)
109
114
  end
110
115
  end
111
116
 
@@ -369,20 +374,34 @@ describe Dentaku::Calculator do
369
374
  expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: false)).to be_falsey
370
375
  end
371
376
 
372
- it 'compares Time variables' do
377
+ it 'compares time variables' do
373
378
  expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_truthy
374
379
  expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_falsy
375
380
  expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_falsy
376
381
  expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_truthy
377
382
  end
378
383
 
379
- it 'compares Time literals with Time variables' do
384
+ it 'compares time literals with time variables' do
380
385
  expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_truthy
381
386
  expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_falsy
382
387
  expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_falsy
383
388
  expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_truthy
384
389
  end
385
390
 
391
+ it 'supports date arithmetic' do
392
+ expect(calculator.evaluate!('2020-01-01 + 30').to_date).to eq(Time.local(2020, 1, 31).to_date)
393
+ expect(calculator.evaluate!('2020-01-01 - 1').to_date).to eq(Time.local(2019, 12, 31).to_date)
394
+ expect(calculator.evaluate!('2020-01-01 + duration(1, day)').to_date).to eq(Time.local(2020, 1, 2).to_date)
395
+ expect(calculator.evaluate!('2020-01-01 - duration(1, day)').to_date).to eq(Time.local(2019, 12, 31).to_date)
396
+ expect(calculator.evaluate!('2020-01-01 + duration(30, days)').to_date).to eq(Time.local(2020, 1, 31).to_date)
397
+ expect(calculator.evaluate!('2020-01-01 + duration(1, month)').to_date).to eq(Time.local(2020, 2, 1).to_date)
398
+ expect(calculator.evaluate!('2020-01-01 - duration(1, month)').to_date).to eq(Time.local(2019, 12, 1).to_date)
399
+ expect(calculator.evaluate!('2020-01-01 + duration(30, months)').to_date).to eq(Time.local(2022, 7, 1).to_date)
400
+ expect(calculator.evaluate!('2020-01-01 + duration(1, year)').to_date).to eq(Time.local(2021, 1, 1).to_date)
401
+ expect(calculator.evaluate!('2020-01-01 - duration(1, year)').to_date).to eq(Time.local(2019, 1, 1).to_date)
402
+ expect(calculator.evaluate!('2020-01-01 + duration(30, years)').to_date).to eq(Time.local(2050, 1, 1).to_date)
403
+ end
404
+
386
405
  describe 'functions' do
387
406
  it 'include IF' do
388
407
  expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 2)).to eq(10)
data/spec/dentaku_spec.rb CHANGED
@@ -36,4 +36,11 @@ describe Dentaku do
36
36
  Dentaku.aliases = { roundup: ['roundupup'] }
37
37
  expect(Dentaku.evaluate('roundupup(6.1)')).to eq(7)
38
38
  end
39
+
40
+ it 'sets caching opt-in flags' do
41
+ expect {
42
+ Dentaku.enable_caching!
43
+ }.to change { Dentaku.cache_ast? }.from(false).to(true)
44
+ .and change { Dentaku.cache_dependency_order? }.from(false).to(true)
45
+ end
39
46
  end
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'dentaku'
2
3
  require 'dentaku/calculator'
3
4
 
4
5
  describe Dentaku::Calculator do
@@ -14,6 +15,7 @@ describe Dentaku::Calculator do
14
15
  [:pow, :numeric, ->(mantissa, exponent) { mantissa**exponent }],
15
16
  [:biggest, :numeric, ->(*args) { args.max }],
16
17
  [:smallest, :numeric, ->(*args) { args.min }],
18
+ [:optional, :numeric, ->(x, y, z = 0) { x + y + z }],
17
19
  ]
18
20
 
19
21
  c.add_functions(fns)
@@ -39,6 +41,13 @@ describe Dentaku::Calculator do
39
41
  expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
40
42
  end
41
43
 
44
+ it 'includes OPTIONAL' do
45
+ expect(with_external_funcs.evaluate('OPTIONAL(1,2)')).to eq(3)
46
+ expect(with_external_funcs.evaluate('OPTIONAL(1,2,3)')).to eq(6)
47
+ expect { with_external_funcs.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
48
+ expect { with_external_funcs.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
49
+ end
50
+
42
51
  it 'supports array parameters' do
43
52
  calculator = described_class.new
44
53
  calculator.add_function(
@@ -59,6 +68,19 @@ describe Dentaku::Calculator do
59
68
  expect(calculator.evaluate("hey!()")).to eq("hey!")
60
69
  end
61
70
 
71
+ it 'defines for a given function a properly named class that represents it to support AST marshaling' do
72
+ calculator = described_class.new
73
+ expect {
74
+ calculator.add_function(:ho, :string, -> {})
75
+ }.to change {
76
+ Dentaku::AST::Function.const_defined?("Ho")
77
+ }.from(false).to(true)
78
+
79
+ expect {
80
+ Marshal.dump(calculator.ast('MAX(1, 2)'))
81
+ }.not_to raise_error
82
+ end
83
+
62
84
  it 'does not store functions across all calculators' do
63
85
  calculator1 = Dentaku::Calculator.new
64
86
  calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
@@ -66,17 +88,19 @@ describe Dentaku::Calculator do
66
88
  calculator2 = Dentaku::Calculator.new
67
89
  calculator2.add_function(:my_function, :numeric, ->(x) { 4 * x + 3 })
68
90
 
69
- expect(calculator1.evaluate("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
70
- expect(calculator2.evaluate("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
91
+ expect(calculator1.evaluate!("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
92
+ expect(calculator2.evaluate!("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
71
93
 
72
94
  expect {
73
95
  Dentaku::Calculator.new.evaluate!("1 + my_function(2)")
74
96
  }.to raise_error(Dentaku::ParseError)
75
97
  end
76
98
 
77
- it 'self.add_function adds to default/global function registry' do
78
- Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
79
- expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
99
+ describe 'Dentaku::Calculator.add_function' do
100
+ it 'adds to default/global function registry' do
101
+ Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
102
+ expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
103
+ end
80
104
  end
81
105
  end
82
106
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dentaku
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.2
4
+ version: 3.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-11 00:00:00.000000000 Z
11
+ date: 2019-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: codecov
@@ -159,6 +159,7 @@ files:
159
159
  - lib/dentaku/ast/functions/and.rb
160
160
  - lib/dentaku/ast/functions/avg.rb
161
161
  - lib/dentaku/ast/functions/count.rb
162
+ - lib/dentaku/ast/functions/duration.rb
162
163
  - lib/dentaku/ast/functions/if.rb
163
164
  - lib/dentaku/ast/functions/max.rb
164
165
  - lib/dentaku/ast/functions/min.rb
@@ -184,6 +185,7 @@ files:
184
185
  - lib/dentaku/ast/string.rb
185
186
  - lib/dentaku/bulk_expression_solver.rb
186
187
  - lib/dentaku/calculator.rb
188
+ - lib/dentaku/date_arithmetic.rb
187
189
  - lib/dentaku/dependency_resolver.rb
188
190
  - lib/dentaku/exceptions.rb
189
191
  - lib/dentaku/flat_hash.rb
@@ -249,8 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
249
251
  - !ruby/object:Gem::Version
250
252
  version: '0'
251
253
  requirements: []
252
- rubyforge_project: dentaku
253
- rubygems_version: 2.7.6
254
+ rubygems_version: 3.0.3
254
255
  signing_key:
255
256
  specification_version: 4
256
257
  summary: A formula language parser and evaluator