hayadentaku 3.5.7

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 (132) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rspec.yml +26 -0
  3. data/.github/workflows/rubocop.yml +14 -0
  4. data/.gitignore +14 -0
  5. data/.pryrc +2 -0
  6. data/.rubocop.yml +114 -0
  7. data/.travis.yml +10 -0
  8. data/CHANGELOG.md +328 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE +21 -0
  11. data/README.md +352 -0
  12. data/Rakefile +31 -0
  13. data/hayadentaku.gemspec +35 -0
  14. data/lib/dentaku/ast/access.rb +44 -0
  15. data/lib/dentaku/ast/arithmetic.rb +292 -0
  16. data/lib/dentaku/ast/array.rb +38 -0
  17. data/lib/dentaku/ast/bitwise.rb +42 -0
  18. data/lib/dentaku/ast/case/case_conditional.rb +38 -0
  19. data/lib/dentaku/ast/case/case_else.rb +35 -0
  20. data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
  21. data/lib/dentaku/ast/case/case_then.rb +35 -0
  22. data/lib/dentaku/ast/case/case_when.rb +39 -0
  23. data/lib/dentaku/ast/case.rb +93 -0
  24. data/lib/dentaku/ast/combinators.rb +50 -0
  25. data/lib/dentaku/ast/comparators.rb +88 -0
  26. data/lib/dentaku/ast/datetime.rb +8 -0
  27. data/lib/dentaku/ast/function.rb +56 -0
  28. data/lib/dentaku/ast/function_registry.rb +107 -0
  29. data/lib/dentaku/ast/functions/abs.rb +5 -0
  30. data/lib/dentaku/ast/functions/all.rb +19 -0
  31. data/lib/dentaku/ast/functions/and.rb +25 -0
  32. data/lib/dentaku/ast/functions/any.rb +19 -0
  33. data/lib/dentaku/ast/functions/avg.rb +13 -0
  34. data/lib/dentaku/ast/functions/count.rb +26 -0
  35. data/lib/dentaku/ast/functions/duration.rb +51 -0
  36. data/lib/dentaku/ast/functions/enum.rb +54 -0
  37. data/lib/dentaku/ast/functions/filter.rb +21 -0
  38. data/lib/dentaku/ast/functions/if.rb +47 -0
  39. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  40. data/lib/dentaku/ast/functions/map.rb +19 -0
  41. data/lib/dentaku/ast/functions/max.rb +5 -0
  42. data/lib/dentaku/ast/functions/min.rb +5 -0
  43. data/lib/dentaku/ast/functions/mul.rb +12 -0
  44. data/lib/dentaku/ast/functions/not.rb +5 -0
  45. data/lib/dentaku/ast/functions/or.rb +25 -0
  46. data/lib/dentaku/ast/functions/pluck.rb +34 -0
  47. data/lib/dentaku/ast/functions/reduce.rb +60 -0
  48. data/lib/dentaku/ast/functions/round.rb +5 -0
  49. data/lib/dentaku/ast/functions/rounddown.rb +8 -0
  50. data/lib/dentaku/ast/functions/roundup.rb +8 -0
  51. data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
  52. data/lib/dentaku/ast/functions/string_functions.rb +212 -0
  53. data/lib/dentaku/ast/functions/sum.rb +12 -0
  54. data/lib/dentaku/ast/functions/switch.rb +8 -0
  55. data/lib/dentaku/ast/functions/xor.rb +44 -0
  56. data/lib/dentaku/ast/grouping.rb +23 -0
  57. data/lib/dentaku/ast/identifier.rb +52 -0
  58. data/lib/dentaku/ast/literal.rb +30 -0
  59. data/lib/dentaku/ast/logical.rb +8 -0
  60. data/lib/dentaku/ast/negation.rb +54 -0
  61. data/lib/dentaku/ast/nil.rb +13 -0
  62. data/lib/dentaku/ast/node.rb +29 -0
  63. data/lib/dentaku/ast/numeric.rb +8 -0
  64. data/lib/dentaku/ast/operation.rb +44 -0
  65. data/lib/dentaku/ast/string.rb +15 -0
  66. data/lib/dentaku/ast.rb +42 -0
  67. data/lib/dentaku/bulk_expression_solver.rb +158 -0
  68. data/lib/dentaku/calculator.rb +192 -0
  69. data/lib/dentaku/date_arithmetic.rb +60 -0
  70. data/lib/dentaku/dependency_resolver.rb +29 -0
  71. data/lib/dentaku/exceptions.rb +116 -0
  72. data/lib/dentaku/flat_hash.rb +161 -0
  73. data/lib/dentaku/parser.rb +318 -0
  74. data/lib/dentaku/print_visitor.rb +112 -0
  75. data/lib/dentaku/string_casing.rb +7 -0
  76. data/lib/dentaku/token.rb +48 -0
  77. data/lib/dentaku/token_matcher.rb +138 -0
  78. data/lib/dentaku/token_matchers.rb +29 -0
  79. data/lib/dentaku/token_scanner.rb +240 -0
  80. data/lib/dentaku/tokenizer.rb +127 -0
  81. data/lib/dentaku/version.rb +3 -0
  82. data/lib/dentaku/visitor/infix.rb +86 -0
  83. data/lib/dentaku.rb +69 -0
  84. data/spec/ast/abs_spec.rb +26 -0
  85. data/spec/ast/addition_spec.rb +67 -0
  86. data/spec/ast/all_spec.rb +38 -0
  87. data/spec/ast/and_function_spec.rb +35 -0
  88. data/spec/ast/and_spec.rb +32 -0
  89. data/spec/ast/any_spec.rb +36 -0
  90. data/spec/ast/arithmetic_spec.rb +147 -0
  91. data/spec/ast/avg_spec.rb +42 -0
  92. data/spec/ast/case_spec.rb +84 -0
  93. data/spec/ast/comparator_spec.rb +87 -0
  94. data/spec/ast/count_spec.rb +40 -0
  95. data/spec/ast/division_spec.rb +64 -0
  96. data/spec/ast/filter_spec.rb +25 -0
  97. data/spec/ast/function_spec.rb +69 -0
  98. data/spec/ast/intercept_spec.rb +30 -0
  99. data/spec/ast/map_spec.rb +40 -0
  100. data/spec/ast/max_spec.rb +33 -0
  101. data/spec/ast/min_spec.rb +33 -0
  102. data/spec/ast/mul_spec.rb +43 -0
  103. data/spec/ast/negation_spec.rb +48 -0
  104. data/spec/ast/node_spec.rb +43 -0
  105. data/spec/ast/numeric_spec.rb +16 -0
  106. data/spec/ast/or_spec.rb +35 -0
  107. data/spec/ast/pluck_spec.rb +49 -0
  108. data/spec/ast/reduce_spec.rb +22 -0
  109. data/spec/ast/round_spec.rb +35 -0
  110. data/spec/ast/rounddown_spec.rb +35 -0
  111. data/spec/ast/roundup_spec.rb +35 -0
  112. data/spec/ast/string_functions_spec.rb +217 -0
  113. data/spec/ast/sum_spec.rb +43 -0
  114. data/spec/ast/switch_spec.rb +30 -0
  115. data/spec/ast/xor_spec.rb +35 -0
  116. data/spec/benchmark.rb +70 -0
  117. data/spec/bulk_expression_solver_spec.rb +241 -0
  118. data/spec/calculator_spec.rb +1003 -0
  119. data/spec/dentaku_spec.rb +52 -0
  120. data/spec/dependency_resolver_spec.rb +18 -0
  121. data/spec/exceptions_spec.rb +9 -0
  122. data/spec/external_function_spec.rb +177 -0
  123. data/spec/parser_spec.rb +183 -0
  124. data/spec/print_visitor_spec.rb +77 -0
  125. data/spec/spec_helper.rb +69 -0
  126. data/spec/token_matcher_spec.rb +134 -0
  127. data/spec/token_scanner_spec.rb +49 -0
  128. data/spec/token_spec.rb +16 -0
  129. data/spec/tokenizer_spec.rb +375 -0
  130. data/spec/visitor/infix_spec.rb +52 -0
  131. data/spec/visitor_spec.rb +139 -0
  132. metadata +353 -0
@@ -0,0 +1,217 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/string_functions'
3
+
4
+ describe Dentaku::AST::StringFunctions::Left do
5
+ let(:string) { identifier('string') }
6
+ let(:length) { identifier('length') }
7
+
8
+ subject { described_class.new(string, length) }
9
+
10
+ it 'returns the left N characters of the string' do
11
+ expect(subject.value('string' => 'ABCDEFG', 'length' => 4)).to eq 'ABCD'
12
+ end
13
+
14
+ it 'works correctly with literals' do
15
+ left = literal('ABCD')
16
+ len = literal(2)
17
+ fn = described_class.new(left, len)
18
+ expect(fn.value).to eq 'AB'
19
+ end
20
+
21
+ it 'handles an empty string correctly' do
22
+ expect(subject.value('string' => '', 'length' => 4)).to eq ''
23
+ end
24
+
25
+ it 'handles size greater than input string length correctly' do
26
+ expect(subject.value('string' => 'abcdefg', 'length' => 40)).to eq 'abcdefg'
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
48
+ end
49
+
50
+ describe Dentaku::AST::StringFunctions::Right do
51
+ it 'returns the right N characters of the string' do
52
+ subject = described_class.new(literal('ABCDEFG'), literal(4))
53
+ expect(subject.value).to eq 'DEFG'
54
+ end
55
+
56
+ it 'handles an empty string correctly' do
57
+ subject = described_class.new(literal(''), literal(4))
58
+ expect(subject.value).to eq ''
59
+ end
60
+
61
+ it 'handles size greater than input string length correctly' do
62
+ subject = described_class.new(literal('abcdefg'), literal(40))
63
+ expect(subject.value).to eq 'abcdefg'
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
79
+ end
80
+
81
+ describe Dentaku::AST::StringFunctions::Mid do
82
+ it 'returns a substring from the middle of the string' do
83
+ subject = described_class.new(literal('ABCDEFG'), literal(4), literal(2))
84
+ expect(subject.value).to eq 'DE'
85
+ end
86
+
87
+ it 'handles an empty string correctly' do
88
+ subject = described_class.new(literal(''), literal(4), literal(2))
89
+ expect(subject.value).to eq ''
90
+ end
91
+
92
+ it 'handles offset greater than input string length correctly' do
93
+ subject = described_class.new(literal('abcdefg'), literal(40), literal(4))
94
+ expect(subject.value).to eq ''
95
+ end
96
+
97
+ it 'handles size greater than input string length correctly' do
98
+ subject = described_class.new(literal('abcdefg'), literal(4), literal(40))
99
+ expect(subject.value).to eq 'defg'
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
120
+ end
121
+
122
+ describe Dentaku::AST::StringFunctions::Len do
123
+ it 'returns the length of a string' do
124
+ subject = described_class.new(literal('ABCDEFG'))
125
+ expect(subject.value).to eq 7
126
+ end
127
+
128
+ it 'handles an empty string correctly' do
129
+ subject = described_class.new(literal(''))
130
+ expect(subject.value).to eq 0
131
+ end
132
+
133
+ it 'has the proper type' do
134
+ expect(subject.type).to eq(:numeric)
135
+ end
136
+ end
137
+
138
+ describe Dentaku::AST::StringFunctions::Find do
139
+ it 'returns the position of a substring within a string' do
140
+ subject = described_class.new(literal('DE'), literal('ABCDEFG'))
141
+ expect(subject.value).to eq 4
142
+ end
143
+
144
+ it 'handles an empty substring correctly' do
145
+ subject = described_class.new(literal(''), literal('ABCDEFG'))
146
+ expect(subject.value).to eq 1
147
+ end
148
+
149
+ it 'handles an empty string correctly' do
150
+ subject = described_class.new(literal('DE'), literal(''))
151
+ expect(subject.value).to be_nil
152
+ end
153
+
154
+ it 'has the proper type' do
155
+ expect(subject.type).to eq(:numeric)
156
+ end
157
+ end
158
+
159
+ describe Dentaku::AST::StringFunctions::Substitute do
160
+ it 'replaces a substring within a string' do
161
+ subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal('xy'))
162
+ expect(subject.value).to eq 'ABCxyFG'
163
+ end
164
+
165
+ it 'handles an empty search string correctly' do
166
+ subject = described_class.new(literal('ABCDEFG'), literal(''), literal('xy'))
167
+ expect(subject.value).to eq 'xyABCDEFG'
168
+ end
169
+
170
+ it 'handles an empty replacement string correctly' do
171
+ subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal(''))
172
+ expect(subject.value).to eq 'ABCFG'
173
+ end
174
+
175
+ it 'has the proper type' do
176
+ expect(subject.type).to eq(:string)
177
+ end
178
+ end
179
+
180
+ describe Dentaku::AST::StringFunctions::Concat do
181
+ it 'concatenates two strings' do
182
+ subject = described_class.new(literal('ABC'), literal('DEF'))
183
+ expect(subject.value).to eq 'ABCDEF'
184
+ end
185
+
186
+ it 'concatenates a string onto an empty string' do
187
+ subject = described_class.new(literal(''), literal('ABC'))
188
+ expect(subject.value).to eq 'ABC'
189
+ end
190
+
191
+ it 'concatenates an empty string onto a string' do
192
+ subject = described_class.new(literal('ABC'), literal(''))
193
+ expect(subject.value).to eq 'ABC'
194
+ end
195
+
196
+ it 'concatenates two empty strings' do
197
+ subject = described_class.new(literal(''), literal(''))
198
+ expect(subject.value).to eq ''
199
+ end
200
+
201
+ it 'has the proper type' do
202
+ expect(subject.type).to eq(:string)
203
+ end
204
+ end
205
+
206
+ describe Dentaku::AST::StringFunctions::Contains do
207
+ it 'checks for substrings' do
208
+ subject = described_class.new(literal('app'), literal('apple'))
209
+ expect(subject.value).to be_truthy
210
+ subject = described_class.new(literal('app'), literal('orange'))
211
+ expect(subject.value).to be_falsy
212
+ end
213
+
214
+ it 'has the proper type' do
215
+ expect(subject.type).to eq(:logical)
216
+ end
217
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/sum'
3
+ require 'dentaku'
4
+
5
+ describe 'Dentaku::AST::Function::Sum' do
6
+ it 'returns the sum of an array of Numeric values' do
7
+ result = Dentaku('SUM(1, x, 1.8)', x: 2.3)
8
+ expect(result).to eq(5.1)
9
+ end
10
+
11
+ it 'returns the sum of a single entry array of a Numeric value' do
12
+ result = Dentaku('SUM(x)', x: 2.3)
13
+ expect(result).to eq(2.3)
14
+ end
15
+
16
+ it 'returns the sum even if a String is passed' do
17
+ result = Dentaku('SUM(1, x, 1.8)', x: '2.3')
18
+ expect(result).to eq(5.1)
19
+ end
20
+
21
+ it 'returns the sum even if an array is passed' do
22
+ result = Dentaku('SUM(1, x, 2.3)', x: [4, 5])
23
+ expect(result).to eq(12.3)
24
+ end
25
+
26
+ it 'returns the sum of nested sums' do
27
+ result = Dentaku('SUM(1, x, SUM(4, 5))', x: '2.3')
28
+ expect(result).to eq(12.3)
29
+ end
30
+
31
+ context 'checking errors' do
32
+ let(:calculator) { Dentaku::Calculator.new }
33
+
34
+ it 'raises an error if no arguments are passed' do
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)
41
+ end
42
+ end
43
+ 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
@@ -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
data/spec/benchmark.rb ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dentaku'
4
+ require 'allocation_stats'
5
+ require 'benchmark'
6
+
7
+ puts "Dentaku version #{Dentaku::VERSION}"
8
+ puts "Ruby version #{RUBY_VERSION}"
9
+
10
+ with_duplicate_variables = [
11
+ "R1+R2+R3+R4+R5+R6",
12
+ {"R1" => 100000, "R2" => 0, "R3" => 200000, "R4" => 0, "R5" => 500000, "R6" => 0, "r1" => 100000, "r2" => 0, "r3" => 200000, "r4" => 0, "r5" => 500000, "r6" => 0}
13
+ ]
14
+
15
+ without_duplicate_variables = [
16
+ "R1+R2+R3+R4+R5+R6",
17
+ {"R1" => 100000, "R2" => 0, "R3" => 200000, "R4" => 0, "R5" => 500000, "R6" => 0}
18
+ ]
19
+
20
+ def test(args, custom_function: true)
21
+ calls = [ args ] * 100
22
+
23
+ 10.times do |i|
24
+
25
+ stats = nil
26
+ bm = Benchmark.measure do
27
+ stats = AllocationStats.trace do
28
+
29
+ calls.each do |formula, bound|
30
+
31
+ calculator = Dentaku::Calculator.new
32
+
33
+ if custom_function
34
+ calculator.add_function(
35
+ :sum,
36
+ :numeric,
37
+ ->(numbers) { numbers.inject(:+) }
38
+ )
39
+ end
40
+
41
+ calculator.evaluate(formula, bound)
42
+ end
43
+ end
44
+ end
45
+
46
+ puts " run #{i}: #{bm.total}"
47
+ puts stats.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text
48
+ end
49
+ end
50
+
51
+ case ARGV[0]
52
+ when '1'
53
+ puts "with duplicate (downcased) variables, with a custom function:"
54
+ test(with_duplicate_variables, custom_function: true)
55
+
56
+ when '2'
57
+ puts "with duplicate (downcased) variables, without a custom function:"
58
+ test(with_duplicate_variables, custom_function: false)
59
+
60
+ when '3'
61
+ puts "without duplicate (downcased) variables, with a custom function:"
62
+ test(without_duplicate_variables, custom_function: true)
63
+
64
+ when '4'
65
+ puts "with duplicate (downcased) variables, without a custom function:"
66
+ test(without_duplicate_variables, custom_function: false)
67
+
68
+ else
69
+ puts "select a run option (1-4)"
70
+ end
@@ -0,0 +1,241 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/bulk_expression_solver'
4
+ require 'dentaku/calculator'
5
+ require 'dentaku/exceptions'
6
+
7
+ RSpec.describe Dentaku::BulkExpressionSolver do
8
+ let(:calculator) { Dentaku::Calculator.new }
9
+
10
+ describe "#solve!" do
11
+ it "evaluates properly with variables, even if some in memory" do
12
+ expressions = {
13
+ weekly_fruit_budget: "weekly_apple_budget + pear * 4",
14
+ weekly_apple_budget: "apples * 7",
15
+ pear: "1"
16
+ }
17
+ solver = described_class.new(expressions, calculator.store(apples: 3))
18
+ expect(solver.solve!)
19
+ .to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
20
+ end
21
+
22
+ it "lets you know if a variable is unbound" do
23
+ expressions = {more_apples: "apples + 1"}
24
+ expect {
25
+ described_class.new(expressions, calculator).solve!
26
+ }.to raise_error(Dentaku::UnboundVariableError)
27
+ end
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
+
36
+ it "lets you know if the result is a div/0 error when dividing" do
37
+ expressions = {more_apples: "1/0"}
38
+ expect {
39
+ described_class.new(expressions, calculator).solve!
40
+ }.to raise_error(Dentaku::ZeroDivisionError)
41
+ end
42
+
43
+ it "lets you know if the result is a div/0 error when taking modulo" do
44
+ expressions = {more_apples: "1%0"}
45
+ expect {
46
+ described_class.new(expressions, calculator).solve!
47
+ }.to raise_error(Dentaku::ZeroDivisionError)
48
+ end
49
+
50
+ it "does not require keys to be parseable" do
51
+ expressions = { "the value of x, incremented" => "x + 1" }
52
+ solver = described_class.new(expressions, calculator.store("x" => 3))
53
+ expect(solver.solve!).to eq("the value of x, incremented" => 4)
54
+ end
55
+
56
+ it "allows self-referential formulas" do
57
+ expressions = { x: "x + 1" }
58
+ solver = described_class.new(expressions, calculator.store(x: 1))
59
+ expect(solver.solve!).to eq(x: 2)
60
+
61
+ expressions = { x: "y + 3", y: "x * 2" }
62
+ solver = described_class.new(expressions, calculator.store(x: 5, y: 3))
63
+ expect(solver.solve!).to eq(x: 6, y: 12) # x = 6 by the time y is calculated
64
+ end
65
+
66
+ it "does not execute functions unnecessarily" do
67
+ calls = 0
68
+ external = ->() { calls += 1 }
69
+ hash = {test: 'EXTERNAL()'}
70
+ calculator = Dentaku::Calculator.new
71
+ calculator.add_function(:external, :numeric, external)
72
+ calculator.solve(hash)
73
+ expect(calls).to eq(1)
74
+ end
75
+
76
+ it "evaluates expressions in hashes and arrays, and expands the results" do
77
+ calculator.store(
78
+ fruit_quantities: {
79
+ apple: 5,
80
+ pear: 9
81
+ },
82
+ fruit_prices: {
83
+ apple: 1.66,
84
+ pear: 2.50
85
+ }
86
+ )
87
+ expressions = {
88
+ weekly_budget: {
89
+ fruit: "weekly_budget.apples + weekly_budget.pears",
90
+ apples: "fruit_quantities.apple * discounted_fruit_prices.apple",
91
+ pears: "fruit_quantities.pear * discounted_fruit_prices.pear",
92
+ },
93
+ discounted_fruit_prices: {
94
+ apple: "round(fruit_prices.apple * discounts[0], 2)",
95
+ pear: "round(fruit_prices.pear * discounts[1], 2)"
96
+ },
97
+ discounts: ["0.4 * 2", "0.3 * 2"],
98
+ }
99
+ solver = described_class.new(expressions, calculator)
100
+
101
+ expect(solver.solve!).to eq(
102
+ weekly_budget: {
103
+ fruit: 20.15,
104
+ apples: 6.65,
105
+ pears: 13.50
106
+ },
107
+ discounted_fruit_prices: {
108
+ apple: 1.33,
109
+ pear: 1.50
110
+ },
111
+ discounts: [0.8, 0.6]
112
+ )
113
+ end
114
+ end
115
+
116
+ describe "#solve" do
117
+ it 'resolves capitalized keys when they are declared out of order' do
118
+ expressions = {
119
+ FIRST: "SECOND * 2",
120
+ SECOND: "THIRD * 2",
121
+ THIRD: 2,
122
+ }
123
+
124
+ result = described_class.new(expressions, calculator).solve
125
+
126
+ expect(result).to eq(
127
+ FIRST: 8,
128
+ SECOND: 4,
129
+ THIRD: 2
130
+ )
131
+ end
132
+
133
+ it "returns :undefined when variables are unbound" do
134
+ expressions = {more_apples: "apples + 1"}
135
+ expect(described_class.new(expressions, calculator).solve)
136
+ .to eq(more_apples: :undefined)
137
+ end
138
+
139
+ it "allows passing in a custom value to an error handler when a variable is unbound" do
140
+ expressions = {more_apples: "apples + 1"}
141
+ expect(described_class.new(expressions, calculator).solve { :foo })
142
+ .to eq(more_apples: :foo)
143
+ end
144
+
145
+ it "allows passing in a custom value to an error handler when there is a div/0 error" do
146
+ expressions = {more_apples: "1/0"}
147
+ expect(described_class.new(expressions, calculator).solve { :foo })
148
+ .to eq(more_apples: :foo)
149
+ end
150
+
151
+ it "allows passing in ast as expression" do
152
+ expressions = {more_apples: calculator.ast("1/0")}
153
+ expect(described_class.new(expressions, calculator).solve { :foo })
154
+ .to eq(more_apples: :foo)
155
+ end
156
+
157
+ it 'stores the recipient variable on the exception when there is a div/0 error' do
158
+ expressions = {more_apples: "1/0"}
159
+ exception = nil
160
+ described_class.new(expressions, calculator).solve do |ex|
161
+ exception = ex
162
+ end
163
+ expect(exception.recipient_variable).to eq('more_apples')
164
+ end
165
+
166
+ it 'stores the recipient variable on the exception when there is an unbound variable' do
167
+ expressions = {more_apples: "apples + 1"}
168
+ exception = nil
169
+ described_class.new(expressions, calculator).solve do |ex|
170
+ exception = ex
171
+ end
172
+ expect(exception.recipient_variable).to eq('more_apples')
173
+ end
174
+
175
+ it 'stores the recipient variable on the exception when there is an ArgumentError' do
176
+ expressions = {apples: "NULL", more_apples: "1 + apples"}
177
+ exception = nil
178
+ described_class.new(expressions, calculator).solve do |ex|
179
+ exception = ex
180
+ end
181
+ expect(exception.recipient_variable).to eq('more_apples')
182
+ end
183
+
184
+ it 'safely handles argument errors' do
185
+ expressions = {i: "a / 5 + d", a: "m * 12", d: "a + b"}
186
+ result = described_class.new(expressions, calculator.store(m: 3)).solve
187
+ expect(result).to eq(
188
+ i: :undefined,
189
+ d: :undefined,
190
+ a: 36,
191
+ )
192
+ end
193
+
194
+ it 'supports nested hashes of expressions using dot notation' do
195
+ expressions = {
196
+ a: "25",
197
+ b: {
198
+ c: "a / 5",
199
+ d: [3, 4, 5]
200
+ },
201
+ e: ["b.c + b.d[1]"],
202
+ f: "e[0] + 1"
203
+ }
204
+ results = described_class.new(expressions, calculator).solve
205
+ expect(results[:f]).to eq(10)
206
+ end
207
+
208
+ it 'uses stored values for expressions when they are known' do
209
+ calculator.store(Force: 50, Mass: 25)
210
+ expressions = {
211
+ Force: "Mass * Acceleration",
212
+ Mass: "Force / Acceleration",
213
+ Acceleration: "Force / Mass",
214
+ }
215
+ solver = described_class.new(expressions, calculator)
216
+ results = solver.solve
217
+ expect(results).to eq(Force: 50, Mass: 25, Acceleration: 2)
218
+ end
219
+
220
+ it 'solves all array expressions for which context exists, returning :undefined for the rest' do
221
+ calculator.store(first: 1, equation: 3)
222
+ system = {'key' => ['first * equation', 'second * equation'] }
223
+ solver = described_class.new(system, calculator)
224
+ expect(solver.dependencies).to eq('key' => ['second'])
225
+ results = solver.solve
226
+ expect(results).to eq('key' => [3, :undefined])
227
+ expect { solver.solve! }.to raise_error(Dentaku::UnboundVariableError)
228
+ end
229
+
230
+ it do
231
+ calculator.store(val: nil)
232
+ expressions = {
233
+ a: 'IF(5 / 0 > 0, 100, 1000)',
234
+ b: 'IF(val = 0, 0, IF(val > 0, 0, 0))'
235
+ }
236
+ solver = described_class.new(expressions, calculator)
237
+ results = solver.solve
238
+ expect(results).to eq(a: :undefined, b: :undefined)
239
+ end
240
+ end
241
+ end