dentaku 3.3.2 → 3.3.3

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