dentaku 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -10
  3. data/.travis.yml +3 -6
  4. data/CHANGELOG.md +38 -1
  5. data/README.md +2 -2
  6. data/dentaku.gemspec +0 -2
  7. data/lib/dentaku.rb +14 -6
  8. data/lib/dentaku/ast.rb +5 -0
  9. data/lib/dentaku/ast/access.rb +15 -1
  10. data/lib/dentaku/ast/arithmetic.rb +29 -6
  11. data/lib/dentaku/ast/array.rb +15 -1
  12. data/lib/dentaku/ast/case.rb +13 -3
  13. data/lib/dentaku/ast/case/case_conditional.rb +13 -2
  14. data/lib/dentaku/ast/case/case_else.rb +12 -4
  15. data/lib/dentaku/ast/case/case_switch_variable.rb +8 -0
  16. data/lib/dentaku/ast/case/case_then.rb +12 -4
  17. data/lib/dentaku/ast/case/case_when.rb +12 -4
  18. data/lib/dentaku/ast/function.rb +11 -2
  19. data/lib/dentaku/ast/function_registry.rb +21 -0
  20. data/lib/dentaku/ast/functions/all.rb +36 -0
  21. data/lib/dentaku/ast/functions/any.rb +36 -0
  22. data/lib/dentaku/ast/functions/avg.rb +2 -2
  23. data/lib/dentaku/ast/functions/count.rb +8 -0
  24. data/lib/dentaku/ast/functions/duration.rb +51 -0
  25. data/lib/dentaku/ast/functions/if.rb +15 -2
  26. data/lib/dentaku/ast/functions/map.rb +36 -0
  27. data/lib/dentaku/ast/functions/mul.rb +3 -2
  28. data/lib/dentaku/ast/functions/pluck.rb +29 -0
  29. data/lib/dentaku/ast/functions/round.rb +1 -1
  30. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  31. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  32. data/lib/dentaku/ast/functions/ruby_math.rb +47 -3
  33. data/lib/dentaku/ast/functions/string_functions.rb +68 -4
  34. data/lib/dentaku/ast/functions/sum.rb +3 -2
  35. data/lib/dentaku/ast/grouping.rb +3 -1
  36. data/lib/dentaku/ast/identifier.rb +5 -1
  37. data/lib/dentaku/ast/negation.rb +3 -1
  38. data/lib/dentaku/ast/node.rb +4 -0
  39. data/lib/dentaku/ast/operation.rb +8 -0
  40. data/lib/dentaku/bulk_expression_solver.rb +34 -25
  41. data/lib/dentaku/calculator.rb +19 -6
  42. data/lib/dentaku/date_arithmetic.rb +45 -0
  43. data/lib/dentaku/exceptions.rb +4 -4
  44. data/lib/dentaku/flat_hash.rb +9 -2
  45. data/lib/dentaku/parser.rb +31 -14
  46. data/lib/dentaku/token_matcher.rb +1 -1
  47. data/lib/dentaku/token_scanner.rb +1 -1
  48. data/lib/dentaku/tokenizer.rb +7 -2
  49. data/lib/dentaku/version.rb +1 -1
  50. data/spec/ast/addition_spec.rb +7 -1
  51. data/spec/ast/and_function_spec.rb +6 -6
  52. data/spec/ast/and_spec.rb +1 -1
  53. data/spec/ast/arithmetic_spec.rb +57 -29
  54. data/spec/ast/avg_spec.rb +9 -5
  55. data/spec/ast/count_spec.rb +7 -7
  56. data/spec/ast/division_spec.rb +7 -1
  57. data/spec/ast/function_spec.rb +9 -9
  58. data/spec/ast/max_spec.rb +3 -3
  59. data/spec/ast/min_spec.rb +3 -3
  60. data/spec/ast/mul_spec.rb +10 -6
  61. data/spec/ast/negation_spec.rb +48 -0
  62. data/spec/ast/node_spec.rb +11 -8
  63. data/spec/ast/numeric_spec.rb +1 -1
  64. data/spec/ast/or_spec.rb +6 -6
  65. data/spec/ast/round_spec.rb +14 -4
  66. data/spec/ast/rounddown_spec.rb +14 -4
  67. data/spec/ast/roundup_spec.rb +14 -4
  68. data/spec/ast/string_functions_spec.rb +35 -0
  69. data/spec/ast/sum_spec.rb +10 -6
  70. data/spec/ast/switch_spec.rb +5 -5
  71. data/spec/bulk_expression_solver_spec.rb +18 -1
  72. data/spec/calculator_spec.rb +173 -28
  73. data/spec/dentaku_spec.rb +18 -5
  74. data/spec/external_function_spec.rb +29 -5
  75. data/spec/parser_spec.rb +85 -123
  76. data/spec/spec_helper.rb +6 -4
  77. data/spec/token_matcher_spec.rb +8 -8
  78. data/spec/token_scanner_spec.rb +4 -4
  79. data/spec/tokenizer_spec.rb +32 -13
  80. metadata +11 -4
@@ -5,36 +5,36 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Count' do
6
6
  it 'returns the length of an array' do
7
7
  result = Dentaku('COUNT(1, x, 1.8)', x: 2.3)
8
- expect(result).to eq 3
8
+ expect(result).to eq(3)
9
9
  end
10
10
 
11
11
  it 'returns the length of a single number object' do
12
12
  result = Dentaku('COUNT(x)', x: 2.3)
13
- expect(result).to eq 1
13
+ expect(result).to eq(1)
14
14
  end
15
15
 
16
16
  it 'returns the length if a single String is passed' do
17
17
  result = Dentaku('COUNT(x)', x: 'dentaku')
18
- expect(result).to eq 7
18
+ expect(result).to eq(7)
19
19
  end
20
20
 
21
21
  it 'returns the length if an array is passed' do
22
22
  result = Dentaku('COUNT(x)', x: [4, 5])
23
- expect(result).to eq 2
23
+ expect(result).to eq(2)
24
24
  end
25
25
 
26
26
  it 'returns the length if an array with one element is passed' do
27
27
  result = Dentaku('COUNT(x)', x: [4])
28
- expect(result).to eq 1
28
+ expect(result).to eq(1)
29
29
  end
30
30
 
31
31
  it 'returns the length if an array even if it has nested array' do
32
32
  result = Dentaku('COUNT(1, x, 3)', x: [4, 5])
33
- expect(result).to eq 3
33
+ expect(result).to eq(3)
34
34
  end
35
35
 
36
36
  it 'returns the length if an array is passed' do
37
37
  result = Dentaku('COUNT()')
38
- expect(result).to eq 0
38
+ expect(result).to eq(0)
39
39
  end
40
40
  end
@@ -9,9 +9,15 @@ describe Dentaku::AST::Division do
9
9
 
10
10
  let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
11
11
 
12
+ it 'allows access to its sub-trees' do
13
+ node = described_class.new(five, six)
14
+ expect(node.left).to eq(five)
15
+ expect(node.right).to eq(six)
16
+ end
17
+
12
18
  it 'performs division' do
13
19
  node = described_class.new(five, six)
14
- expect(node.value.round(4)).to eq 0.8333
20
+ expect(node.value.round(4)).to eq(0.8333)
15
21
  end
16
22
 
17
23
  it 'requires numeric operands' do
@@ -14,7 +14,7 @@ describe Dentaku::AST::Function do
14
14
  described_class.register("flarble", :string, -> { "flarble" })
15
15
  expect { described_class.get("flarble") }.not_to raise_error
16
16
  function = described_class.get("flarble").new
17
- expect(function.value).to eq "flarble"
17
+ expect(function.value).to eq("flarble")
18
18
  end
19
19
 
20
20
  it 'does not throw an error when registering a function with a name that matches a currently defined constant' do
@@ -22,15 +22,15 @@ 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
- expect(zero.arity).to eq 0
27
+ expect(zero.arity).to eq(0)
28
28
 
29
29
  one = described_class.register("one", :numeric, ->(x) { x * 2 })
30
- expect(one.arity).to eq 1
30
+ expect(one.arity).to eq(1)
31
31
 
32
32
  two = described_class.register("two", :numeric, ->(x, y) { x + y })
33
- expect(two.arity).to eq 2
33
+ expect(two.arity).to eq(2)
34
34
 
35
35
  many = described_class.register("many", :numeric, ->(*args) { args.max })
36
36
  expect(many.arity).to be_nil
@@ -38,19 +38,19 @@ describe Dentaku::AST::Function do
38
38
  end
39
39
 
40
40
  it 'casts a String to an Integer if possible' do
41
- expect(described_class.numeric('3')).to eq 3
41
+ expect(described_class.numeric('3')).to eq(3)
42
42
  end
43
43
 
44
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
45
+ expect(described_class.numeric('3.2')).to eq(3.2)
46
46
  end
47
47
 
48
48
  it 'casts a String to a BigDecimal with a negative number' do
49
- expect(described_class.numeric('-3.2')).to eq -3.2
49
+ expect(described_class.numeric('-3.2')).to eq(-3.2)
50
50
  end
51
51
 
52
52
  it 'casts a String to a BigDecimal without a leading zero' do
53
- expect(described_class.numeric('-.2')).to eq -0.2
53
+ expect(described_class.numeric('-.2')).to eq(-0.2)
54
54
  end
55
55
 
56
56
  it 'raises an error if the value could not be cast to a Numeric' do
@@ -5,16 +5,16 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Max' do
6
6
  it 'returns the largest numeric value in an array of Numeric values' do
7
7
  result = Dentaku('MAX(1, x, 1.8)', x: 2.3)
8
- expect(result).to eq 2.3
8
+ expect(result).to eq(2.3)
9
9
  end
10
10
 
11
11
  it 'returns the largest value even if a String is passed' do
12
12
  result = Dentaku('MAX(1, x, 1.8)', x: '2.3')
13
- expect(result).to eq 2.3
13
+ expect(result).to eq(2.3)
14
14
  end
15
15
 
16
16
  it 'returns the largest value even if an Array is passed' do
17
17
  result = Dentaku('MAX(1, x, 1.8)', x: [1.5, 2.3, 1.7])
18
- expect(result).to eq 2.3
18
+ expect(result).to eq(2.3)
19
19
  end
20
20
  end
@@ -5,16 +5,16 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Min' do
6
6
  it 'returns the smallest numeric value in an array of Numeric values' do
7
7
  result = Dentaku('MIN(1, x, 1.8)', x: 2.3)
8
- expect(result).to eq 1
8
+ expect(result).to eq(1)
9
9
  end
10
10
 
11
11
  it 'returns the smallest value even if a String is passed' do
12
12
  result = Dentaku('MIN(1, x, 1.8)', x: '0.3')
13
- expect(result).to eq 0.3
13
+ expect(result).to eq(0.3)
14
14
  end
15
15
 
16
16
  it 'returns the smallest value even if an Array is passed' do
17
17
  result = Dentaku('MIN(1, x, 1.8)', x: [1.5, 0.3, 1.7])
18
- expect(result).to eq 0.3
18
+ expect(result).to eq(0.3)
19
19
  end
20
20
  end
@@ -5,34 +5,38 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Mul' do
6
6
  it 'returns the product of an array of Numeric values' do
7
7
  result = Dentaku('MUL(1, x, 1.8)', x: 2.3)
8
- expect(result).to eq 4.14
8
+ expect(result).to eq(4.14)
9
9
  end
10
10
 
11
11
  it 'returns the product of a single entry array of a Numeric value' do
12
12
  result = Dentaku('MUL(x)', x: 2.3)
13
- expect(result).to eq 2.3
13
+ expect(result).to eq(2.3)
14
14
  end
15
15
 
16
16
  it 'coerces string inputs to numeric' do
17
17
  result = Dentaku('mul(1, x, 1.8)', x: '2.3')
18
- expect(result).to eq 4.14
18
+ expect(result).to eq(4.14)
19
19
  end
20
20
 
21
21
  it 'returns the product even if an array is passed' do
22
22
  result = Dentaku('mul(1, x, 2.3)', x: [4, 5])
23
- expect(result).to eq 46
23
+ expect(result).to eq(46)
24
24
  end
25
25
 
26
26
  it 'handles nested calls' do
27
27
  result = Dentaku('mul(1, x, mul(4, 5))', x: '2.3')
28
- expect(result).to eq 46
28
+ expect(result).to eq(46)
29
29
  end
30
30
 
31
31
  context 'checking errors' do
32
32
  let(:calculator) { Dentaku::Calculator.new }
33
33
 
34
34
  it 'raises an error if no arguments are passed' do
35
- expect { calculator.evaluate!('MUL()') }.to raise_error(ArgumentError)
35
+ expect { calculator.evaluate!('MUL()') }.to raise_error(Dentaku::ArgumentError)
36
+ end
37
+
38
+ it 'raises an error if an empty array is passed' do
39
+ expect { calculator.evaluate!('MUL(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
36
40
  end
37
41
  end
38
42
  end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/arithmetic'
3
+
4
+ require 'dentaku/token'
5
+
6
+ describe Dentaku::AST::Negation do
7
+ let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
8
+ let(:t) { Dentaku::AST::Logical.new Dentaku::Token.new(:logical, true) }
9
+ let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
10
+
11
+ it 'allows access to its sub-node' do
12
+ node = described_class.new(five)
13
+ expect(node.node).to eq(five)
14
+ end
15
+
16
+ it 'performs negation' do
17
+ node = described_class.new(five)
18
+ expect(node.value).to eq(-5)
19
+ end
20
+
21
+ it 'requires numeric operands' do
22
+ expect {
23
+ described_class.new(t)
24
+ }.to raise_error(Dentaku::NodeError, /requires numeric operands/)
25
+
26
+ expression = Dentaku::AST::Negation.new(five)
27
+ group = Dentaku::AST::Grouping.new(expression)
28
+
29
+ expect {
30
+ described_class.new(group)
31
+ }.not_to raise_error
32
+ end
33
+
34
+ it 'correctly parses string operands to numeric values' do
35
+ node = described_class.new(x)
36
+ expect(node.value('x' => '5')).to eq(-5)
37
+ end
38
+
39
+ it 'raises error if input string is not coercible to numeric' do
40
+ node = described_class.new(x)
41
+ expect { node.value('x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
42
+ end
43
+
44
+ it 'raises error if given a non-numeric argument' do
45
+ node = described_class.new(x)
46
+ expect { node.value('x' => true) }.to raise_error(Dentaku::ArgumentError)
47
+ end
48
+ end
@@ -6,30 +6,33 @@ require 'dentaku/parser'
6
6
  describe Dentaku::AST::Node do
7
7
  it 'returns list of dependencies' do
8
8
  node = make_node('x + 5')
9
- expect(node.dependencies).to eq ['x']
9
+ expect(node.dependencies).to eq(['x'])
10
10
 
11
11
  node = make_node('5 < x')
12
- expect(node.dependencies).to eq ['x']
12
+ expect(node.dependencies).to eq(['x'])
13
13
 
14
14
  node = make_node('5 < 7')
15
- expect(node.dependencies).to eq []
15
+ expect(node.dependencies).to eq([])
16
16
 
17
17
  node = make_node('(y * 7)')
18
- expect(node.dependencies).to eq ['y']
18
+ expect(node.dependencies).to eq(['y'])
19
19
 
20
20
  node = make_node('if(x > 5, y, z)')
21
- expect(node.dependencies).to eq ['x', 'y', 'z']
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
- expect(node.dependencies).to eq []
30
+ expect(node.dependencies).to eq([])
28
31
  end
29
32
 
30
33
  it 'returns unique list of dependencies' do
31
34
  node = make_node('x + x')
32
- expect(node.dependencies).to eq ['x']
35
+ expect(node.dependencies).to eq(['x'])
33
36
  end
34
37
 
35
38
  private
@@ -7,7 +7,7 @@ describe Dentaku::AST::Numeric do
7
7
  subject { described_class.new(Dentaku::Token.new(:numeric, 5)) }
8
8
 
9
9
  it 'has numeric type' do
10
- expect(subject.type).to eq :numeric
10
+ expect(subject.type).to eq(:numeric)
11
11
  end
12
12
 
13
13
  it 'has no dependencies' do
@@ -7,29 +7,29 @@ describe 'Dentaku::AST::Or' do
7
7
 
8
8
  it 'returns false if all of the arguments are false' do
9
9
  result = Dentaku('OR(1 = "1", 0 = 1)')
10
- expect(result).to eq false
10
+ expect(result).to eq(false)
11
11
  end
12
12
 
13
13
  it 'supports nested expressions' do
14
14
  result = Dentaku('OR(y = 1, x = 1)', x: 1, y: 2)
15
- expect(result).to eq true
15
+ expect(result).to eq(true)
16
16
  end
17
17
 
18
18
  it 'returns true if any of the arguments is true' do
19
19
  result = Dentaku('OR(1 = "1", "2" = "2", true = false, false)')
20
- expect(result).to eq true
20
+ expect(result).to eq(true)
21
21
  end
22
22
 
23
23
  it 'returns true if any nested OR function returns true' do
24
24
  result = Dentaku('OR(OR(1 = 0), OR(true = false, OR(true)))')
25
- expect(result).to eq true
25
+ expect(result).to eq(true)
26
26
  end
27
27
 
28
28
  it 'raises an error if no arguments are passed' do
29
- expect { calculator.evaluate!('OR()') }.to raise_error(ArgumentError)
29
+ expect { calculator.evaluate!('OR()') }.to raise_error(Dentaku::ArgumentError)
30
30
  end
31
31
 
32
32
  it 'raises an error if a non logical argument is passed' do
33
- expect { calculator.evaluate!('OR("r")') }.to raise_error(ArgumentError)
33
+ expect { calculator.evaluate!('OR("r")') }.to raise_error(Dentaku::ArgumentError)
34
34
  end
35
35
  end
@@ -5,21 +5,31 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Round' do
6
6
  it 'returns the rounded down value' do
7
7
  result = Dentaku('ROUND(1.8)')
8
- expect(result).to eq 2
8
+ expect(result).to eq(2)
9
9
  end
10
10
 
11
11
  it 'returns the rounded down value to the given precision' do
12
12
  result = Dentaku('ROUND(x, y)', x: 1.8453, y: 3)
13
- expect(result).to eq 1.845
13
+ expect(result).to eq(1.845)
14
14
  end
15
15
 
16
16
  it 'returns the rounded down value to the given precision, also with strings' do
17
17
  result = Dentaku('ROUND(x, y)', x: '1.8453', y: '3')
18
- expect(result).to eq 1.845
18
+ expect(result).to eq(1.845)
19
19
  end
20
20
 
21
21
  it 'returns the rounded down value to the given precision, also with nil' do
22
22
  result = Dentaku('ROUND(x, y)', x: '1.8453', y: nil)
23
- expect(result).to eq 2
23
+ expect(result).to eq(2)
24
+ end
25
+
26
+ context 'checking errors' do
27
+ it 'raises an error if first argument is not numeric' do
28
+ expect { Dentaku!("ROUND(2020-1-1, 0)") }.to raise_error(Dentaku::ArgumentError)
29
+ end
30
+
31
+ it 'raises an error if places is not numeric' do
32
+ expect { Dentaku!("ROUND(1.8, 2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
33
+ end
24
34
  end
25
35
  end
@@ -5,21 +5,31 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Round' do
6
6
  it 'returns the rounded value' do
7
7
  result = Dentaku('ROUNDDOWN(1.8)')
8
- expect(result).to eq 1
8
+ expect(result).to eq(1)
9
9
  end
10
10
 
11
11
  it 'returns the rounded value to the given precision' do
12
12
  result = Dentaku('ROUNDDOWN(x, y)', x: 1.8453, y: 3)
13
- expect(result).to eq 1.845
13
+ expect(result).to eq(1.845)
14
14
  end
15
15
 
16
16
  it 'returns the rounded value to the given precision, also with strings' do
17
17
  result = Dentaku('ROUNDDOWN(x, y)', x: '1.8453', y: '3')
18
- expect(result).to eq 1.845
18
+ expect(result).to eq(1.845)
19
19
  end
20
20
 
21
21
  it 'returns the rounded value to the given precision, also with nil' do
22
22
  result = Dentaku('ROUNDDOWN(x, y)', x: '1.8453', y: nil)
23
- expect(result).to eq 1
23
+ expect(result).to eq(1)
24
+ end
25
+
26
+ context 'checking errors' do
27
+ it 'raises an error if first argument is not numeric' do
28
+ expect { Dentaku!("ROUND(2020-1-1, 0)") }.to raise_error(Dentaku::ArgumentError)
29
+ end
30
+
31
+ it 'raises an error if places is not numeric' do
32
+ expect { Dentaku!("ROUND(1.8, 2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
33
+ end
24
34
  end
25
35
  end
@@ -5,21 +5,31 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Round' do
6
6
  it 'returns the rounded value' do
7
7
  result = Dentaku('ROUNDUP(1.8)')
8
- expect(result).to eq 2
8
+ expect(result).to eq(2)
9
9
  end
10
10
 
11
11
  it 'returns the rounded value to the given precision' do
12
12
  result = Dentaku('ROUNDUP(x, y)', x: 1.8453, y: 3)
13
- expect(result).to eq 1.846
13
+ expect(result).to eq(1.846)
14
14
  end
15
15
 
16
16
  it 'returns the rounded value to the given precision, also with strings' do
17
17
  result = Dentaku('ROUNDUP(x, y)', x: '1.8453', y: '3')
18
- expect(result).to eq 1.846
18
+ expect(result).to eq(1.846)
19
19
  end
20
20
 
21
21
  it 'returns the rounded value to the given precision, also with nil' do
22
22
  result = Dentaku('ROUNDUP(x, y)', x: '1.8453', y: nil)
23
- expect(result).to eq 2
23
+ expect(result).to eq(2)
24
+ end
25
+
26
+ context 'checking errors' do
27
+ it 'raises an error if first argument is not numeric' do
28
+ expect { Dentaku!("ROUND(2020-1-1, 0)") }.to raise_error(Dentaku::ArgumentError)
29
+ end
30
+
31
+ it 'raises an error if places is not numeric' do
32
+ expect { Dentaku!("ROUND(1.8, 2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
33
+ end
24
34
  end
25
35
  end