dentaku 3.2.0 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +5 -10
  3. data/.travis.yml +4 -6
  4. data/CHANGELOG.md +86 -2
  5. data/README.md +7 -6
  6. data/dentaku.gemspec +1 -1
  7. data/lib/dentaku/ast/access.rb +21 -1
  8. data/lib/dentaku/ast/arithmetic.rb +51 -15
  9. data/lib/dentaku/ast/array.rb +41 -0
  10. data/lib/dentaku/ast/bitwise.rb +30 -5
  11. data/lib/dentaku/ast/case/case_conditional.rb +17 -2
  12. data/lib/dentaku/ast/case/case_else.rb +17 -3
  13. data/lib/dentaku/ast/case/case_switch_variable.rb +14 -0
  14. data/lib/dentaku/ast/case/case_then.rb +17 -3
  15. data/lib/dentaku/ast/case/case_when.rb +21 -3
  16. data/lib/dentaku/ast/case.rb +19 -3
  17. data/lib/dentaku/ast/comparators.rb +38 -28
  18. data/lib/dentaku/ast/function.rb +11 -3
  19. data/lib/dentaku/ast/function_registry.rb +21 -0
  20. data/lib/dentaku/ast/functions/all.rb +23 -0
  21. data/lib/dentaku/ast/functions/and.rb +2 -2
  22. data/lib/dentaku/ast/functions/any.rb +23 -0
  23. data/lib/dentaku/ast/functions/avg.rb +2 -2
  24. data/lib/dentaku/ast/functions/count.rb +8 -0
  25. data/lib/dentaku/ast/functions/duration.rb +51 -0
  26. data/lib/dentaku/ast/functions/enum.rb +37 -0
  27. data/lib/dentaku/ast/functions/filter.rb +23 -0
  28. data/lib/dentaku/ast/functions/if.rb +19 -2
  29. data/lib/dentaku/ast/functions/map.rb +23 -0
  30. data/lib/dentaku/ast/functions/or.rb +4 -4
  31. data/lib/dentaku/ast/functions/pluck.rb +30 -0
  32. data/lib/dentaku/ast/functions/round.rb +1 -1
  33. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  34. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  35. data/lib/dentaku/ast/functions/ruby_math.rb +50 -3
  36. data/lib/dentaku/ast/functions/string_functions.rb +105 -12
  37. data/lib/dentaku/ast/functions/xor.rb +44 -0
  38. data/lib/dentaku/ast/grouping.rb +3 -1
  39. data/lib/dentaku/ast/identifier.rb +16 -4
  40. data/lib/dentaku/ast/literal.rb +10 -0
  41. data/lib/dentaku/ast/negation.rb +7 -1
  42. data/lib/dentaku/ast/nil.rb +4 -0
  43. data/lib/dentaku/ast/node.rb +8 -0
  44. data/lib/dentaku/ast/operation.rb +17 -0
  45. data/lib/dentaku/ast/string.rb +7 -0
  46. data/lib/dentaku/ast.rb +8 -0
  47. data/lib/dentaku/bulk_expression_solver.rb +38 -27
  48. data/lib/dentaku/calculator.rb +21 -8
  49. data/lib/dentaku/date_arithmetic.rb +45 -0
  50. data/lib/dentaku/exceptions.rb +11 -8
  51. data/lib/dentaku/flat_hash.rb +9 -2
  52. data/lib/dentaku/parser.rb +57 -16
  53. data/lib/dentaku/print_visitor.rb +101 -0
  54. data/lib/dentaku/token_matcher.rb +1 -1
  55. data/lib/dentaku/token_scanner.rb +9 -3
  56. data/lib/dentaku/tokenizer.rb +7 -2
  57. data/lib/dentaku/version.rb +1 -1
  58. data/lib/dentaku/visitor/infix.rb +82 -0
  59. data/lib/dentaku.rb +20 -7
  60. data/spec/ast/addition_spec.rb +7 -1
  61. data/spec/ast/all_spec.rb +25 -0
  62. data/spec/ast/and_function_spec.rb +6 -6
  63. data/spec/ast/and_spec.rb +1 -1
  64. data/spec/ast/any_spec.rb +23 -0
  65. data/spec/ast/arithmetic_spec.rb +64 -29
  66. data/spec/ast/avg_spec.rb +9 -5
  67. data/spec/ast/comparator_spec.rb +31 -1
  68. data/spec/ast/count_spec.rb +7 -7
  69. data/spec/ast/division_spec.rb +7 -1
  70. data/spec/ast/filter_spec.rb +25 -0
  71. data/spec/ast/function_spec.rb +20 -15
  72. data/spec/ast/map_spec.rb +27 -0
  73. data/spec/ast/max_spec.rb +16 -3
  74. data/spec/ast/min_spec.rb +16 -3
  75. data/spec/ast/mul_spec.rb +11 -6
  76. data/spec/ast/negation_spec.rb +48 -0
  77. data/spec/ast/node_spec.rb +11 -8
  78. data/spec/ast/numeric_spec.rb +1 -1
  79. data/spec/ast/or_spec.rb +7 -7
  80. data/spec/ast/pluck_spec.rb +32 -0
  81. data/spec/ast/round_spec.rb +14 -4
  82. data/spec/ast/rounddown_spec.rb +14 -4
  83. data/spec/ast/roundup_spec.rb +14 -4
  84. data/spec/ast/string_functions_spec.rb +73 -0
  85. data/spec/ast/sum_spec.rb +11 -6
  86. data/spec/ast/switch_spec.rb +5 -5
  87. data/spec/ast/xor_spec.rb +35 -0
  88. data/spec/bulk_expression_solver_spec.rb +37 -1
  89. data/spec/calculator_spec.rb +341 -32
  90. data/spec/dentaku_spec.rb +19 -6
  91. data/spec/external_function_spec.rb +32 -6
  92. data/spec/parser_spec.rb +100 -123
  93. data/spec/print_visitor_spec.rb +66 -0
  94. data/spec/spec_helper.rb +6 -4
  95. data/spec/token_matcher_spec.rb +8 -8
  96. data/spec/token_scanner_spec.rb +4 -4
  97. data/spec/tokenizer_spec.rb +56 -13
  98. data/spec/visitor/infix_spec.rb +31 -0
  99. data/spec/visitor_spec.rb +138 -0
  100. metadata +52 -7
data/spec/ast/or_spec.rb CHANGED
@@ -6,30 +6,30 @@ describe 'Dentaku::AST::Or' do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
7
 
8
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
9
+ result = Dentaku('OR(1 = "2", 0 = 1)')
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
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/pluck'
3
+ require 'dentaku'
4
+
5
+ describe Dentaku::AST::Pluck do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+ it 'operates on each value in an array' do
8
+ result = Dentaku('PLUCK(users, age)', users: [
9
+ {name: "Bob", age: 44},
10
+ {name: "Jane", age: 27}
11
+ ])
12
+ expect(result).to eq([44, 27])
13
+ end
14
+
15
+ it 'works with an empty array' do
16
+ result = Dentaku('PLUCK(users, age)', users: [])
17
+ expect(result).to eq([])
18
+ end
19
+
20
+ it 'raises argument error if a string is passed as identifier' do
21
+ expect do Dentaku.evaluate!('PLUCK(users, "age")', users: [
22
+ {name: "Bob", age: 44},
23
+ {name: "Jane", age: 27}
24
+ ]) end.to raise_error(Dentaku::ArgumentError, 'PLUCK() requires second argument to be an identifier')
25
+ end
26
+
27
+ it 'raises argument error if a non array of hashes is passed as collection' do
28
+ expect { calculator.evaluate!('PLUCK({1, 2, 3}, age)') }.to raise_error(
29
+ Dentaku::ArgumentError, 'PLUCK() requires first argument to be an array of hashes'
30
+ )
31
+ end
32
+ 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
@@ -25,6 +25,26 @@ describe Dentaku::AST::StringFunctions::Left do
25
25
  it 'handles size greater than input string length correctly' do
26
26
  expect(subject.value('string' => 'abcdefg', 'length' => 40)).to eq 'abcdefg'
27
27
  end
28
+
29
+ it 'accepts strings as length if they can be parsed to a number' do
30
+ expect(subject.value('string' => 'ABCDEFG', 'length' => '4')).to eq 'ABCD'
31
+ end
32
+
33
+ it 'has the proper type' do
34
+ expect(subject.type).to eq(:string)
35
+ end
36
+
37
+ it 'raises an error if given invalid length' do
38
+ expect {
39
+ subject.value('string' => 'abcdefg', 'length' => -2)
40
+ }.to raise_error(Dentaku::ArgumentError, /LEFT\(\) requires length to be positive/)
41
+ end
42
+
43
+ it 'raises an error when given a junk length' do
44
+ expect {
45
+ subject.value('string' => 'abcdefg', 'length' => 'junk')
46
+ }.to raise_error(Dentaku::ArgumentError, "'junk' is not coercible to numeric")
47
+ end
28
48
  end
29
49
 
30
50
  describe Dentaku::AST::StringFunctions::Right do
@@ -42,6 +62,20 @@ describe Dentaku::AST::StringFunctions::Right do
42
62
  subject = described_class.new(literal('abcdefg'), literal(40))
43
63
  expect(subject.value).to eq 'abcdefg'
44
64
  end
65
+
66
+ it 'accepts strings as length if they can be parsed to a number' do
67
+ subject = described_class.new(literal('ABCDEFG'), literal('4'))
68
+ expect(subject.value).to eq 'DEFG'
69
+ end
70
+
71
+ it 'has the proper type' do
72
+ expect(subject.type).to eq(:string)
73
+ end
74
+
75
+ it 'raises an error when given a junk length' do
76
+ subject = described_class.new(literal('abcdefg'), literal('junk'))
77
+ expect { subject.value }.to raise_error(Dentaku::ArgumentError, "'junk' is not coercible to numeric")
78
+ end
45
79
  end
46
80
 
47
81
  describe Dentaku::AST::StringFunctions::Mid do
@@ -64,6 +98,25 @@ describe Dentaku::AST::StringFunctions::Mid do
64
98
  subject = described_class.new(literal('abcdefg'), literal(4), literal(40))
65
99
  expect(subject.value).to eq 'defg'
66
100
  end
101
+
102
+ it 'accepts strings as offset and length if they can be parsed to a number' do
103
+ subject = described_class.new(literal('ABCDEFG'), literal('4'), literal('2'))
104
+ expect(subject.value).to eq 'DE'
105
+ end
106
+
107
+ it 'has the proper type' do
108
+ expect(subject.type).to eq(:string)
109
+ end
110
+
111
+ it 'raises an error when given a junk offset' do
112
+ subject = described_class.new(literal('abcdefg'), literal('junk offset'), literal(2))
113
+ expect { subject.value }.to raise_error(Dentaku::ArgumentError, "'junk offset' is not coercible to numeric")
114
+ end
115
+
116
+ it 'raises an error when given a junk length' do
117
+ subject = described_class.new(literal('abcdefg'), literal(4), literal('junk'))
118
+ expect { subject.value }.to raise_error(Dentaku::ArgumentError, "'junk' is not coercible to numeric")
119
+ end
67
120
  end
68
121
 
69
122
  describe Dentaku::AST::StringFunctions::Len do
@@ -76,6 +129,10 @@ describe Dentaku::AST::StringFunctions::Len do
76
129
  subject = described_class.new(literal(''))
77
130
  expect(subject.value).to eq 0
78
131
  end
132
+
133
+ it 'has the proper type' do
134
+ expect(subject.type).to eq(:numeric)
135
+ end
79
136
  end
80
137
 
81
138
  describe Dentaku::AST::StringFunctions::Find do
@@ -93,6 +150,10 @@ describe Dentaku::AST::StringFunctions::Find do
93
150
  subject = described_class.new(literal('DE'), literal(''))
94
151
  expect(subject.value).to be_nil
95
152
  end
153
+
154
+ it 'has the proper type' do
155
+ expect(subject.type).to eq(:numeric)
156
+ end
96
157
  end
97
158
 
98
159
  describe Dentaku::AST::StringFunctions::Substitute do
@@ -110,6 +171,10 @@ describe Dentaku::AST::StringFunctions::Substitute do
110
171
  subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal(''))
111
172
  expect(subject.value).to eq 'ABCFG'
112
173
  end
174
+
175
+ it 'has the proper type' do
176
+ expect(subject.type).to eq(:string)
177
+ end
113
178
  end
114
179
 
115
180
  describe Dentaku::AST::StringFunctions::Concat do
@@ -132,6 +197,10 @@ describe Dentaku::AST::StringFunctions::Concat do
132
197
  subject = described_class.new(literal(''), literal(''))
133
198
  expect(subject.value).to eq ''
134
199
  end
200
+
201
+ it 'has the proper type' do
202
+ expect(subject.type).to eq(:string)
203
+ end
135
204
  end
136
205
 
137
206
  describe Dentaku::AST::StringFunctions::Contains do
@@ -141,4 +210,8 @@ describe Dentaku::AST::StringFunctions::Contains do
141
210
  subject = described_class.new(literal('app'), literal('orange'))
142
211
  expect(subject.value).to be_falsy
143
212
  end
213
+
214
+ it 'has the proper type' do
215
+ expect(subject.type).to eq(:logical)
216
+ end
144
217
  end
data/spec/ast/sum_spec.rb CHANGED
@@ -5,34 +5,39 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Sum' do
6
6
  it 'returns the sum of an array of Numeric values' do
7
7
  result = Dentaku('SUM(1, x, 1.8)', x: 2.3)
8
- expect(result).to eq 5.1
8
+ expect(result).to eq(5.1)
9
9
  end
10
10
 
11
11
  it 'returns the sum of a single entry array of a Numeric value' do
12
12
  result = Dentaku('SUM(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 'returns the sum even if a String is passed' do
17
17
  result = Dentaku('SUM(1, x, 1.8)', x: '2.3')
18
- expect(result).to eq 5.1
18
+ expect(result).to eq(5.1)
19
19
  end
20
20
 
21
21
  it 'returns the sum even if an array is passed' do
22
22
  result = Dentaku('SUM(1, x, 2.3)', x: [4, 5])
23
- expect(result).to eq 12.3
23
+ expect(result).to eq(12.3)
24
24
  end
25
25
 
26
26
  it 'returns the sum of nested sums' do
27
27
  result = Dentaku('SUM(1, x, SUM(4, 5))', x: '2.3')
28
- expect(result).to eq 12.3
28
+ expect(result).to eq(12.3)
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!('SUM()') }.to raise_error(ArgumentError)
35
+ expect { calculator.evaluate!('SUM()') }.to raise_error(Dentaku::ArgumentError)
36
+ end
37
+
38
+ it 'does not raise an error if an empty array is passed' do
39
+ result = calculator.evaluate!('SUM(x)', x: [])
40
+ expect(result).to eq(0)
36
41
  end
37
42
  end
38
43
  end
@@ -5,26 +5,26 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Switch' do
6
6
  it 'returns the match if present in argumtents' do
7
7
  result = Dentaku('SWITCH(1, 1, "one", 2, "two")')
8
- expect(result).to eq 'one'
8
+ expect(result).to eq('one')
9
9
  end
10
10
 
11
11
  it 'returns nil if no match was found' do
12
12
  result = Dentaku('SWITCH(3, 1, "one", 2, "two")')
13
- expect(result).to eq nil
13
+ expect(result).to eq(nil)
14
14
  end
15
15
 
16
16
  it 'returns the default value if present and no match was found' do
17
17
  result = Dentaku('SWITCH(3, 1, "one", 2, "two", "no match")')
18
- expect(result).to eq 'no match'
18
+ expect(result).to eq('no match')
19
19
  end
20
20
 
21
21
  it 'returns the first match if multiple matches exist' do
22
22
  result = Dentaku('SWITCH(1, 1, "one", 2, "two", 1, "three")')
23
- expect(result).to eq 'one'
23
+ expect(result).to eq('one')
24
24
  end
25
25
 
26
26
  it 'does not return a match where a value matches the search value' do
27
27
  result = Dentaku('SWITCH(1, "one", 1, 2, "two", 3)')
28
- expect(result).to eq 3
28
+ expect(result).to eq(3)
29
29
  end
30
30
  end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/ast/functions/or'
4
+
5
+ describe 'Dentaku::AST::Xor' do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+
8
+ it 'returns false if all of the arguments are false' do
9
+ result = Dentaku('XOR(false, false)')
10
+ expect(result).to eq(false)
11
+ end
12
+
13
+ it 'returns true if only one of the arguments is true' do
14
+ result = Dentaku('XOR(false, true)')
15
+ expect(result).to eq(true)
16
+ end
17
+
18
+ it 'returns false if more than one of the arguments is true' do
19
+ result = Dentaku('XOR(false, true, true)')
20
+ expect(result).to eq(false)
21
+ end
22
+
23
+ it 'supports nested expressions' do
24
+ result = Dentaku('XOR(y = 1, x = 1)', x: 1, y: 2)
25
+ expect(result).to eq(true)
26
+ end
27
+
28
+ it 'raises an error if no arguments are passed' do
29
+ expect { calculator.evaluate!('XOR()') }.to raise_error(Dentaku::ParseError)
30
+ end
31
+
32
+ it 'raises an error if a non logical argument is passed' do
33
+ expect { calculator.evaluate!('XOR("r")') }.to raise_error(Dentaku::ArgumentError)
34
+ end
35
+ end
@@ -26,6 +26,13 @@ RSpec.describe Dentaku::BulkExpressionSolver do
26
26
  }.to raise_error(Dentaku::UnboundVariableError)
27
27
  end
28
28
 
29
+ it "properly handles access on an unbound variable" do
30
+ expressions = {more_apples: "apples[0]"}
31
+ expect {
32
+ described_class.new(expressions, calculator).solve!
33
+ }.to raise_error(Dentaku::UnboundVariableError)
34
+ end
35
+
29
36
  it "lets you know if the result is a div/0 error" do
30
37
  expressions = {more_apples: "1/0"}
31
38
  expect {
@@ -39,6 +46,26 @@ RSpec.describe Dentaku::BulkExpressionSolver do
39
46
  expect(solver.solve!).to eq("the value of x, incremented" => 4)
40
47
  end
41
48
 
49
+ it "allows self-referential formulas" do
50
+ expressions = { x: "x + 1" }
51
+ solver = described_class.new(expressions, calculator.store(x: 1))
52
+ expect(solver.solve!).to eq(x: 2)
53
+
54
+ expressions = { x: "y + 3", y: "x * 2" }
55
+ solver = described_class.new(expressions, calculator.store(x: 5, y: 3))
56
+ expect(solver.solve!).to eq(x: 6, y: 12) # x = 6 by the time y is calculated
57
+ end
58
+
59
+ it "does not execute functions unnecessarily" do
60
+ calls = 0
61
+ external = ->() { calls += 1 }
62
+ hash = {test: 'EXTERNAL()'}
63
+ calculator = Dentaku::Calculator.new
64
+ calculator.add_function(:external, :numeric, external)
65
+ calculator.solve(hash)
66
+ expect(calls).to eq(1)
67
+ end
68
+
42
69
  it "evaluates expressions in hashes and arrays, and expands the results" do
43
70
  calculator.store(
44
71
  fruit_quantities: {
@@ -116,6 +143,15 @@ RSpec.describe Dentaku::BulkExpressionSolver do
116
143
  expect(exception.recipient_variable).to eq('more_apples')
117
144
  end
118
145
 
146
+ it 'stores the recipient variable on the exception when there is an ArgumentError' do
147
+ expressions = {apples: "NULL", more_apples: "1 + apples"}
148
+ exception = nil
149
+ described_class.new(expressions, calculator).solve do |ex|
150
+ exception = ex
151
+ end
152
+ expect(exception.recipient_variable).to eq('more_apples')
153
+ end
154
+
119
155
  it 'safely handles argument errors' do
120
156
  expressions = {i: "a / 5 + d", a: "m * 12", d: "a + b"}
121
157
  result = described_class.new(expressions, calculator.store(m: 3)).solve
@@ -137,7 +173,7 @@ RSpec.describe Dentaku::BulkExpressionSolver do
137
173
  f: "e[0] + 1"
138
174
  }
139
175
  results = described_class.new(expressions, calculator).solve
140
- expect(results[:f]).to eq 10
176
+ expect(results[:f]).to eq(10)
141
177
  end
142
178
 
143
179
  it 'uses stored values for expressions when they are known' do