dentaku 3.2.0 → 3.2.1

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
- SHA1:
3
- metadata.gz: 402c5a79a4d28d7323c53de8852007fa1c49c087
4
- data.tar.gz: e5bd251b569708d740a7aeedde619e3bbfa34037
2
+ SHA256:
3
+ metadata.gz: bc147a87584da15958dfa99d62dcbc680333b21d2f9a35758ab11b4d7327787b
4
+ data.tar.gz: a3138608c305adc7ff88168e5379bc5f7da7fa0712b60d9f5d4afb8c63bfa2c3
5
5
  SHA512:
6
- metadata.gz: a7dda0285c345400228dd7ec606958a95b9f57bd54f87a4333b4533d8f671b9ac66485ec8d29f66360b0dc026b00bb57ca842f4cf07e763c5840a2d84fa5a89e
7
- data.tar.gz: d58d8f1451b0092feb6a8038f0d1efe8b6957c5de0a67badf53538395963098912853ea848c5fab03581ffeeddf89e4bb8812a735ec8ba6be48a76db35760e97
6
+ metadata.gz: 24da7dff044e4909cc1a6f900fb91c1aa30064e940ddf57e1c68112b0d61331b66fa066173012daeb302f757d10ea7ccec1cc505c95d6a8f32c019b2d7d219ff
7
+ data.tar.gz: 5745ef2e7896a3e0570d236e0b04bdd627659b15da2234855f81adde950c18c3b04d73eaeb34ae836756bfeb37f54bff9886d8730912a32c9e2700161807ac10
@@ -1,6 +1,9 @@
1
1
  # Change Log
2
2
 
3
- ## [v3.2.0] Unreleased
3
+ ## [v3.2.1] 2018-10-24
4
+ - make `evaluate` rescue more exceptions
5
+
6
+ ## [v3.2.0] 2018-03-14
4
7
  - add `COUNT` and `AVG` functions
5
8
  - add unicode support 😎
6
9
  - fix CASE parsing bug
@@ -8,7 +11,7 @@
8
11
  - add variadic MUL function
9
12
  - performance optimization
10
13
 
11
- ## [v3.1.0] 2017-01-10
14
+ ## [v3.1.0] 2018-01-10
12
15
  - allow decimals with no leading zero
13
16
  - nested hash and array support in bulk expression solver
14
17
  - add a variadic SUM function
@@ -159,6 +162,8 @@
159
162
  ## [v0.1.0] 2012-01-20
160
163
  - initial release
161
164
 
165
+ [HEAD]: https://github.com/rubysolo/dentaku/compare/v3.2.1...HEAD
166
+ [v3.2.1]: https://github.com/rubysolo/dentaku/compare/v3.2.0...v3.2.1
162
167
  [v3.2.0]: https://github.com/rubysolo/dentaku/compare/v3.1.0...v3.2.0
163
168
  [v3.1.0]: https://github.com/rubysolo/dentaku/compare/v3.0.0...v3.1.0
164
169
  [v3.0.0]: https://github.com/rubysolo/dentaku/compare/v2.0.11...v3.0.0
@@ -54,3 +54,7 @@ end
54
54
  def Dentaku(expression, data = {})
55
55
  Dentaku.evaluate(expression, data)
56
56
  end
57
+
58
+ def Dentaku!(expression, data = {})
59
+ Dentaku.evaluate!(expression, data)
60
+ end
@@ -159,6 +159,14 @@ module Dentaku
159
159
  end
160
160
  end
161
161
 
162
+ def dependencies(context = {})
163
+ if percent?
164
+ @right.dependencies(context)
165
+ else
166
+ super
167
+ end
168
+ end
169
+
162
170
  def percent?
163
171
  left.nil?
164
172
  end
@@ -14,11 +14,21 @@ module Dentaku
14
14
  def operator
15
15
  raise NotImplementedError
16
16
  end
17
+
18
+ private
19
+
20
+ def value
21
+ yield
22
+ rescue ::ArgumentError => argument_error
23
+ raise Dentaku::ArgumentError, argument_error.message
24
+ rescue NoMethodError => no_method_error
25
+ raise Dentaku::Error, no_method_error.message
26
+ end
17
27
  end
18
28
 
19
29
  class LessThan < Comparator
20
30
  def value(context = {})
21
- left.value(context) < right.value(context)
31
+ super() { left.value(context) < right.value(context) }
22
32
  end
23
33
 
24
34
  def operator
@@ -28,7 +38,7 @@ module Dentaku
28
38
 
29
39
  class LessThanOrEqual < Comparator
30
40
  def value(context = {})
31
- left.value(context) <= right.value(context)
41
+ super() { left.value(context) <= right.value(context) }
32
42
  end
33
43
 
34
44
  def operator
@@ -38,7 +48,7 @@ module Dentaku
38
48
 
39
49
  class GreaterThan < Comparator
40
50
  def value(context = {})
41
- left.value(context) > right.value(context)
51
+ super() { left.value(context) > right.value(context) }
42
52
  end
43
53
 
44
54
  def operator
@@ -48,7 +58,7 @@ module Dentaku
48
58
 
49
59
  class GreaterThanOrEqual < Comparator
50
60
  def value(context = {})
51
- left.value(context) >= right.value(context)
61
+ super() { left.value(context) >= right.value(context) }
52
62
  end
53
63
 
54
64
  def operator
@@ -58,7 +68,7 @@ module Dentaku
58
68
 
59
69
  class NotEqual < Comparator
60
70
  def value(context = {})
61
- left.value(context) != right.value(context)
71
+ super() { left.value(context) != right.value(context) }
62
72
  end
63
73
 
64
74
  def operator
@@ -68,7 +78,7 @@ module Dentaku
68
78
 
69
79
  class Equal < Comparator
70
80
  def value(context = {})
71
- left.value(context) == right.value(context)
81
+ super() { left.value(context) == right.value(context) }
72
82
  end
73
83
 
74
84
  def operator
@@ -41,7 +41,8 @@ module Dentaku
41
41
  return number.include?('.') ? ::BigDecimal.new(number, DIG) : number.to_i if number
42
42
  end
43
43
 
44
- raise TypeError, "#{value || value.class} could not be cast to a number."
44
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: value, for: Numeric),
45
+ "'#{value || value.class}' is not coercible to numeric"
45
46
  end
46
47
  end
47
48
  end
@@ -11,9 +11,9 @@ Dentaku::AST::Function.register(:and, :logical, lambda { |*args|
11
11
 
12
12
  args.all? do |arg|
13
13
  case arg
14
- when TrueClass, nil
14
+ when TrueClass
15
15
  true
16
- when FalseClass
16
+ when FalseClass, nil
17
17
  false
18
18
  else
19
19
  raise Dentaku::ArgumentError.for(
@@ -11,15 +11,15 @@ Dentaku::AST::Function.register(:or, :logical, lambda { |*args|
11
11
 
12
12
  args.any? do |arg|
13
13
  case arg
14
- when TrueClass, nil
14
+ when TrueClass
15
15
  true
16
- when FalseClass
16
+ when FalseClass, nil
17
17
  false
18
18
  else
19
19
  raise Dentaku::ArgumentError.for(
20
20
  :incompatible_type,
21
- function_name: 'AND()', expect: :logical, actual: arg.class
22
- ), 'AND() requires arguments to be logical expressions'
21
+ function_name: 'OR()', expect: :logical, actual: arg.class
22
+ ), 'OR() requires arguments to be logical expressions'
23
23
  end
24
24
  end
25
25
  })
@@ -20,8 +20,9 @@ module Dentaku
20
20
  results = load_results(&error_handler)
21
21
 
22
22
  FlatHash.expand(
23
- expression_hash.each_with_object({}) do |(k, _), r|
24
- r[k] = results[k.to_s]
23
+ expression_hash.each_with_object({}) do |(k, v), r|
24
+ default = v.nil? ? v : :undefined
25
+ r[k] = results.fetch(k.to_s, default)
25
26
  end
26
27
  )
27
28
  end
@@ -81,6 +82,9 @@ module Dentaku
81
82
  r[var_name] = block.call(ex)
82
83
  end
83
84
  end
85
+ rescue TSort::Cyclic => ex
86
+ block.call(ex)
87
+ {}
84
88
  end
85
89
 
86
90
  def expressions
@@ -46,7 +46,7 @@ module Dentaku
46
46
 
47
47
  def evaluate(expression, data = {}, &block)
48
48
  evaluate!(expression, data)
49
- rescue UnboundVariableError, Dentaku::ArgumentError => ex
49
+ rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
50
50
  block.call(expression, ex) if block_given?
51
51
  end
52
52
 
@@ -1,5 +1,8 @@
1
1
  module Dentaku
2
- class UnboundVariableError < StandardError
2
+ class Error < StandardError
3
+ end
4
+
5
+ class UnboundVariableError < Error
3
6
  attr_accessor :recipient_variable
4
7
 
5
8
  attr_reader :unbound_variables
@@ -9,7 +12,7 @@ module Dentaku
9
12
  end
10
13
  end
11
14
 
12
- class NodeError < StandardError
15
+ class NodeError < Error
13
16
  attr_reader :child, :expect, :actual
14
17
 
15
18
  def initialize(expect, actual, child)
@@ -19,7 +22,7 @@ module Dentaku
19
22
  end
20
23
  end
21
24
 
22
- class ParseError < StandardError
25
+ class ParseError < Error
23
26
  attr_reader :reason, :meta
24
27
 
25
28
  def initialize(reason, **meta)
@@ -45,7 +48,7 @@ module Dentaku
45
48
  end
46
49
  end
47
50
 
48
- class TokenizerError < StandardError
51
+ class TokenizerError < Error
49
52
  attr_reader :reason, :meta
50
53
 
51
54
  def initialize(reason, **meta)
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.2.0"
2
+ VERSION = "3.2.1"
3
3
  end
@@ -8,7 +8,10 @@ describe Dentaku::AST::Comparator do
8
8
  let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
9
9
  let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
10
10
  let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
11
- let(:ctx) { { 'x' => 'hello', 'y' => 'world' } }
11
+ let(:nilly) do
12
+ Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'nilly')
13
+ end
14
+ let(:ctx) { { 'x' => 'hello', 'y' => 'world', 'nilly' => nil } }
12
15
 
13
16
  it 'performs comparison with numeric operands' do
14
17
  expect(less_than(one, two).value(ctx)).to be_truthy
@@ -18,6 +21,24 @@ describe Dentaku::AST::Comparator do
18
21
  expect(equal(x, y).value(ctx)).to be_falsey
19
22
  end
20
23
 
24
+ it 'raises a dentaku argument error when incorrect arguments are passed in' do
25
+ expect { less_than(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
26
+ expect { less_than_or_equal(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
27
+ expect { greater_than(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
28
+ expect { greater_than_or_equal(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
29
+ expect { not_equal(one, nilly).value(ctx) }.to_not raise_error
30
+ expect { equal(one, nilly).value(ctx) }.to_not raise_error
31
+ end
32
+
33
+ it 'raises a dentaku error when nil is passed in as first argument' do
34
+ expect { less_than(nilly, one).value(ctx) }.to raise_error Dentaku::Error
35
+ expect { less_than_or_equal(nilly, one).value(ctx) }.to raise_error Dentaku::Error
36
+ expect { greater_than(nilly, one).value(ctx) }.to raise_error Dentaku::Error
37
+ expect { greater_than_or_equal(nilly, one).value(ctx) }.to raise_error Dentaku::Error
38
+ expect { not_equal(nilly, one).value(ctx) }.to_not raise_error
39
+ expect { equal(nilly, one).value(ctx) }.to_not raise_error
40
+ end
41
+
21
42
  it 'returns correct operator symbols' do
22
43
  expect(less_than(one, two).operator).to eq(:<)
23
44
  expect(less_than_or_equal(one, two).operator).to eq(:<=)
@@ -29,6 +50,10 @@ describe Dentaku::AST::Comparator do
29
50
  .to raise_error(NotImplementedError)
30
51
  end
31
52
 
53
+ it 'relies on inheriting classes to expose value method' do
54
+ expect { described_class.new(one, two).value(ctx) }.to raise_error NoMethodError
55
+ end
56
+
32
57
  private
33
58
 
34
59
  def less_than(left, right)
@@ -54,11 +54,11 @@ describe Dentaku::AST::Function do
54
54
  end
55
55
 
56
56
  it 'raises an error if the value could not be cast to a Numeric' do
57
- expect { described_class.numeric('flarble') }.to raise_error TypeError
58
- expect { described_class.numeric('-') }.to raise_error TypeError
59
- expect { described_class.numeric('') }.to raise_error TypeError
60
- expect { described_class.numeric(nil) }.to raise_error TypeError
61
- expect { described_class.numeric('7.') }.to raise_error TypeError
62
- expect { described_class.numeric(true) }.to raise_error TypeError
57
+ expect { described_class.numeric('flarble') }.to raise_error Dentaku::ArgumentError
58
+ expect { described_class.numeric('-') }.to raise_error Dentaku::ArgumentError
59
+ expect { described_class.numeric('') }.to raise_error Dentaku::ArgumentError
60
+ expect { described_class.numeric(nil) }.to raise_error Dentaku::ArgumentError
61
+ expect { described_class.numeric('7.') }.to raise_error Dentaku::ArgumentError
62
+ expect { described_class.numeric(true) }.to raise_error Dentaku::ArgumentError
63
63
  end
64
64
  end
@@ -1,6 +1,5 @@
1
1
  require 'spec_helper'
2
2
  require 'dentaku'
3
-
4
3
  describe Dentaku::Calculator do
5
4
  let(:calculator) { described_class.new }
6
5
  let(:with_memory) { described_class.new.store(apples: 3) }
@@ -39,6 +38,70 @@ describe Dentaku::Calculator do
39
38
  expect(calculator.evaluate('t + 1*24*60*60', t: Time.local(2017, 1, 1))).to eq(Time.local(2017, 1, 2))
40
39
  expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
41
40
  expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
41
+ expect(calculator.evaluate("5%")).to eq (0.05)
42
+ end
43
+
44
+ describe 'evaluate' do
45
+ it 'returns nil when formula has error' do
46
+ expect(calculator.evaluate('1 + + 1')).to be_nil
47
+ end
48
+
49
+ it 'suppresses unbound variable errors' do
50
+ expect(calculator.evaluate('AND(a,b)')).to be_nil
51
+ expect(calculator.evaluate('IF(a, 1, 0)')).to be_nil
52
+ expect(calculator.evaluate('MAX(a,b)')).to be_nil
53
+ expect(calculator.evaluate('MIN(a,b)')).to be_nil
54
+ expect(calculator.evaluate('NOT(a)')).to be_nil
55
+ expect(calculator.evaluate('OR(a,b)')).to be_nil
56
+ expect(calculator.evaluate('ROUND(a)')).to be_nil
57
+ expect(calculator.evaluate('ROUNDDOWN(a)')).to be_nil
58
+ expect(calculator.evaluate('ROUNDUP(a)')).to be_nil
59
+ expect(calculator.evaluate('SUM(a,b)')).to be_nil
60
+ end
61
+
62
+ it 'suppresses numeric coercion errors' do
63
+ expect(calculator.evaluate('MAX(a,b)', a: nil, b: nil)).to be_nil
64
+ expect(calculator.evaluate('MIN(a,b)', a: nil, b: nil)).to be_nil
65
+ expect(calculator.evaluate('ROUND(a)', a: nil)).to be_nil
66
+ expect(calculator.evaluate('ROUNDDOWN(a)', a: nil)).to be_nil
67
+ expect(calculator.evaluate('ROUNDUP(a)', a: nil)).to be_nil
68
+ expect(calculator.evaluate('SUM(a,b)', a: nil, b: nil)).to be_nil
69
+ end
70
+
71
+ it 'treats explicit nil as logical false' do
72
+ expect(calculator.evaluate('AND(a,b)', a: nil, b: nil)).to be_falsy
73
+ expect(calculator.evaluate('IF(a,1,0)', a: nil, b: nil)).to eq(0)
74
+ expect(calculator.evaluate('NOT(a)', a: nil, b: nil)).to be_truthy
75
+ expect(calculator.evaluate('OR(a,b)', a: nil, b: nil)).to be_falsy
76
+ end
77
+ end
78
+
79
+ describe 'evaluate!' do
80
+ it 'raises exception when formula has error' do
81
+ expect { calculator.evaluate!('1 + + 1') }.to raise_error(Dentaku::ParseError)
82
+ end
83
+
84
+ it 'raises unbound variable errors' do
85
+ expect { calculator.evaluate!('AND(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
86
+ expect { calculator.evaluate!('IF(a, 1, 0)') }.to raise_error(Dentaku::UnboundVariableError)
87
+ expect { calculator.evaluate!('MAX(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
88
+ expect { calculator.evaluate!('MIN(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
89
+ expect { calculator.evaluate!('NOT(a)') }.to raise_error(Dentaku::UnboundVariableError)
90
+ expect { calculator.evaluate!('OR(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
91
+ expect { calculator.evaluate!('ROUND(a)') }.to raise_error(Dentaku::UnboundVariableError)
92
+ expect { calculator.evaluate!('ROUNDDOWN(a)') }.to raise_error(Dentaku::UnboundVariableError)
93
+ expect { calculator.evaluate!('ROUNDUP(a)') }.to raise_error(Dentaku::UnboundVariableError)
94
+ expect { calculator.evaluate!('SUM(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
95
+ end
96
+
97
+ it 'raises numeric coersion errors' do
98
+ expect { calculator.evaluate!('MAX(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
99
+ expect { calculator.evaluate!('MIN(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
100
+ expect { calculator.evaluate!('ROUND(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
101
+ expect { calculator.evaluate!('ROUNDDOWN(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
102
+ expect { calculator.evaluate!('ROUNDUP(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
103
+ expect { calculator.evaluate!('SUM(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
104
+ end
42
105
  end
43
106
 
44
107
  it 'supports unicode characters in identifiers' do
@@ -84,7 +147,7 @@ describe Dentaku::Calculator do
84
147
  expect(calculator.evaluate!('a[x+1]', x: 1)).to eq 3
85
148
  end
86
149
 
87
- it 'evalutates arrays' do
150
+ it 'evaluates arrays' do
88
151
  expect(calculator.evaluate([1, 2, 3])).to eq([1, 2, 3])
89
152
  end
90
153
  end
@@ -160,6 +223,15 @@ describe Dentaku::Calculator do
160
223
 
161
224
  expect(result[:weight]).to eq 130.368
162
225
  end
226
+
227
+ it 'raises an exception if there are cyclic dependencies' do
228
+ expect {
229
+ calculator.solve!(
230
+ make_money: "have_money",
231
+ have_money: "make_money"
232
+ )
233
+ }.to raise_error(TSort::Cyclic)
234
+ end
163
235
  end
164
236
 
165
237
  describe 'solve' do
@@ -198,6 +270,19 @@ describe Dentaku::Calculator do
198
270
  d: 0,
199
271
  )
200
272
  end
273
+
274
+ it 'returns undefined if there are cyclic dependencies' do
275
+ expect {
276
+ result = calculator.solve(
277
+ make_money: "have_money",
278
+ have_money: "make_money"
279
+ )
280
+ expect(result).to eq(
281
+ make_money: :undefined,
282
+ have_money: :undefined
283
+ )
284
+ }.not_to raise_error
285
+ end
201
286
  end
202
287
 
203
288
  it 'evaluates a statement with no variables' do
@@ -22,7 +22,7 @@ describe Dentaku do
22
22
 
23
23
  it 'raises a parse error for bad logic expressions' do
24
24
  expect {
25
- Dentaku('true AND')
25
+ Dentaku!('true AND')
26
26
  }.to raise_error(Dentaku::ParseError)
27
27
  end
28
28
 
@@ -69,7 +69,9 @@ describe Dentaku::Calculator do
69
69
  expect(calculator1.evaluate("1 + my_function(2)")). to eq (1 + 2 * 2 + 1)
70
70
  expect(calculator2.evaluate("1 + my_function(2)")). to eq (1 + 4 * 2 + 3)
71
71
 
72
- expect { Dentaku::Calculator.new.evaluate("1 + my_function(2)") }.to raise_error(Dentaku::ParseError)
72
+ expect {
73
+ Dentaku::Calculator.new.evaluate!("1 + my_function(2)")
74
+ }.to raise_error(Dentaku::ParseError)
73
75
  end
74
76
 
75
77
  it 'self.add_function adds to default/global function registry' do
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.2.0
4
+ version: 3.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-14 00:00:00.000000000 Z
11
+ date: 2018-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: codecov
@@ -248,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
248
248
  version: '0'
249
249
  requirements: []
250
250
  rubyforge_project: dentaku
251
- rubygems_version: 2.6.13
251
+ rubygems_version: 2.7.6
252
252
  signing_key:
253
253
  specification_version: 4
254
254
  summary: A formula language parser and evaluator