dentaku 2.0.11 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +0 -1
  4. data/CHANGELOG.md +19 -0
  5. data/README.md +3 -2
  6. data/dentaku.gemspec +1 -0
  7. data/lib/dentaku/ast.rb +4 -0
  8. data/lib/dentaku/ast/access.rb +27 -0
  9. data/lib/dentaku/ast/arithmetic.rb +49 -7
  10. data/lib/dentaku/ast/case.rb +17 -3
  11. data/lib/dentaku/ast/combinators.rb +8 -2
  12. data/lib/dentaku/ast/function.rb +16 -0
  13. data/lib/dentaku/ast/function_registry.rb +15 -2
  14. data/lib/dentaku/ast/functions/and.rb +25 -0
  15. data/lib/dentaku/ast/functions/max.rb +1 -1
  16. data/lib/dentaku/ast/functions/min.rb +1 -1
  17. data/lib/dentaku/ast/functions/or.rb +25 -0
  18. data/lib/dentaku/ast/functions/round.rb +2 -2
  19. data/lib/dentaku/ast/functions/rounddown.rb +3 -2
  20. data/lib/dentaku/ast/functions/roundup.rb +3 -2
  21. data/lib/dentaku/ast/functions/ruby_math.rb +3 -3
  22. data/lib/dentaku/ast/functions/switch.rb +8 -0
  23. data/lib/dentaku/ast/identifier.rb +3 -2
  24. data/lib/dentaku/ast/negation.rb +5 -1
  25. data/lib/dentaku/ast/node.rb +3 -0
  26. data/lib/dentaku/bulk_expression_solver.rb +1 -2
  27. data/lib/dentaku/calculator.rb +7 -6
  28. data/lib/dentaku/exceptions.rb +75 -1
  29. data/lib/dentaku/parser.rb +73 -12
  30. data/lib/dentaku/token.rb +4 -0
  31. data/lib/dentaku/token_scanner.rb +20 -3
  32. data/lib/dentaku/tokenizer.rb +31 -4
  33. data/lib/dentaku/version.rb +1 -1
  34. data/spec/ast/addition_spec.rb +6 -6
  35. data/spec/ast/and_function_spec.rb +35 -0
  36. data/spec/ast/and_spec.rb +1 -1
  37. data/spec/ast/arithmetic_spec.rb +56 -0
  38. data/spec/ast/division_spec.rb +1 -1
  39. data/spec/ast/function_spec.rb +43 -6
  40. data/spec/ast/max_spec.rb +15 -0
  41. data/spec/ast/min_spec.rb +15 -0
  42. data/spec/ast/or_spec.rb +35 -0
  43. data/spec/ast/round_spec.rb +25 -0
  44. data/spec/ast/rounddown_spec.rb +25 -0
  45. data/spec/ast/roundup_spec.rb +25 -0
  46. data/spec/ast/switch_spec.rb +30 -0
  47. data/spec/calculator_spec.rb +26 -4
  48. data/spec/exceptions_spec.rb +1 -1
  49. data/spec/parser_spec.rb +22 -3
  50. data/spec/spec_helper.rb +12 -2
  51. data/spec/token_scanner_spec.rb +0 -4
  52. data/spec/tokenizer_spec.rb +40 -2
  53. metadata +39 -3
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "2.0.11"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -4,8 +4,8 @@ require 'dentaku/ast/arithmetic'
4
4
  require 'dentaku/token'
5
5
 
6
6
  describe Dentaku::AST::Addition do
7
- let(:five) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 5) }
8
- let(:six) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 6) }
7
+ let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
8
+ let(:six) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 6) }
9
9
 
10
10
  let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
11
11
 
@@ -17,7 +17,7 @@ describe Dentaku::AST::Addition do
17
17
  it 'requires numeric operands' do
18
18
  expect {
19
19
  described_class.new(five, t)
20
- }.to raise_error(Dentaku::ParseError, /requires numeric operands/)
20
+ }.to raise_error(Dentaku::NodeError, /requires numeric operands/)
21
21
 
22
22
  expression = Dentaku::AST::Multiplication.new(five, five)
23
23
  group = Dentaku::AST::Grouping.new(expression)
@@ -29,7 +29,7 @@ describe Dentaku::AST::Addition do
29
29
 
30
30
  it 'allows operands that respond to addition' do
31
31
  # Sample struct that has a custom definition for addition
32
-
32
+
33
33
  Operand = Struct.new(:value) do
34
34
  def +(other)
35
35
  case other
@@ -41,8 +41,8 @@ describe Dentaku::AST::Addition do
41
41
  end
42
42
  end
43
43
 
44
- operand_five = Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, Operand.new(5))
45
- operand_six = Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, Operand.new(6))
44
+ operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Operand.new(5))
45
+ operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Operand.new(6))
46
46
 
47
47
  expect {
48
48
  described_class.new(operand_five, operand_six)
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/ast/functions/and'
4
+
5
+ describe 'Dentaku::AST::And' do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+
8
+ it 'returns false if any of the arguments is false' do
9
+ result = Dentaku('AND(1 = 1, 0 = 1)')
10
+ expect(result).to eq false
11
+ end
12
+
13
+ it 'supports nested expressions' do
14
+ result = Dentaku('AND(y = 1, x = 1)', x: 1, y: 2)
15
+ expect(result).to eq false
16
+ end
17
+
18
+ it 'returns true if all of the arguments are true' do
19
+ result = Dentaku('AND(1 = 1, "2" = "2", true = true, true)')
20
+ expect(result).to eq true
21
+ end
22
+
23
+ it 'returns true if all nested AND functions return true' do
24
+ result = Dentaku('AND(AND(1 = 1), AND(true != false, AND(true)))')
25
+ expect(result).to eq true
26
+ end
27
+
28
+ it 'raises an error if no arguments are passed' do
29
+ expect { calculator.evaluate!('AND()') }.to raise_error(ArgumentError)
30
+ end
31
+
32
+ it 'raises an error if a non logical argument is passed' do
33
+ expect { calculator.evaluate!('AND("r")') }.to raise_error(ArgumentError)
34
+ end
35
+ end
@@ -17,7 +17,7 @@ describe Dentaku::AST::And do
17
17
  it 'requires logical operands' do
18
18
  expect {
19
19
  described_class.new(t, five)
20
- }.to raise_error(Dentaku::ParseError, /requires logical operands/)
20
+ }.to raise_error(Dentaku::NodeError, /requires logical operands/)
21
21
 
22
22
  expression = Dentaku::AST::LessThanOrEqual.new(five, five)
23
23
  expect {
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/arithmetic'
3
+
4
+ require 'dentaku/token'
5
+
6
+ describe Dentaku::AST::Arithmetic do
7
+ let(:one) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 1) }
8
+ let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
9
+ let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
10
+ let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
11
+ let(:ctx) {{'x' => 1, 'y' => 2}}
12
+
13
+ it 'performs an arithmetic operation with numeric operands' do
14
+ expect(add(one, two)).to eq 3
15
+ expect(sub(one, two)).to eq -1
16
+ expect(mul(one, two)).to eq 2
17
+ expect(div(one, two)).to eq 0.5
18
+ end
19
+
20
+ it 'performs an arithmetic operation with one numeric operand and one string operand' do
21
+ expect(add(one, x)).to eq 2
22
+ expect(sub(one, x)).to eq 0
23
+ expect(mul(one, x)).to eq 1
24
+ expect(div(one, x)).to eq 1
25
+
26
+ expect(add(y, two)).to eq 4
27
+ expect(sub(y, two)).to eq 0
28
+ expect(mul(y, two)).to eq 4
29
+ expect(div(y, two)).to eq 1
30
+ end
31
+
32
+ it 'performs an arithmetic operation with string operands' do
33
+ expect(add(x, y)).to eq 3
34
+ expect(sub(x, y)).to eq -1
35
+ expect(mul(x, y)).to eq 2
36
+ expect(div(x, y)).to eq 0.5
37
+ end
38
+
39
+ private
40
+
41
+ def add(left, right)
42
+ Dentaku::AST::Addition.new(left, right).value(ctx)
43
+ end
44
+
45
+ def sub(left, right)
46
+ Dentaku::AST::Subtraction.new(left, right).value(ctx)
47
+ end
48
+
49
+ def mul(left, right)
50
+ Dentaku::AST::Multiplication.new(left, right).value(ctx)
51
+ end
52
+
53
+ def div(left, right)
54
+ Dentaku::AST::Division.new(left, right).value(ctx)
55
+ end
56
+ end
@@ -17,7 +17,7 @@ describe Dentaku::AST::Division do
17
17
  it 'requires numeric operands' do
18
18
  expect {
19
19
  described_class.new(five, t)
20
- }.to raise_error(Dentaku::ParseError, /requires numeric operands/)
20
+ }.to raise_error(Dentaku::NodeError, /requires numeric operands/)
21
21
 
22
22
  expression = Dentaku::AST::Multiplication.new(five, five)
23
23
  group = Dentaku::AST::Grouping.new(expression)
@@ -1,5 +1,7 @@
1
+ require 'bigdecimal'
1
2
  require 'spec_helper'
2
3
  require 'dentaku/ast/function'
4
+ require 'dentaku/exceptions'
3
5
 
4
6
  class Clazz; end
5
7
 
@@ -8,12 +10,6 @@ describe Dentaku::AST::Function do
8
10
  expect(described_class).to respond_to(:get)
9
11
  end
10
12
 
11
- it 'raises an exception when trying to access an undefined function' do
12
- expect {
13
- described_class.get("flarble")
14
- }.to raise_error(Dentaku::ParseError, /undefined function/i)
15
- end
16
-
17
13
  it 'registers a custom function' do
18
14
  described_class.register("flarble", :string, -> { "flarble" })
19
15
  expect { described_class.get("flarble") }.not_to raise_error
@@ -24,4 +20,45 @@ describe Dentaku::AST::Function do
24
20
  it 'does not throw an error when registering a function with a name that matches a currently defined constant' do
25
21
  expect { described_class.register("clazz", :string, -> { "clazzified" }) }.not_to raise_error
26
22
  end
23
+
24
+ describe "#arity" do
25
+ it "gives the correct arity for custom functions" do
26
+ zero = described_class.register("zero", :numeric, ->() { 0 })
27
+ expect(zero.arity).to eq 0
28
+
29
+ one = described_class.register("one", :numeric, ->(x) { x * 2 })
30
+ expect(one.arity).to eq 1
31
+
32
+ two = described_class.register("two", :numeric, ->(x,y) { x + y })
33
+ expect(two.arity).to eq 2
34
+
35
+ many = described_class.register("many", :numeric, ->(*args) { args.max })
36
+ expect(many.arity).to be_nil
37
+ end
38
+ end
39
+
40
+ it 'casts a String to an Integer if possible' do
41
+ expect(described_class.numeric('3')).to eq 3
42
+ end
43
+
44
+ it 'casts a String to a BigDecimal if possible and if Integer would loose information' do
45
+ expect(described_class.numeric('3.2')).to eq 3.2
46
+ end
47
+
48
+ it 'casts a String to a BigDecimal with a negative number' do
49
+ expect(described_class.numeric('-3.2')).to eq -3.2
50
+ end
51
+
52
+ it 'casts a String to a BigDecimal without a leading zero' do
53
+ expect(described_class.numeric('-.2')).to eq -0.2
54
+ end
55
+
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
63
+ end
27
64
  end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/max'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Max' do
6
+ it 'returns the largest numeric value in an array of Numeric values' do
7
+ result = Dentaku('MAX(1, x, 1.8)', x: 2.3)
8
+ expect(result).to eq 2.3
9
+ end
10
+
11
+ it 'returns the largest value even if a String is passed' do
12
+ result = Dentaku('MAX(1, x, 1.8)', x: '2.3')
13
+ expect(result).to eq 2.3
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/min'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Min' do
6
+ it 'returns the smallest numeric value in an array of Numeric values' do
7
+ result = Dentaku('MIN(1, x, 1.8)', x: 2.3)
8
+ expect(result).to eq 1
9
+ end
10
+
11
+ it 'returns the smallest value even if a String is passed' do
12
+ result = Dentaku('MIN(1, x, 1.8)', x: '0.3')
13
+ expect(result).to eq 0.3
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/ast/functions/or'
4
+
5
+ describe 'Dentaku::AST::Or' do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+
8
+ it 'returns false if all of the arguments are false' do
9
+ result = Dentaku('OR(1 = "1", 0 = 1)')
10
+ expect(result).to eq false
11
+ end
12
+
13
+ it 'supports nested expressions' do
14
+ result = Dentaku('OR(y = 1, x = 1)', x: 1, y: 2)
15
+ expect(result).to eq true
16
+ end
17
+
18
+ it 'returns true if any of the arguments is true' do
19
+ result = Dentaku('OR(1 = "1", "2" = "2", true = false, false)')
20
+ expect(result).to eq true
21
+ end
22
+
23
+ it 'returns true if any nested OR function returns true' do
24
+ result = Dentaku('OR(OR(1 = 0), OR(true = false, OR(true)))')
25
+ expect(result).to eq true
26
+ end
27
+
28
+ it 'raises an error if no arguments are passed' do
29
+ expect { calculator.evaluate!('OR()') }.to raise_error(ArgumentError)
30
+ end
31
+
32
+ it 'raises an error if a non logical argument is passed' do
33
+ expect { calculator.evaluate!('OR("r")') }.to raise_error(ArgumentError)
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/round'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Round' do
6
+ it 'returns the rounded down value' do
7
+ result = Dentaku('ROUND(1.8)')
8
+ expect(result).to eq 2
9
+ end
10
+
11
+ it 'returns the rounded down value to the given precision' do
12
+ result = Dentaku('ROUND(x, y)', x: 1.8453, y: 3)
13
+ expect(result).to eq 1.845
14
+ end
15
+
16
+ it 'returns the rounded down value to the given precision, also with strings' do
17
+ result = Dentaku('ROUND(x, y)', x: '1.8453', y: '3')
18
+ expect(result).to eq 1.845
19
+ end
20
+
21
+ it 'returns the rounded down value to the given precision, also with nil' do
22
+ result = Dentaku('ROUND(x, y)', x: '1.8453', y: nil)
23
+ expect(result).to eq 2
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/rounddown'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Round' do
6
+ it 'returns the rounded value' do
7
+ result = Dentaku('ROUNDDOWN(1.8)')
8
+ expect(result).to eq 1
9
+ end
10
+
11
+ it 'returns the rounded value to the given precision' do
12
+ result = Dentaku('ROUNDDOWN(x, y)', x: 1.8453, y: 3)
13
+ expect(result).to eq 1.845
14
+ end
15
+
16
+ it 'returns the rounded value to the given precision, also with strings' do
17
+ result = Dentaku('ROUNDDOWN(x, y)', x: '1.8453', y: '3')
18
+ expect(result).to eq 1.845
19
+ end
20
+
21
+ it 'returns the rounded value to the given precision, also with nil' do
22
+ result = Dentaku('ROUNDDOWN(x, y)', x: '1.8453', y: nil)
23
+ expect(result).to eq 1
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/roundup'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Round' do
6
+ it 'returns the rounded value' do
7
+ result = Dentaku('ROUNDUP(1.8)')
8
+ expect(result).to eq 2
9
+ end
10
+
11
+ it 'returns the rounded value to the given precision' do
12
+ result = Dentaku('ROUNDUP(x, y)', x: 1.8453, y: 3)
13
+ expect(result).to eq 1.846
14
+ end
15
+
16
+ it 'returns the rounded value to the given precision, also with strings' do
17
+ result = Dentaku('ROUNDUP(x, y)', x: '1.8453', y: '3')
18
+ expect(result).to eq 1.846
19
+ end
20
+
21
+ it 'returns the rounded value to the given precision, also with nil' do
22
+ result = Dentaku('ROUNDUP(x, y)', x: '1.8453', y: nil)
23
+ expect(result).to eq 2
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/switch'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Switch' do
6
+ it 'returns the match if present in argumtents' do
7
+ result = Dentaku('SWITCH(1, 1, "one", 2, "two")')
8
+ expect(result).to eq 'one'
9
+ end
10
+
11
+ it 'returns nil if no match was found' do
12
+ result = Dentaku('SWITCH(3, 1, "one", 2, "two")')
13
+ expect(result).to eq nil
14
+ end
15
+
16
+ it 'returns the default value if present and no match was found' do
17
+ result = Dentaku('SWITCH(3, 1, "one", 2, "two", "no match")')
18
+ expect(result).to eq 'no match'
19
+ end
20
+
21
+ it 'returns the first match if multiple matches exist' do
22
+ result = Dentaku('SWITCH(1, 1, "one", 2, "two", 1, "three")')
23
+ expect(result).to eq 'one'
24
+ end
25
+
26
+ it 'does not return a match where a value matches the search value' do
27
+ result = Dentaku('SWITCH(1, "one", 1, 2, "two", 3)')
28
+ expect(result).to eq 3
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  require 'spec_helper'
2
- require 'dentaku/calculator'
2
+ require 'dentaku'
3
3
 
4
4
  describe Dentaku::Calculator do
5
5
  let(:calculator) { described_class.new }
@@ -32,6 +32,7 @@ describe Dentaku::Calculator do
32
32
  expect(calculator.evaluate('0.253/0.253')).to eq(1)
33
33
  expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
34
34
  expect(calculator.evaluate('10 + x', x: 'abc')).to be_nil
35
+ expect(calculator.evaluate('a/b', a: '10', b: '2')).to eq(5)
35
36
  expect(calculator.evaluate('t + 1*24*60*60', t: Time.local(2017, 1, 1))).to eq(Time.local(2017, 1, 2))
36
37
  expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
37
38
  expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
@@ -68,6 +69,13 @@ describe Dentaku::Calculator do
68
69
  expect(calculator.evaluate!('a.basket.of')).to eq 'apples'
69
70
  expect(calculator.evaluate!('b')).to eq 2
70
71
  end
72
+
73
+ it 'stores arrays' do
74
+ calculator.store({a: [1, 2, 3]})
75
+ expect(calculator.evaluate!('a[0]')).to eq 1
76
+ expect(calculator.evaluate!('a[x]', x: 1)).to eq 2
77
+ expect(calculator.evaluate!('a[x+1]', x: 1)).to eq 3
78
+ end
71
79
  end
72
80
 
73
81
  describe 'dependencies' do
@@ -192,14 +200,20 @@ describe Dentaku::Calculator do
192
200
  expect { calculator.evaluate!(unbound) }.to raise_error do |error|
193
201
  expect(error.unbound_variables).to eq ['foo']
194
202
  end
203
+ expect { calculator.evaluate!('a + b') }.to raise_error do |error|
204
+ expect(error.unbound_variables).to eq ['a', 'b']
205
+ end
195
206
  expect(calculator.evaluate(unbound)).to be_nil
196
207
  expect(calculator.evaluate(unbound) { :bar }).to eq :bar
197
208
  expect(calculator.evaluate(unbound) { |e| e }).to eq unbound
198
209
  end
199
210
 
200
211
  it 'fails to evaluate incomplete statements' do
201
- incomplete = 'true AND'
202
- expect { calculator.evaluate!(incomplete) }.to raise_error(Dentaku::ParseError)
212
+ ['true AND', 'a a ^&'].each do |statement|
213
+ expect {
214
+ calculator.evaluate!(statement)
215
+ }.to raise_error(Dentaku::ParseError)
216
+ end
203
217
  end
204
218
 
205
219
  it 'evaluates unbound statements given a binding in memory' do
@@ -455,7 +469,7 @@ describe Dentaku::Calculator do
455
469
  Math.methods(false).each do |method|
456
470
  it method do
457
471
  if Math.method(method).arity == 2
458
- expect(calculator.evaluate("#{method}(1,2)")).to eq Math.send(method, 1, 2)
472
+ expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq Math.send(method, 1, 2)
459
473
  else
460
474
  expect(calculator.evaluate("#{method}(1)")).to eq Math.send(method, 1)
461
475
  end
@@ -517,4 +531,12 @@ describe Dentaku::Calculator do
517
531
  ).to eq 'abcdef'
518
532
  end
519
533
  end
534
+
535
+ describe 'zero-arity functions' do
536
+ it 'can be used in formulas' do
537
+ calculator.add_function(:two, :numeric, -> { 2 })
538
+ expect(calculator.evaluate("max(two(), 1)")).to eq 2
539
+ expect(calculator.evaluate("max(1, two())")).to eq 2
540
+ end
541
+ end
520
542
  end