dentaku 2.0.11 → 3.0.0

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