dentaku 3.3.3 → 3.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -7
- data/.travis.yml +4 -4
- data/CHANGELOG.md +34 -2
- data/README.md +4 -2
- data/dentaku.gemspec +1 -1
- data/lib/dentaku.rb +16 -5
- data/lib/dentaku/ast.rb +4 -0
- data/lib/dentaku/ast/access.rb +3 -1
- data/lib/dentaku/ast/arithmetic.rb +7 -2
- data/lib/dentaku/ast/array.rb +3 -1
- data/lib/dentaku/ast/case/case_else.rb +12 -4
- data/lib/dentaku/ast/case/case_switch_variable.rb +8 -0
- data/lib/dentaku/ast/case/case_then.rb +12 -4
- data/lib/dentaku/ast/case/case_when.rb +12 -4
- data/lib/dentaku/ast/function.rb +10 -1
- data/lib/dentaku/ast/functions/all.rb +36 -0
- data/lib/dentaku/ast/functions/any.rb +36 -0
- data/lib/dentaku/ast/functions/avg.rb +2 -2
- data/lib/dentaku/ast/functions/filter.rb +36 -0
- data/lib/dentaku/ast/functions/map.rb +36 -0
- data/lib/dentaku/ast/functions/pluck.rb +29 -0
- data/lib/dentaku/ast/functions/round.rb +1 -1
- data/lib/dentaku/ast/functions/rounddown.rb +1 -1
- data/lib/dentaku/ast/functions/roundup.rb +1 -1
- data/lib/dentaku/ast/functions/ruby_math.rb +49 -3
- data/lib/dentaku/ast/functions/string_functions.rb +52 -4
- data/lib/dentaku/ast/grouping.rb +3 -1
- data/lib/dentaku/ast/identifier.rb +6 -4
- data/lib/dentaku/bulk_expression_solver.rb +36 -25
- data/lib/dentaku/calculator.rb +14 -6
- data/lib/dentaku/date_arithmetic.rb +1 -1
- data/lib/dentaku/exceptions.rb +3 -3
- data/lib/dentaku/flat_hash.rb +7 -0
- data/lib/dentaku/parser.rb +2 -1
- data/lib/dentaku/tokenizer.rb +1 -1
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +19 -5
- data/spec/ast/avg_spec.rb +4 -0
- data/spec/ast/filter_spec.rb +18 -0
- data/spec/ast/map_spec.rb +15 -0
- data/spec/ast/max_spec.rb +13 -0
- data/spec/ast/min_spec.rb +13 -0
- data/spec/ast/mul_spec.rb +5 -0
- data/spec/ast/negation_spec.rb +18 -2
- data/spec/ast/round_spec.rb +10 -0
- data/spec/ast/rounddown_spec.rb +10 -0
- data/spec/ast/roundup_spec.rb +10 -0
- data/spec/ast/string_functions_spec.rb +35 -0
- data/spec/ast/sum_spec.rb +5 -0
- data/spec/bulk_expression_solver_spec.rb +27 -0
- data/spec/calculator_spec.rb +130 -0
- data/spec/dentaku_spec.rb +14 -8
- data/spec/parser_spec.rb +13 -0
- data/spec/tokenizer_spec.rb +24 -5
- metadata +26 -3
data/lib/dentaku/calculator.rb
CHANGED
@@ -59,10 +59,10 @@ module Dentaku
|
|
59
59
|
store(data) do
|
60
60
|
node = expression
|
61
61
|
node = ast(node) unless node.is_a?(AST::Node)
|
62
|
-
unbound = node.dependencies
|
62
|
+
unbound = node.dependencies(memory)
|
63
63
|
unless unbound.empty?
|
64
64
|
raise UnboundVariableError.new(unbound),
|
65
|
-
"no value provided for variables: #{unbound.join(', ')}"
|
65
|
+
"no value provided for variables: #{unbound.uniq.join(', ')}"
|
66
66
|
end
|
67
67
|
node.value(memory)
|
68
68
|
end
|
@@ -77,13 +77,21 @@ module Dentaku
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def dependencies(expression, context = {})
|
80
|
-
|
81
|
-
|
80
|
+
test_context = context.nil? ? {} : store(context) { memory }
|
81
|
+
|
82
|
+
case expression
|
83
|
+
when Dentaku::AST::Node
|
84
|
+
expression.dependencies(test_context)
|
85
|
+
when Array
|
86
|
+
expression.flat_map { |e| dependencies(e, context) }
|
87
|
+
else
|
88
|
+
ast(expression).dependencies(test_context)
|
82
89
|
end
|
83
|
-
store(context) { ast(expression).dependencies(memory) }
|
84
90
|
end
|
85
91
|
|
86
92
|
def ast(expression)
|
93
|
+
return expression.map { |e| ast(e) } if expression.is_a? Array
|
94
|
+
|
87
95
|
@ast_cache.fetch(expression) {
|
88
96
|
options = {
|
89
97
|
case_sensitive: case_sensitive,
|
@@ -119,7 +127,7 @@ module Dentaku
|
|
119
127
|
restore = Hash[memory]
|
120
128
|
|
121
129
|
if value.nil?
|
122
|
-
key_or_hash = FlatHash.
|
130
|
+
key_or_hash = FlatHash.from_hash_with_intermediates(key_or_hash) if nested_data_support
|
123
131
|
key_or_hash.each do |key, val|
|
124
132
|
memory[standardize_case(key.to_s)] = val
|
125
133
|
end
|
data/lib/dentaku/exceptions.rb
CHANGED
@@ -44,7 +44,7 @@ module Dentaku
|
|
44
44
|
raise ::ArgumentError, "Unhandled #{reason}"
|
45
45
|
end
|
46
46
|
|
47
|
-
new
|
47
|
+
new(reason, **meta)
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -68,7 +68,7 @@ module Dentaku
|
|
68
68
|
raise ::ArgumentError, "Unhandled #{reason}"
|
69
69
|
end
|
70
70
|
|
71
|
-
new
|
71
|
+
new(reason, **meta)
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
@@ -92,7 +92,7 @@ module Dentaku
|
|
92
92
|
raise ::ArgumentError, "Unhandled #{reason}"
|
93
93
|
end
|
94
94
|
|
95
|
-
new
|
95
|
+
new(reason, **meta)
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
data/lib/dentaku/flat_hash.rb
CHANGED
@@ -6,6 +6,13 @@ module Dentaku
|
|
6
6
|
flatten_keys(acc)
|
7
7
|
end
|
8
8
|
|
9
|
+
def self.from_hash_with_intermediates(h, key = [], acc = {})
|
10
|
+
acc.update(key => h) unless key.empty?
|
11
|
+
return unless h.is_a? Hash
|
12
|
+
h.each { |k, v| from_hash_with_intermediates(v, key + [k], acc) }
|
13
|
+
flatten_keys(acc)
|
14
|
+
end
|
15
|
+
|
9
16
|
def self.flatten_keys(hash)
|
10
17
|
hash.each_with_object({}) do |(k, v), h|
|
11
18
|
h[flatten_key(k)] = v
|
data/lib/dentaku/parser.rb
CHANGED
@@ -51,6 +51,7 @@ module Dentaku
|
|
51
51
|
fail! :too_many_operands, operator: operator, expect: max_size, actual: output.length
|
52
52
|
end
|
53
53
|
|
54
|
+
fail! :invalid_statement if output.size < args_size
|
54
55
|
args = Array.new(args_size) { output.pop }.reverse
|
55
56
|
|
56
57
|
output.push operator.new(*args)
|
@@ -335,7 +336,7 @@ module Dentaku
|
|
335
336
|
raise ::ArgumentError, "Unhandled #{reason}"
|
336
337
|
end
|
337
338
|
|
338
|
-
raise ParseError.for(reason, meta), message
|
339
|
+
raise ParseError.for(reason, **meta), message
|
339
340
|
end
|
340
341
|
end
|
341
342
|
end
|
data/lib/dentaku/tokenizer.rb
CHANGED
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/arithmetic_spec.rb
CHANGED
@@ -4,11 +4,12 @@ require 'dentaku/ast/arithmetic'
|
|
4
4
|
require 'dentaku/token'
|
5
5
|
|
6
6
|
describe Dentaku::AST::Arithmetic do
|
7
|
-
let(:one)
|
8
|
-
let(:two)
|
9
|
-
let(:x)
|
10
|
-
let(:y)
|
11
|
-
let(:ctx)
|
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
|
+
let(:date) { Dentaku::AST::DateTime.new Dentaku::Token.new(:datetime, DateTime.new(2020, 4, 16)) }
|
12
13
|
|
13
14
|
it 'performs an arithmetic operation with numeric operands' do
|
14
15
|
expect(add(one, two)).to eq(3)
|
@@ -46,6 +47,19 @@ describe Dentaku::AST::Arithmetic do
|
|
46
47
|
expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
|
47
48
|
end
|
48
49
|
|
50
|
+
it 'performs arithmetic on arrays' do
|
51
|
+
expect(add(x, y, 'x' => [1], 'y' => [2])).to eq([1, 2])
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'performs date arithmetic' do
|
55
|
+
expect(add(date, one)).to eq(DateTime.new(2020, 4, 17))
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'raises ArgumentError if given individually valid but incompatible arguments' do
|
59
|
+
expect { add(one, date) }.to raise_error(Dentaku::ArgumentError)
|
60
|
+
expect { add(x, one, 'x' => [1]) }.to raise_error(Dentaku::ArgumentError)
|
61
|
+
end
|
62
|
+
|
49
63
|
private
|
50
64
|
|
51
65
|
def add(left, right, context = ctx)
|
data/spec/ast/avg_spec.rb
CHANGED
@@ -29,5 +29,9 @@ describe 'Dentaku::AST::Function::Avg' do
|
|
29
29
|
it 'raises an error if no arguments are passed' do
|
30
30
|
expect { calculator.evaluate!('AVG()') }.to raise_error(Dentaku::ArgumentError)
|
31
31
|
end
|
32
|
+
|
33
|
+
it 'raises an error if an empty array is passed' do
|
34
|
+
expect { calculator.evaluate!('AVG(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
|
35
|
+
end
|
32
36
|
end
|
33
37
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/filter'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe Dentaku::AST::Filter do
|
6
|
+
it 'excludes unmatched values' do
|
7
|
+
result = Dentaku('SUM(FILTER(vals, val, val > 1))', vals: [1, 2, 3])
|
8
|
+
expect(result).to eq(5)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'works with a single value if needed for some reason' do
|
12
|
+
result = Dentaku('FILTER(vals, val, val > 1)', vals: 1)
|
13
|
+
expect(result).to eq([])
|
14
|
+
|
15
|
+
result = Dentaku('FILTER(vals, val, val > 1)', vals: 2)
|
16
|
+
expect(result).to eq([2])
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/map'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe Dentaku::AST::Map do
|
6
|
+
it 'operates on each value in an array' do
|
7
|
+
result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
|
8
|
+
expect(result).to eq(9)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'works with an empty array' do
|
12
|
+
result = Dentaku('MAP(vals, val, val + 1)', vals: [])
|
13
|
+
expect(result).to eq([])
|
14
|
+
end
|
15
|
+
end
|
data/spec/ast/max_spec.rb
CHANGED
@@ -17,4 +17,17 @@ describe 'Dentaku::AST::Function::Max' do
|
|
17
17
|
result = Dentaku('MAX(1, x, 1.8)', x: [1.5, 2.3, 1.7])
|
18
18
|
expect(result).to eq(2.3)
|
19
19
|
end
|
20
|
+
|
21
|
+
it 'returns the largest value if only an Array is passed' do
|
22
|
+
result = Dentaku('MAX(x)', x: [1.5, 2.3, 1.7])
|
23
|
+
expect(result).to eq(2.3)
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'checking errors' do
|
27
|
+
let(:calculator) { Dentaku::Calculator.new }
|
28
|
+
|
29
|
+
it 'does not raise an error if an empty array is passed' do
|
30
|
+
expect(calculator.evaluate!('MAX(x)', x: [])).to eq(nil)
|
31
|
+
end
|
32
|
+
end
|
20
33
|
end
|
data/spec/ast/min_spec.rb
CHANGED
@@ -17,4 +17,17 @@ describe 'Dentaku::AST::Function::Min' do
|
|
17
17
|
result = Dentaku('MIN(1, x, 1.8)', x: [1.5, 0.3, 1.7])
|
18
18
|
expect(result).to eq(0.3)
|
19
19
|
end
|
20
|
+
|
21
|
+
it 'returns the smallest value if only an Array is passed' do
|
22
|
+
result = Dentaku('MIN(x)', x: [1.5, 2.3, 1.7])
|
23
|
+
expect(result).to eq(1.5)
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'checking errors' do
|
27
|
+
let(:calculator) { Dentaku::Calculator.new }
|
28
|
+
|
29
|
+
it 'does not raise an error if an empty array is passed' do
|
30
|
+
expect(calculator.evaluate!('MIN(x)', x: [])).to eq(nil)
|
31
|
+
end
|
32
|
+
end
|
20
33
|
end
|
data/spec/ast/mul_spec.rb
CHANGED
@@ -34,5 +34,10 @@ describe 'Dentaku::AST::Function::Mul' do
|
|
34
34
|
it 'raises an error if no arguments are passed' do
|
35
35
|
expect { calculator.evaluate!('MUL()') }.to raise_error(Dentaku::ArgumentError)
|
36
36
|
end
|
37
|
+
|
38
|
+
it 'does not raise an error if an empty array is passed' do
|
39
|
+
result = calculator.evaluate!('MUL(x)', x: [])
|
40
|
+
expect(result).to eq(1)
|
41
|
+
end
|
37
42
|
end
|
38
43
|
end
|
data/spec/ast/negation_spec.rb
CHANGED
@@ -4,8 +4,9 @@ require 'dentaku/ast/arithmetic'
|
|
4
4
|
require 'dentaku/token'
|
5
5
|
|
6
6
|
describe Dentaku::AST::Negation do
|
7
|
-
let(:five) { Dentaku::AST::
|
8
|
-
let(:t) { Dentaku::AST::
|
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') }
|
9
10
|
|
10
11
|
it 'allows access to its sub-node' do
|
11
12
|
node = described_class.new(five)
|
@@ -29,4 +30,19 @@ describe Dentaku::AST::Negation do
|
|
29
30
|
described_class.new(group)
|
30
31
|
}.not_to raise_error
|
31
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
|
32
48
|
end
|
data/spec/ast/round_spec.rb
CHANGED
@@ -22,4 +22,14 @@ describe 'Dentaku::AST::Function::Round' do
|
|
22
22
|
result = Dentaku('ROUND(x, y)', x: '1.8453', y: nil)
|
23
23
|
expect(result).to eq(2)
|
24
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
|
34
|
+
end
|
25
35
|
end
|
data/spec/ast/rounddown_spec.rb
CHANGED
@@ -22,4 +22,14 @@ describe 'Dentaku::AST::Function::Round' do
|
|
22
22
|
result = Dentaku('ROUNDDOWN(x, y)', x: '1.8453', y: nil)
|
23
23
|
expect(result).to eq(1)
|
24
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
|
34
|
+
end
|
25
35
|
end
|
data/spec/ast/roundup_spec.rb
CHANGED
@@ -22,4 +22,14 @@ describe 'Dentaku::AST::Function::Round' do
|
|
22
22
|
result = Dentaku('ROUNDUP(x, y)', x: '1.8453', y: nil)
|
23
23
|
expect(result).to eq(2)
|
24
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
|
34
|
+
end
|
25
35
|
end
|
@@ -26,6 +26,10 @@ describe Dentaku::AST::StringFunctions::Left do
|
|
26
26
|
expect(subject.value('string' => 'abcdefg', 'length' => 40)).to eq 'abcdefg'
|
27
27
|
end
|
28
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
|
+
|
29
33
|
it 'has the proper type' do
|
30
34
|
expect(subject.type).to eq(:string)
|
31
35
|
end
|
@@ -35,6 +39,12 @@ describe Dentaku::AST::StringFunctions::Left do
|
|
35
39
|
subject.value('string' => 'abcdefg', 'length' => -2)
|
36
40
|
}.to raise_error(Dentaku::ArgumentError, /LEFT\(\) requires length to be positive/)
|
37
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
|
38
48
|
end
|
39
49
|
|
40
50
|
describe Dentaku::AST::StringFunctions::Right do
|
@@ -53,9 +63,19 @@ describe Dentaku::AST::StringFunctions::Right do
|
|
53
63
|
expect(subject.value).to eq 'abcdefg'
|
54
64
|
end
|
55
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
|
+
|
56
71
|
it 'has the proper type' do
|
57
72
|
expect(subject.type).to eq(:string)
|
58
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
|
59
79
|
end
|
60
80
|
|
61
81
|
describe Dentaku::AST::StringFunctions::Mid do
|
@@ -79,9 +99,24 @@ describe Dentaku::AST::StringFunctions::Mid do
|
|
79
99
|
expect(subject.value).to eq 'defg'
|
80
100
|
end
|
81
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
|
+
|
82
107
|
it 'has the proper type' do
|
83
108
|
expect(subject.type).to eq(:string)
|
84
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
|
85
120
|
end
|
86
121
|
|
87
122
|
describe Dentaku::AST::StringFunctions::Len do
|
data/spec/ast/sum_spec.rb
CHANGED
@@ -34,5 +34,10 @@ describe 'Dentaku::AST::Function::Sum' do
|
|
34
34
|
it 'raises an error if no arguments are passed' do
|
35
35
|
expect { calculator.evaluate!('SUM()') }.to raise_error(Dentaku::ArgumentError)
|
36
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)
|
41
|
+
end
|
37
42
|
end
|
38
43
|
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: {
|